Compare commits

..
Author SHA1 Message Date
BZLZHH 4bf9c1cae6 [Perf] (MG_Backend/DirectVulkanTMP): Optimize buffer uploads with staging ring + batched copies. 2026-02-21 14:04:05 +08:00
BZLZHH c121328c98 [Feat] (MG_Backend/DirectVulkanTMP): Migrate to Vulkan 1.3 and use dynamic rendering. 2026-02-21 13:17:05 +08:00
BZLZHH 5fe5aae138 [Perf] (MG_Backend/DirectVulkan): Batch layout transitions and Reuse staging buffers. 2026-02-20 22:04:50 +08:00
BZLZHH d2485a3fbe [Improvement] (MG_Backend/DirectVulkanTMP): Rename current DirectVulkan to DirectVulkanTMP. 2026-02-20 16:33:26 +08:00
BZLZHH 3fe0999550 [Misc] (MG_Backend/DirectVulkan): Remove "VulkanRenerer". 2026-02-20 16:27:02 +08:00
BZLZHH 39eb6a9026 [Feat] (MG_Backend/DirectVulkan|MG_Util): Get Vulkan device info. 2026-02-20 16:21:28 +08:00
BZLZHH 99c1697fba Merge branch 'dev' into Feat/Backend-Direct-Vulkan-TMP 2026-02-20 15:27:21 +08:00
BZLZHH 5db5545b33 Merge branch 'dev' into Feat/Backend-Direct-Vulkan-TMP 2026-02-19 02:04:57 +08:00
BZLZHH e63eebf425 [Fix] (MG_Backend/DirectVulkan): Correct viewport setting. 2026-02-16 18:29:03 +08:00
BZLZHH 03d3fcdc66 [Feat|Fix] (MG_Backend): Implement BlitFramebuffer & DrawElementsBaseVertex. 2026-02-16 17:12:33 +08:00
BZLZHH 40fc317f69 [Fix] (MG_Backend/DirectVulkan): Add shader position transforms & correct namespace name. 2026-02-16 14:06:34 +08:00
BZLZHH 3bafdee45d [Feat] (MG_Backend/DirectVulkan): Implement DV::TmpImpl and use. 2026-02-11 02:01:08 +08:00
BZLZHH 188275c004 Merge branch 'Feat/Backend-Direct-Vulkan' of github.com:MobileGL-Dev/MobileGL into Feat/Backend-Direct-Vulkan 2026-02-09 21:03:10 +08:00
554 changed files with 16254 additions and 150192 deletions
+2
View File
@@ -14,6 +14,7 @@ bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase, bugprone-inaccurate-erase,
bugprone-incorrect-roundings, bugprone-incorrect-roundings,
bugprone-integer-division, bugprone-integer-division,
bugprone-lambda-function-name,
bugprone-macro-parentheses, bugprone-macro-parentheses,
bugprone-macro-repeated-side-effects, bugprone-macro-repeated-side-effects,
bugprone-misplaced-operator-in-strlen-in-alloc, bugprone-misplaced-operator-in-strlen-in-alloc,
@@ -62,6 +63,7 @@ cert-str34-c,
cppcoreguidelines-interfaces-global-init, cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-narrowing-conversions, cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-pro-type-member-init, cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-slicing, cppcoreguidelines-slicing,
google-default-arguments, google-default-arguments,
google-runtime-operator, google-runtime-operator,
-4
View File
@@ -1,4 +0,0 @@
tools/trace_replay/fixtures/*.tgz filter=lfs diff=lfs merge=lfs -text
tools/trace_replay/fixtures/*.png filter=lfs diff=lfs merge=lfs -text
tools/trace_replay/fixtures/openra.tgz -filter -diff -merge -text
tools/trace_replay/fixtures/openra.0000031249.png -filter -diff -merge -text
-246
View File
@@ -1,246 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: $0 <trace-case> [fixture-dir]" >&2
exit 2
fi
case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
# 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}")"
# 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
echo "Fixture files for ${case_name} are stored in Git: ${include}"
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 "fixture should not be stored as an LFS pointer: ${file}" >&2
exit 1
fi
done
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
}
# Files no mirror could serve, even after retrying every mirror. Only these fall
# back to Git LFS, so a mirror that served the rest of the case still spares
# GitHub the bandwidth for those files.
mirror_failures=()
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
local name
local url
local base
local fetched=0
name="$(basename "${file}")"
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
mirror_failures+=("${file}")
fi
done
[ "${#mirror_failures[@]}" -eq 0 ]
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
git lfs install --local
git lfs pull --include="${fallback_include}" --exclude=""
fi
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
-75
View File
@@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <aapt2> <plugin-apk> <trace-apk>" >&2
exit 64
fi
aapt2=$1
plugin_apk=$2
trace_apk=$3
require() {
local needle=$1
local content=$2
local description=$3
if ! grep -Fq -- "$needle" <<<"$content"; then
echo "::error::Missing ${description}: ${needle}" >&2
exit 1
fi
}
for apk in "$plugin_apk" "$trace_apk"; do
[[ -f "$apk" ]] || { echo "::error::APK not found: $apk" >&2; exit 1; }
done
plugin_manifest=$("$aapt2" dump xmltree --file AndroidManifest.xml "$plugin_apk")
plugin_resources=$("$aapt2" dump resources "$plugin_apk")
plugin_resource_text=$(tr -d '"' <<<"$plugin_resources")
trace_manifest=$("$aapt2" dump xmltree --file AndroidManifest.xml "$trace_apk")
plugin_contents=$(unzip -Z1 "$plugin_apk")
require 'top.mobilegl.plugin' "$plugin_manifest" 'plugin package name'
require 'MobileGL' "$plugin_manifest" 'plugin label'
require 'fclPlugin' "$plugin_manifest" 'legacy plugin marker'
require 'fclPlugin_V2' "$plugin_manifest" 'V2 plugin marker'
require 'LIBGL_ES=3:POJAV_RENDERER=opengles3:MOBILEGL_BACKEND_TYPE=DirectGLES' "$plugin_manifest" 'V1 DirectGLES fallback'
require 'string/config' "$plugin_resources" 'V2 renderer configuration resource'
require '{displayName:MobileGL,rendererId:opengles3' "$plugin_resource_text" 'V2 MobileGL entry and renderer ID'
require 'rendererGLPath:**|libMobileGL.so' "$plugin_resource_text" 'V2 GL library path'
require 'rendererEGLPath:**|libMobileGL.so' "$plugin_resource_text" 'V2 EGL library path'
require 'key:LIBGL_ES,value:3' "$plugin_resource_text" 'V2 fixed LIBGL_ES variable'
require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable'
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
exit 1
fi
if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
echo '::error::Plugin APK does not contain libMobileGL.so' >&2
exit 1
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
exit 1
fi
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
-560
View File
@@ -1,560 +0,0 @@
name: MobileGL APK
on:
push:
branches:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-version: 8.10.2
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache
run: |
sudo apt-get update
sudo apt-get install -y ccache
ccache --version
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Install Android NDK
run: |
sdkmanager "ndk;27.3.13750724"
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
- name: Update glslang external sources
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Build plugin APK
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Download ANGLE x86_64 libraries
run: |
angle_dir="android-plugin/app/src/trace/jniLibs/x86_64"
rm -rf "${angle_dir}"
mkdir -p "${angle_dir}"
package_angle_variant() {
variant="$1"
commit="$2"
egl_sha="$3"
gles_sha="$4"
source_dir="${RUNNER_TEMP}/mobilegl-angle-${variant}"
base="https://raw.githubusercontent.com/FCL-Team/FoldCraftLauncher/${commit}/FCLauncher/src/main/jniLibs/x86_64"
mkdir -p "${source_dir}"
curl -L --fail --retry 3 -o "${source_dir}/libEGL_angle.so" "${base}/libEGL_angle.so"
curl -L --fail --retry 3 -o "${source_dir}/libGLESv2_angle.so" "${base}/libGLESv2_angle.so"
echo "${egl_sha} ${source_dir}/libEGL_angle.so" | sha256sum -c -
echo "${gles_sha} ${source_dir}/libGLESv2_angle.so" | sha256sum -c -
for library in libEGL_angle libGLESv2_angle; do
filename="${library}_${variant}.so"
cp "${source_dir}/${library}.so" "${angle_dir}/${filename}"
done
}
package_angle_variant \
ec889e6ea831 \
f2a3d510dffd8f6540a52e1a7d0c5787d151075b \
c41828768d089899fa058ec0bee711a91be88347f29bdb935223da6be1149c40 \
e4f820d99f94365c66df868c7740fef142fe5c0cd7c941790a9e30638857ca4d
package_angle_variant \
90a62123d794 \
bdcc96ac11c79001018ae4375eb73cb54a9f682f \
d0f4298ccc770cc801fc52e21733521646161e8a4adb3bd0052d9a1b57ee0ca8 \
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
- name: Build retrace APK
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Verify APK metadata and packaging
run: |
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
plugin_apk="android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk"
trace_apk="android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${plugin_apk}"
test -f "${trace_apk}"
bash .github/scripts/validate-plugin-apks.sh "$AAPT2" "$plugin_apk" "$trace_apk"
- name: Verify signed APKs
run: |
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner -type f | sort -V | tail -n 1)"
mapfile -t APKS < <(printf '%s\n' \
"android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${GITHUB_SHA}.apk" \
"android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk")
for APK in "${APKS[@]}"; do
if [[ ! -f "$APK" ]]; then
echo "::error::Expected release APK was not produced: $APK"
exit 1
fi
done
for APK in "${APKS[@]}"; do
if [[ "$APK" == *-unsigned.apk ]]; then
echo "::error::Unsigned release APK produced: $APK"
exit 1
fi
"$APKSIGNER" verify --verbose "$APK"
done
- name: Upload plugin APK
uses: actions/upload-artifact@v7
with:
name: MobileGL-plugin-${{ env.date_today }}-${{ github.sha }}
path: android-plugin/app/build/outputs/apk/plugin/release/MobileGL-plugin-release-${{ github.sha }}.apk
archive: false
if-no-files-found: error
- name: Upload retrace APK
uses: actions/upload-artifact@v7
with:
name: MobileGL-retrace-apk-${{ env.date_today }}-${{ github.sha }}
path: android-plugin/app/build/outputs/apk/trace/release/MobileGL-plugin-trace-release-${{ github.sha }}.apk
archive: false
if-no-files-found: error
trace-cases:
name: trace case matrix
runs-on: ubuntu-latest
needs: build
outputs:
android: ${{ steps.trace-cases.outputs.android }}
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Load trace cases
id: trace-cases
run: |
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
runs-on: ubuntu-latest
needs: trace-cases
strategy:
fail-fast: false
max-parallel: 4
matrix:
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Fetch trace fixture
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
stage_dir="trace-fixtures/${safe_case}"
mkdir -p "${stage_dir}"
python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
while IFS= read -r file; do
cp "${file}" "${stage_dir}/"
done
- name: Upload trace fixture
uses: actions/upload-artifact@v7
with:
name: MobileGL-trace-fixture-${{ matrix.case }}
path: trace-fixtures/**
if-no-files-found: error
android-avd:
name: android avd image
runs-on: ubuntu-latest
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
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ 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'
run: |
sh android-plugin/run-avd-ci.sh create \
--api-level 35 \
--target google_apis \
--arch x86_64 \
--profile pixel_6 \
--avd-name "${AVD_NAME}"
retrace:
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
runs-on: ubuntu-latest
needs:
- build
- android-avd
- trace-cases
- trace-fixtures
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
timeout-minutes: 75
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
matrix:
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 8
- name: Checkout repo
uses: actions/checkout@v6
- name: Download trace fixture
uses: actions/download-artifact@v8
with:
name: MobileGL-trace-fixture-${{ matrix.case.name }}
path: trace-fixture-download
- name: Install trace fixture
run: |
mkdir -p tools/trace_replay/fixtures
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
echo "EMULATOR_LOG=${RUNNER_TEMP}/mobilegl-emulator.log" >> "$GITHUB_ENV"
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
- name: Restore Android AVD cache
id: android-avd-cache
uses: actions/cache/restore@v5
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ 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
with:
name: MobileGL-plugin-trace-release-${{ github.sha }}.apk
path: android-retrace-apks
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
run: |
sh android-plugin/run-avd-ci.sh create \
--api-level 35 \
--target google_apis \
--arch x86_64 \
--profile pixel_6 \
--avd-name "${AVD_NAME}"
- name: Launch Emulator
run: |
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
- name: Retrace and validate
env:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
extra_retrace_args=()
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
fi
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush)
fi
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()
run: |
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
mkdir -p "${result_dir}"
if [ -s "${{ matrix.case.golden }}" ]; then
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
fi
if [ -n "${{ matrix.case.alternate_golden || '' }}" ] && [ -s "${{ matrix.case.alternate_golden || '' }}" ]; then
cp "${{ matrix.case.alternate_golden || '' }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-alternate-golden.png"
fi
- name: Collect emulator diagnostics
if: always()
run: |
mkdir -p android-retrace-result/diagnostics
adb devices -l > android-retrace-result/diagnostics/adb-devices.txt || true
timeout 30 adb logcat -d -t 1000 > android-retrace-result/diagnostics/logcat.txt || true
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
fi
- name: Stop Emulator
if: always()
run: |
sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}"
- name: Upload Android retrace result
if: always()
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
retrace-summary:
name: retrace summary
runs-on: ubuntu-latest
needs: retrace
if: always()
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Download Android retrace results
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-*
path: retrace-artifacts
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts \
--output-dir android-retrace-summary \
--title "MobileGL Android retrace overview" \
--group-label "Android Emulator" \
--html mobilegl-android-retrace-overview.html
- name: Upload Android retrace summary
uses: actions/upload-artifact@v7
with:
path: android-retrace-summary/mobilegl-android-retrace-overview.html
archive: false
if-no-files-found: error
remove-artifact-clutter:
name: remove artifact clutter
runs-on: ubuntu-latest
needs: retrace-summary
if: always()
permissions:
actions: write
steps:
- name: Delete intermediate Android retrace artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else
echo "All retrace jobs succeeded; no fixtures need to be retained."
fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
case_name="${artifact_name#MobileGL-trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
fi
echo "Deleting ${artifact_name} (${artifact_id})"
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
((deleted += 1))
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
)
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
+57
View File
@@ -0,0 +1,57 @@
name: Benchmark
on:
push:
branches:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
jobs:
benchmark:
runs-on: ubuntu-latest
env:
# BENCH_ROOT: ${{github.workspace}}/MobileGL/MG_Benchmark
BENCH_ROOT: ${{github.workspace}}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: true
- name: Get CMake
uses: lukka/get-cmake@latest
- name: Update glslang external sources
working-directory: ${{env.BENCH_ROOT}}/3rdparty/glslang
run: python update_glslang_sources.py
- name: Install clang-20
run: |
sudo apt-get update
sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev
- name: Show installed toolchain
run: |
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' || true
- name: Configure CMake
working-directory: ${{env.BENCH_ROOT}}
run: cmake -S . -B build-bench -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON -DBENCHMARK_ENABLE_TESTING=OFF -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build
working-directory: ${{env.BENCH_ROOT}}/build-bench
run: cmake --build .
- name: Benchmark
working-directory: ${{env.BENCH_ROOT}}/build-bench/MobileGL/MG_Benchmark
run: ctest -V -C Release
+24 -530
View File
@@ -6,568 +6,62 @@ on:
- dev - dev
- Feat/Backend-Direct-GLES - Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan - Feat/Backend-Direct-Vulkan
workflow_dispatch:
jobs: jobs:
build-linux:
runs-on: ubuntu-latest
env:
BUILD_DIR: build-linux
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
- name: Show installed toolchain
run: |
ccache --version
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
- name: Configure CMake
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
BUILD_TYPE=Debug
else
BUILD_TYPE=Release
fi
cmake -S . -B "${BUILD_DIR}" -G Ninja \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=ON \
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
-DBENCHMARK_ENABLE_TESTING=OFF \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Package Linux runtime
run: |
mkdir -p ci-artifacts
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
tar \
--exclude='*/CMakeFiles' \
--exclude='*.o' \
--exclude='*.a' \
--exclude='*.ninja*' \
--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[@]}"
- name: Upload Linux runtime
uses: actions/upload-artifact@v7
with:
name: mobilegl-linux-runtime
path: ci-artifacts/mobilegl-linux-runtime.tgz
if-no-files-found: error
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-linux
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: tar -xzf mobilegl-linux-runtime.tgz
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Test
working-directory: build-linux
run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L unit --no-tests=error
else
ctest --output-on-failure -L unit --no-tests=error
fi
benchmark:
runs-on: ubuntu-latest
needs: build-linux
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
# libegl-mesa0 is the EGL vendor library itself: DriverBench brings up a
# real GL context, and libegl1 is only glvnd's dispatch. It normally
# arrives as a Recommends of libegl1, which is too quiet a dependency for
# the one job that needs a working driver.
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: tar -xzf mobilegl-linux-runtime.tgz
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Benchmark
working-directory: build-linux
run: ctest -V -C Release -L benchmark --no-tests=error
build-retrace:
runs-on: ubuntu-latest
needs:
- build-linux
- test
- benchmark
env: env:
BUILD_DIR: build-retrace # TEST_ROOT: ${{github.workspace}}/MobileGL/MG_Test
CCACHE_BASEDIR: ${{ github.workspace }} TEST_ROOT: ${{github.workspace}}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_LIBRARY: ${{ github.workspace }}/build-linux/libMobileGL.so
steps: steps:
- name: Set Swap Space - name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0 uses: pierotofy/set-swap-space@master
with: with:
swap-size-gb: 32 swap-size-gb: 32
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: true
- name: Get CMake - name: Get CMake
uses: lukka/get-cmake@v4.3.3 uses: lukka/get-cmake@latest
- name: Restore ccache
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources - name: Update glslang external sources
working-directory: 3rdparty/glslang working-directory: ${{env.TEST_ROOT}}/3rdparty/glslang
run: python update_glslang_sources.py run: python update_glslang_sources.py
- name: Install dependencies - name: Install clang-20
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build sudo apt-get install -y clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev
- name: Show installed toolchain - name: Show installed toolchain
run: | run: |
ccache --version
clang-20 --version clang-20 --version
clang++-20 --version clang++-20 --version
ld.lld-20 --version || ld.lld --version || true ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true dpkg -l 'libc++*' || true
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Unpack Linux runtime
run: |
tar -xzf mobilegl-linux-runtime.tgz
test -f "${MOBILEGL_LIBRARY}"
- name: Configure CMake - name: Configure CMake
working-directory: ${{env.TEST_ROOT}}
run: | run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then
BUILD_TYPE=Debug cmake -S . -B build-test -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Debug -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_BENCHMARK=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5
else else
BUILD_TYPE=Release cmake -S . -B build-test -G Ninja -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_BENCHMARK=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5
fi fi
- name: Build
working-directory: ${{env.TEST_ROOT}}/build-test
run: cmake --build .
cmake -S . -B "${BUILD_DIR}" -G Ninja \ - name: Test
-DCMAKE_C_COMPILER=clang-20 \ working-directory: ${{env.TEST_ROOT}}/build-test/MobileGL/MG_Test
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=OFF \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_TRACE_REPLAY=ON \
-DMOBILEGL_TRACE_REPLAY_MOBILEGL_LIBRARY="${MOBILEGL_LIBRARY}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build trace replay
run: cmake --build "${BUILD_DIR}" --target mobilegl_trace_replay --parallel "$(nproc)"
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Normalize CTest command paths
run: | run: |
python - <<'PY' if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" == "true" ]; then
from pathlib import Path ctest -V
import re
for path in Path('build-retrace').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Package trace replay
run: |
mkdir -p ci-artifacts
tar -czf ci-artifacts/mobilegl-trace-replay.tgz \
build-retrace/tools/trace_replay/mobilegl_trace_replay \
build-retrace/tools/trace_replay/CTestTestfile.cmake
- name: Upload trace replay
uses: actions/upload-artifact@v7
with:
name: mobilegl-trace-replay
path: ci-artifacts/mobilegl-trace-replay.tgz
if-no-files-found: error
trace-cases:
name: trace case matrix
runs-on: ubuntu-latest
needs:
- test
- benchmark
outputs:
names: ${{ steps.trace-cases.outputs.names }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Load trace cases
id: trace-cases
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
runs-on: ubuntu-latest
needs: trace-cases
strategy:
fail-fast: false
max-parallel: 4
matrix:
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Fetch trace fixture
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
stage_dir="trace-fixtures/${safe_case}"
mkdir -p "${stage_dir}"
python3 tools/trace_replay/trace_cases.py --format fixture-files --case '${{ matrix.case }}' |
while IFS= read -r file; do
cp "${file}" "${stage_dir}/"
done
- name: Upload trace fixture
uses: actions/upload-artifact@v7
with:
name: trace-fixture-${{ matrix.case }}
path: trace-fixtures/**
if-no-files-found: error
retrace:
name: retrace (${{ matrix.backend }}, ${{ matrix.case }})
runs-on: ubuntu-latest
needs:
- build-linux
- build-retrace
- trace-cases
- trace-fixtures
if: ${{ always() && needs.build-linux.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
backend:
- DirectGLES
- DirectVulkan
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 16
- name: Checkout repo
uses: actions/checkout@v6
- name: Download trace fixture
uses: actions/download-artifact@v8
with:
name: trace-fixture-${{ matrix.case }}
path: trace-fixture-download
- name: Install trace fixture
run: |
mkdir -p tools/trace_replay/fixtures
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers
test -e /usr/lib/x86_64-linux-gnu/libEGL.so
test -e /usr/lib/x86_64-linux-gnu/libGLESv2.so
- name: Download Linux runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime
path: .
- name: Download trace replay
uses: actions/download-artifact@v8
with:
name: mobilegl-trace-replay
path: .
- name: Unpack retrace runtime
run: |
tar -xzf mobilegl-linux-runtime.tgz
tar -xzf mobilegl-trace-replay.tgz
test -f build-linux/libMobileGL.so
test -f build-retrace/tools/trace_replay/mobilegl_trace_replay
- name: Retrace and validate
working-directory: build-retrace/tools/trace_replay
run: |
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
if: always()
uses: actions/upload-artifact@v7
with:
name: retrace-result-${{ matrix.backend }}-${{ matrix.case }}
path: |
build-retrace/tools/trace_replay/${{ matrix.case }}/actual-images/**
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
if-no-files-found: warn
retrace-summary:
name: retrace summary
runs-on: ubuntu-latest
needs: retrace
if: ${{ always() && needs.retrace.result != 'skipped' }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Set artifact metadata
run: |
echo "date_today=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Download retrace results
uses: actions/download-artifact@v8
with:
pattern: retrace-result-*
path: retrace-artifacts
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts \
--output-dir retrace-summary \
--title "MobileGL Linux retrace overview" \
--group-label "Linux" \
--html mobilegl-linux-retrace-overview.html
- name: Upload retrace summary
uses: actions/upload-artifact@v7
with:
path: retrace-summary/mobilegl-linux-retrace-overview.html
archive: false
if-no-files-found: error
remove-artifact-clutter:
name: remove artifact clutter
runs-on: ubuntu-latest
needs: retrace-summary
if: always()
permissions:
actions: write
steps:
- name: Delete intermediate Linux retrace artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else else
echo "All retrace jobs succeeded; no fixtures need to be retained." ctest
fi fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
if [[ "${artifact_name}" == trace-fixture-* ]]; then
case_name="${artifact_name#trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
fi
echo "Deleting ${artifact_name} (${artifact_id})"
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
((deleted += 1))
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
)
echo "Deleted ${deleted} intermediate Linux artifact(s); retained ${retained} failed-retrace fixture(s)."
+1 -13
View File
@@ -14,16 +14,4 @@ MobileGLCodeManager
.vscode .vscode
.clangd .clangd
MobileGL/MG_Test/build MobileGL/MG_Test/build
/build_* /build_*
/cmake-build*
.idea
MobileGL/MG*/build*
MobileGL/MG*/cmake-build*
/android-plugin/.gradle
/android-plugin/build
/android-plugin/app/build
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
__pycache__/
*.py[cod]
-21
View File
@@ -16,24 +16,3 @@
[submodule "3rdparty/xxHash"] [submodule "3rdparty/xxHash"]
path = 3rdparty/xxHash path = 3rdparty/xxHash
url = https://github.com/Cyan4973/xxHash.git url = https://github.com/Cyan4973/xxHash.git
[submodule "3rdparty/VulkanMemoryAllocator"]
path = 3rdparty/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "3rdparty/Vulkan-Utility-Libraries"]
path = 3rdparty/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
[submodule "3rdparty/Vulkan-Headers"]
path = 3rdparty/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "3rdparty/SPIRV-Reflect"]
path = 3rdparty/SPIRV-Reflect
url = https://github.com/KhronosGroup/SPIRV-Reflect.git
[submodule "3rdparty/apitrace"]
path = 3rdparty/apitrace
url = https://github.com/MobileGL-Dev/apitrace.git
[submodule "3rdparty/asio"]
path = 3rdparty/asio
url = https://github.com/chriskohlhoff/asio.git
[submodule "3rdparty/libfork"]
path = 3rdparty/libfork
url = https://github.com/ConorWilliams/libfork.git
Submodule 3rdparty/apitrace deleted from 10935bb5e4
-1
Submodule 3rdparty/asio deleted from 8806a6803c
-1
Submodule 3rdparty/libfork deleted from 9b2b844a5f
+13 -254
View File
@@ -4,27 +4,15 @@ project("MobileGL")
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON ) option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON ) option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
# skips cleanly where there is none. Registered under the `integration-gpu`
# ctest label so a run can select or exclude them.
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON ) option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF) option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
if (ANDROID) if (ANDROID)
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE) set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE) set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
endif() endif()
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF) if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
# Check if ThinLTO or LTO is suppported # Check if ThinLTO or LTO is suppported
include(CheckIPOSupported) include(CheckIPOSupported)
include(CheckCCompilerFlag) include(CheckCCompilerFlag)
@@ -116,20 +104,9 @@ set(SPIRV_CROSS_ENABLE_CPP OFF CACHE BOOL "Disable C++ API target" FORCE)
set(SPIRV_CROSS_CLI OFF CACHE BOOL "Disable CLI binary" FORCE) set(SPIRV_CROSS_CLI OFF CACHE BOOL "Disable CLI binary" FORCE)
set(SPIRV_CROSS_STATIC ON CACHE BOOL "Prefer static libs" FORCE) set(SPIRV_CROSS_STATIC ON CACHE BOOL "Prefer static libs" FORCE)
set(SPIRV_REFLECT_EXECUTABLE OFF CACHE BOOL "Build spirv-reflect executable" FORCE)
set(SPIRV_REFLECT_STATIC_LIB ON CACHE BOOL "Build a SPIRV-Reflect static library" FORCE)
set(SPIRV_REFLECT_BUILD_TESTS OFF CACHE BOOL "Build the SPIRV-Reflect test suite" FORCE)
set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging" FORCE)
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
# add_subdirectory(3rdparty/DiligentCore) # add_subdirectory(3rdparty/DiligentCore)
add_subdirectory(3rdparty/glslang) add_subdirectory(3rdparty/glslang)
add_subdirectory(3rdparty/SPIRV-Cross) add_subdirectory(3rdparty/SPIRV-Cross)
add_subdirectory(3rdparty/VulkanMemoryAllocator)
add_subdirectory(3rdparty/Vulkan-Headers)
add_subdirectory(3rdparty/Vulkan-Utility-Libraries)
add_subdirectory(3rdparty/SPIRV-Reflect)
set(XXHASH_BUILD_XXHSUM OFF) set(XXHASH_BUILD_XXHSUM OFF)
option(BUILD_SHARED_LIBS OFF) option(BUILD_SHARED_LIBS OFF)
@@ -150,13 +127,9 @@ endif ()
set(SOURCE_FILES set(SOURCE_FILES
MobileGL/Init.cpp MobileGL/Init.cpp
MobileGL/GlobalObjects.cpp MobileGL/GlobalObjects.cpp
MobileGL/ConfigLoader.cpp
MobileGL/MG_Util/Debug/Log.cpp MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
MobileGL/MG_Util/Math/VectorTypes.cpp MobileGL/MG_Util/Math/VectorTypes.cpp
MobileGL/MG_Util/Metrics/TextureMetrics.cpp MobileGL/MG_Util/Metrics/TextureMetrics.cpp
@@ -185,40 +158,22 @@ set(SOURCE_FILES
MobileGL/MG_Util/Converters/GLToMG/RenderStateEnumConverter.cpp MobileGL/MG_Util/Converters/GLToMG/RenderStateEnumConverter.cpp
MobileGL/MG_Util/Converters/GLToMG/ProgramEnumConverter.cpp MobileGL/MG_Util/Converters/GLToMG/ProgramEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToVk/RenderStateEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToVk/TextureEnumConverter.cpp
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.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/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
@@ -232,8 +187,6 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
@@ -244,7 +197,6 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
MobileGL/MG_Impl/Init.cpp MobileGL/MG_Impl/Init.cpp
MobileGL/MG_Impl/GetProcAddress.cpp MobileGL/MG_Impl/GetProcAddress.cpp
@@ -256,30 +208,17 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp MobileGL/MG_Backend/DirectVulkanTMP/DirectVulkanTMP.cpp
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp MobileGL/MG_Backend/DirectVulkanTMP/TmpImpl.cpp
MobileGL/MG_Backend/DirectVulkan/VmaImpl.cpp MobileGL/MG_Backend/DirectVulkanTMP/BackendObject_DirectVulkanTMP.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp MobileGL/MG_Backend/DirectVulkanTMP/Renderer/VulkanContext.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp MobileGL/MG_Backend/DirectVulkanTMP/Renderer/SwapchainManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp MobileGL/MG_Backend/DirectVulkanTMP/Renderer/PipelineManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp MobileGL/MG_Backend/DirectVulkanTMP/Renderer/FrameContext.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp MobileGL/MG_Backend/DirectVulkanTMP/Managers/ProgramManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateBuilder.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_State/GLState/Core.cpp MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp MobileGL/MG_State/GLState/BufferState/BufferState.cpp
MobileGL/MG_State/GLState/BufferState/BufferObject.cpp MobileGL/MG_State/GLState/BufferState/BufferObject.cpp
@@ -295,11 +234,7 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
MobileGL/MG_State/GLState/TextureState/TextureState.cpp MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
MobileGL/MG_State/GLState/RenderState/RenderState.cpp MobileGL/MG_State/GLState/RenderState/RenderState.cpp
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
@@ -310,58 +245,14 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
) )
if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp
MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp
)
endif()
if (ANDROID)
list(APPEND SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
# merged pthread into libc, so it links without asking, but the NDK and musl are not
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
find_package(Threads REQUIRED)
set(MOBILEGL_LINK_LIBRARIES set(MOBILEGL_LINK_LIBRARIES
glslang::glslang glslang::glslang
spirv-cross-c spirv-cross-c
SPIRV-Tools-opt SPIRV-Tools-opt
SPIRV-Tools SPIRV-Tools
xxHash::xxhash xxHash::xxhash
GPUOpen::VulkanMemoryAllocator
Vulkan::UtilityHeaders
spirv-reflect-static
Threads::Threads
) )
set(MOBILEGL_COMPILE_DEF
-DVMA_STATIC_VULKAN_FUNCTIONS=0
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
-DVMA_VULKAN_VERSION=1001000
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
# rather than per-target so the shared library and the _s static target agree.
-DASIO_STANDALONE
-DASIO_NO_DEPRECATED
)
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
set(MOBILEGL_INCLUDE_DIR set(MOBILEGL_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/MobileGL ${CMAKE_SOURCE_DIR}/MobileGL
@@ -369,31 +260,12 @@ set(MOBILEGL_INCLUDE_DIR
${spirv-tools_SOURCE_DIR}/include ${spirv-tools_SOURCE_DIR}/include
${spirv-tools_BINARY_DIR} ${spirv-tools_BINARY_DIR}
${SPIRV-Headers_SOURCE_DIR}/include ${SPIRV-Headers_SOURCE_DIR}/include
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
# The second shader-compile execution engine (MOBILEGL_ASYNC_POOL=libfork), on the
# same terms as Asio above: header-only, no add_subdirectory (its CMakeLists only
# declares an INTERFACE target plus install/test scaffolding we do not want), no link
# target, and reachable from exactly one translation unit. libfork's own
# target_compile_features asks for cxx_std_23, which this project already sets
# globally, so its C++20 coroutines need no per-source standard override.
${CMAKE_SOURCE_DIR}/3rdparty/libfork/include
) )
add_library(${CMAKE_PROJECT_NAME} SHARED add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES} ${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") if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default C_VISIBILITY_PRESET default
@@ -413,41 +285,10 @@ target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
) )
target_link_libraries(${CMAKE_PROJECT_NAME} target_link_libraries(${CMAKE_PROJECT_NAME}
PUBLIC PRIVATE
${MOBILEGL_LINK_LIBRARIES} ${MOBILEGL_LINK_LIBRARIES}
) )
target_compile_definitions(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
)
if(UNIX AND NOT APPLE AND NOT ANDROID)
foreach(MOBILEGL_LOADER_ALIAS
libEGL.so libEGL.so.1)
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/${MOBILEGL_LOADER_ALIAS}"
COMMENT "Creating ${MOBILEGL_LOADER_ALIAS} alias for Linux GL/EGL loaders"
)
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) if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES} ${SOURCE_FILES}
@@ -472,15 +313,9 @@ if(NOT ANDROID)
) )
target_link_libraries(${CMAKE_PROJECT_NAME}_s target_link_libraries(${CMAKE_PROJECT_NAME}_s
PUBLIC PRIVATE
${MOBILEGL_LINK_LIBRARIES} ${MOBILEGL_LINK_LIBRARIES}
) )
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
)
endif() endif()
if (TRACY_ENABLE) if (TRACY_ENABLE)
@@ -498,62 +333,7 @@ if (ANDROID)
) )
endif() endif()
if (APPLE AND NOT MOBILEGL_IOS) if (NOT ANDROID)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
objc)
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
objc)
endif()
endif()
if (APPLE AND MOBILEGL_IOS)
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
"-framework QuartzCore"
objc)
if (MOBILEGL_VULKAN_LIBRARY)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC "${MOBILEGL_VULKAN_LIBRARY}")
endif()
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
"-framework QuartzCore"
objc)
if (MOBILEGL_VULKAN_LIBRARY)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC "${MOBILEGL_VULKAN_LIBRARY}")
endif()
endif()
endif()
if (NOT ANDROID AND NOT MOBILEGL_IOS)
find_package(Vulkan) find_package(Vulkan)
if (Vulkan_FOUND) if (Vulkan_FOUND)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers) target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers)
@@ -561,33 +341,12 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR}) target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR}) target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR})
endif () endif ()
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) if (MOBILEGL_BUILD_TEST)
add_subdirectory(MobileGL/MG_Test) add_subdirectory(MobileGL/MG_Test)
endif() endif()
# After MG_Test so googletest is already available when the unit tests are
# built; the module fetches its own copy when they are not.
if (MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
if (MOBILEGL_BUILD_BENCHMARK) if (MOBILEGL_BUILD_BENCHMARK)
add_subdirectory(MobileGL/MG_Benchmark) add_subdirectory(MobileGL/MG_Benchmark)
endif() endif()
if (MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
endif() endif()
+1 -127
View File
@@ -14,134 +14,8 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL"; inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core"; inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)"; inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development}; inline const Version CoreVersion = {26, 2, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true}; inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
extern BackendType ActiveBackendType; 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,
};
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
// never a demand: the renderer clamps it to what the device supports at device
// creation, falling down the chain ext -> indirect -> unroll with one log line.
enum class MultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
Unroll, // one vkCmdDraw* per sub-draw
};
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
// such entry point in core, so every tier below is an emulation; they differ only in
// which driver capability they lean on and how many driver calls a batch costs. Like
// the Magma knob this is a preference, clamped at resolution time to what the ES
// driver actually supports, with one log line when it falls back.
enum class GLESMultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // one glMultiDrawElementsBaseVertexEXT
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
// drawn by a single glDrawElements
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (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"
// (case-insensitive).
//
// Env variables intentionally NOT mirrored here (kept as live std::getenv at their
// call sites):
// - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp).
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
// matrix against both execution engines. Mirroring it here would resolve to the
// default in exactly the tests that exist to tell the engines apart (see
// MG_Util/Async/ShaderCompilePool.cpp, DetectAsyncPoolEngine).
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
// semantics: writes reach the backend without glFlushMappedBufferRange, and
// flush calls on rewritten maps become error-free no-ops. Non-persistent maps
// keep spec FLUSH_EXPLICIT behavior.
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool DisableUboRing = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
// unset picks the best supported tier.
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
// supported tier, which never includes "compute" - see the note on its resolution.
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
// both the threading and the application-visible behaviour change.
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
// auto, which is min(4, big cores); an explicit value is honoured as given.
Uint32 AsyncShaderCompileThreads = 0;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config } // namespace MobileGL::MG_Config
-214
View File
@@ -1,214 +0,0 @@
// MobileGL - MobileGL/ConfigLoader.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 "Config.h"
#include <cerrno>
#include <cstdlib>
#ifndef _WIN32
extern char** environ;
#endif
namespace MobileGL::MG_Config {
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
FeaturesTable Features;
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
static UniquePtr<UnorderedMap<String, String>> acceptedEnvVariablesMap;
static Bool IsAcceptedPrefix(const String& key) {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
} else {
acceptedEnvVariablesMap->clear();
}
char** envPtr = nullptr;
#ifdef _WIN32
envPtr = _environ;
#else // POSIX
envPtr = ::environ;
#endif
if (envPtr == nullptr) return;
for (char** env = envPtr; *env != nullptr; ++env) {
String entry(*env);
SizeT pos = entry.find('=');
if (pos != String::npos) {
String key = entry.substr(0, pos);
String value = entry.substr(pos + 1);
if (IsAcceptedPrefix(key)) {
(*acceptedEnvVariablesMap)[key] = value;
MGLOG_D("Config: Accepted env variable: %s=%s", key.c_str(), value.c_str());
}
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it != acceptedEnvVariablesMap->end()) {
outValue = it->second;
} else {
outValue = defaultValue;
}
}
// Unified truthy rule for boolean feature env variables: set, non-empty, not "0",
// and not "false" (case-insensitive).
static Bool IsTruthyValue(const String& value) {
if (value.empty() || value == "0") {
return false;
}
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lowered != "false";
}
inline Bool QueryEnvFlag(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
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;
}
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::MultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::MultiDrawMode::Auto;
}
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::GLESMultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::GLESMultiDrawMode::Auto;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
char* parseEnd = nullptr;
errno = 0;
const unsigned long parsedValue = std::strtoul(value.c_str(), &parseEnd, 10);
if (parseEnd == value.c_str() || *parseEnd != '\0' || errno == ERANGE || parsedValue < minValue ||
parsedValue > maxValue) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected an integer in range [%u, %u], "
"using default %u",
key.c_str(), value.c_str(), minValue, maxValue, defaultValue);
return defaultValue;
}
return static_cast<Uint32>(parsedValue);
}
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
}
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#define ENTRY(backendType) \
if (backendTypeStr == #backendType) { \
MG_Config::ActiveBackendType = BackendType::backendType; \
MGLOG_I("Config: Active backend type set to " #backendType); \
return; \
}
ENTRY(DirectGLES)
ENTRY(DirectVulkan)
ENTRY(Unknown)
MG_Config::ActiveBackendType = BackendType::Unknown;
#undef ENTRY
}
void Init() {
MGLOG_D("Loading configuration from environment variables...");
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
}
} // namespace MobileGL::MG_ConfigLoader
+8 -17
View File
@@ -32,14 +32,9 @@
#define MOBILEGL_GLX_API MOBILEGL_API #define MOBILEGL_GLX_API MOBILEGL_API
#define MOBILEGL_GL_API MOBILEGL_API #define MOBILEGL_GL_API MOBILEGL_API
#define MOBILEGL_EGL_API MOBILEGL_API #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 ======================= // // ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO #define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
#endif
#define MOBILEGL_LOG_ENABLE_CONSOLE 0 #define MOBILEGL_LOG_ENABLE_CONSOLE 0
#define MOBILEGL_LOG_ENABLE_FILE 1 #define MOBILEGL_LOG_ENABLE_FILE 1
@@ -68,15 +63,11 @@
#endif #endif
// =============================== Utils ================================ // // =============================== Utils ================================ //
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #define MOBILEGL_ASSERT(condition, ...) \
#define MOBILEGL_ASSERT(condition, ...) \ do { \
do { \ if (!(condition)) { \
if (!(condition)) { \ MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \ MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \ TRAP; \
TRAP; \ } \
} \ } while (0)
} while (0)
#else
#define MOBILEGL_ASSERT(condition, ...)
#endif
+2 -8
View File
@@ -10,17 +10,11 @@
namespace MobileGL { namespace MobileGL {
namespace MG_Config { namespace MG_Config {
BackendType ActiveBackendType; BackendType ActiveBackendType = BackendType::DirectVulkanTMP;
} // namespace MG_Config } // namespace MG_Config
namespace MG_Backend { namespace MG_Backend {
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is UniquePtr<BackendObject> pActiveBackendObject;
// 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; GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend } // namespace MG_Backend
} // namespace MobileGL } // namespace MobileGL
+2 -39
View File
@@ -32,14 +32,13 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <cassert> #include <cassert>
#include <climits>
#include <cstdlib>
#include <cstdarg> #include <cstdarg>
#include <cstring> #include <cstring>
#include <numeric> #include <numeric>
#include <expected> #include <expected>
#include <iostream> #include <iostream>
#include <optional> #include <optional>
#include <xxhash.h>
#include <algorithm> #include <algorithm>
#include <stdexcept> #include <stdexcept>
#include <functional> #include <functional>
@@ -52,9 +51,6 @@
// Include FastSTL // Include FastSTL
#include <FastSTL/UnorderedMap.h> #include <FastSTL/UnorderedMap.h>
// Include xxHash
#include <xxhash.h>
// Include spirv_cross // Include spirv_cross
#include <spirv_cross/spirv_cross_c.h> #include <spirv_cross/spirv_cross_c.h>
@@ -64,9 +60,7 @@
#endif #endif
#include <EGL/egl.h> #include <EGL/egl.h>
#define GL_GLEXT_PROTOTYPES #define GL_GLEXT_PROTOTYPES
#ifndef NO_GL_H
#include "GL/gl.h" #include "GL/gl.h"
#endif
#include <GL/glcorearb.h> #include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES #undef GL_GLEXT_PROTOTYPES
@@ -98,44 +92,13 @@
#endif #endif
#ifdef __ANDROID__ #ifdef __ANDROID__
#define VK_USE_PLATFORM_ANDROID_KHR
#include <unistd.h> #include <unistd.h>
#include <pthread.h> #include <pthread.h>
#include <android/log.h> #include <android/log.h>
#include <android/native_window.h> #include <android/native_window.h>
#endif #endif
#ifdef __ANDROID__
#define VK_USE_PLATFORM_ANDROID_KHR
#elif _WIN32
#define VK_USE_PLATFORM_WIN32_KHR
#elif defined(__APPLE__)
#define VK_USE_PLATFORM_METAL_EXT
#elif defined(__linux__)
#define VK_USE_PLATFORM_XLIB_KHR
typedef struct _XDisplay Display;
typedef unsigned long XID;
typedef XID Window;
typedef unsigned long VisualID;
#else
#warning "VK_USE_PLATFORM_*_KHR not defined for this platform!"
#endif
#if defined(VK_USE_PLATFORM_XLIB_KHR)
#pragma push_macro("Bool")
#pragma push_macro("None")
#pragma push_macro("Always")
#pragma push_macro("Status")
#pragma push_macro("LSBFirst")
#pragma push_macro("DestroyAll")
#endif
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#if defined(VK_USE_PLATFORM_XLIB_KHR)
#pragma pop_macro("DestroyAll")
#pragma pop_macro("LSBFirst")
#pragma pop_macro("Status")
#pragma pop_macro("Always")
#pragma pop_macro("None")
#pragma pop_macro("Bool")
#endif
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
+36 -116
View File
@@ -8,140 +8,60 @@
#include "Init.h" #include "Init.h"
#include "Config.h" #include "Config.h"
#include <MG_Impl/Init.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h> #include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <atomic>
#include <mutex>
namespace MobileGL { namespace MobileGL {
namespace { void MG_Initialize() {
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) {
return;
}
if (logLifecycle) {
MGLOG_I("MobileGL closing...");
}
// First, before anything else is torn down. In-flight compile/link jobs own
// their own inputs and are safe against everything below EXCEPT glslang's
// process globals and the TShader/TProgram objects hanging off pGLContext,
// both of which this function is about to destroy. This is the one
// cancellation path in the whole design that waits.
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
// GL syncs die with their contexts, and every context is gone by the
// time full teardown runs: drain the live-sync registry while the
// backend function table can still release the backend handles (and
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
// glslang's process-wide pool allocator and every cached built-in symbol table,
// while the TShader/TProgram objects owned by the shader and program objects
// still reference levels adopted from those tables. Finalizing first left live
// glslang objects pointing at freed memory for the rest of the teardown.
glslang::FinalizeProcess();
// Immediately after, and never apart from it: FinalizeProcess just deleted the
// built-in symbol tables the prewarm latch stands for, so leaving it set would
// make the next Initialize() skip a prewarm it genuinely needs.
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
if (logLifecycle) {
MG_Util::Debug::Close();
}
// TODO: add and use Destroy functions for other subsystems
}
}
void Initialize() {
if (g_isInitialized) {
MGLOG_D("MobileGL already initialized; skipping duplicate Initialize()");
return;
}
MG_Util::Debug::InitFile(); MG_Util::Debug::InitFile();
MGLOG_I("Initializing MobileGL..."); MGLOG_I("Initializing MobileGL...");
MG_ConfigLoader::Init();
MGLOG_I("Config loaded");
MG_State::Init(); MG_State::Init();
MGLOG_D("MG_State initialized"); MGLOG_D("MobileGL State initialized");
MG_Backend::Init(); MG_Backend::Init();
MGLOG_D("MG_Backend initialized"); MGLOG_D("MobileGL Backend initialized");
MG_Impl::Init(); MG_Impl::Init();
MGLOG_D("MG_Impl initialized"); MGLOG_D("MobileGL Implementation initialized");
glslang::InitializeProcess(); glslang::InitializeProcess();
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
// tables lazily under a process-wide lock held for the whole build, so without this
// the first concurrent compiles of a shaderpack all serialize behind the very first
// parse and asynchronous compilation looks like it is doing nothing.
//
// Gated on the flag, because the problem it solves only exists when there are
// workers: with compilation synchronous, nothing ever contends for that lock and the
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
// on the async subsystem (ProgramUtilTest compiles that file without it).
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
}
MGLOG_D("glslang initialized"); MGLOG_D("glslang initialized");
g_isInitialized = true;
MGLOG_I("MobileGL initialized"); MGLOG_I("MobileGL initialized");
} }
void EnsureInitialized() { void MG_Destroy() {
if (g_isInitialized.load(std::memory_order_acquire)) { MGLOG_I("MobileGL closing...");
return; glslang::FinalizeProcess();
} delete MG_State::pGLContext;
// Re-entrant call while this thread is already inside Initialize() delete MG_Impl::GLImpl::TextureImpl::pProxyTextureManager;
// (e.g. an init step routing back through a public entry point). delete MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
if (tl_initializing) { MG_Util::Debug::Close();
return;
} // TODO: add and use Destroy functions for other subsystems
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() { #if defined(__linux__) || defined(__APPLE__)
DestroyImpl(true); __attribute__((constructor)) static void AutoInit() {
MG_Initialize();
} }
// MobileGL's lifecycle is owned entirely by the host-API layers __attribute__((destructor)) static void AutoDestroy() {
// (EGL/WGL/CGL): initialization happens lazily on the first entry point MG_Destroy();
// via EnsureInitialized(), and full teardown happens deterministically }
// when the last EGL display is terminated with nothing current (EGLImpl #endif
// calls Destroy()). There is intentionally no backend-initializing static
// constructor, no static destructor, and no DllMain: the global singletons #ifdef _WIN32
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
// without eglTerminate simply leaks them to the OS instead of running switch (ul_reason_for_call) {
// backend destructors during static teardown. macOS has a lightweight case DLL_PROCESS_ATTACH:
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend MG_Initialize();
// initialization still enters here from the first hooked CGL context. break;
case DLL_PROCESS_DETACH:
MG_Destroy();
break;
}
return TRUE;
}
#endif
} // namespace MobileGL } // namespace MobileGL
+3 -26
View File
@@ -10,29 +10,6 @@
#include "Includes.h" #include "Includes.h"
namespace MobileGL { namespace MobileGL {
void Initialize(); void MG_Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize(). void MG_Destroy();
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so } // namespace MobileGL
// full backend initialization never depends on ELF/DLL static constructors,
// and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
void InitFile();
} // namespace MG_Util::Debug
namespace MG_ConfigLoader {
void Init();
} // namespace MG_ConfigLoader
namespace MG_Backend {
void Init();
} // namespace MG_Backend
namespace MG_Impl {
void Init();
} // namespace MG_Impl
} // namespace MobileGL
-480
View File
@@ -7,489 +7,9 @@
// End of Source File Header // End of Source File Header
#include "BackendObject.h" #include "BackendObject.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include <algorithm>
#include <cstring>
#include <iomanip>
#include <sstream>
namespace MobileGL::MG_Backend { namespace MobileGL::MG_Backend {
namespace {
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
(void)dpy;
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
std::thread::id CurrentThreadKey() {
return std::this_thread::get_id();
}
const char* GetFormatCapabilitySupportString(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
FormatCapability capability) {
if (HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) return "Full";
if (HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex], capability)) return "Caveat";
return "None";
}
SizeT GetPrintedFormatNameWidth() {
SizeT width = 0;
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const auto format = static_cast<TextureInternalFormat>(formatIndex);
width = std::max(width, MG_Util::ConvertTextureInternalFormatToString(format).size());
}
return width;
}
SizeT GetCapabilityColumnWidth(FormatCapability capability) {
SizeT width = std::strlen(GetFormatCapabilityName(capability));
width = std::max<SizeT>(width, std::strlen("Caveat"));
return width;
}
String BuildFormatCapabilityHeader(SizeT formatNameWidth) {
std::ostringstream line;
line << std::left << std::setw(static_cast<Int>(formatNameWidth)) << "";
for (FormatCapability capability : kReportedFormatCapabilities) {
line << " | " << std::left << std::setw(static_cast<Int>(GetCapabilityColumnWidth(capability)))
<< GetFormatCapabilityName(capability);
}
return line.str();
}
String BuildFormatCapabilityRow(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
SizeT formatNameWidth) {
const auto format = static_cast<TextureInternalFormat>(formatIndex);
std::ostringstream line;
line << std::left << std::setw(static_cast<Int>(formatNameWidth))
<< MG_Util::ConvertTextureInternalFormatToString(format);
for (FormatCapability capability : kReportedFormatCapabilities) {
line << " | " << std::left << std::setw(static_cast<Int>(GetCapabilityColumnWidth(capability)))
<< GetFormatCapabilitySupportString(cache, targetIndex, formatIndex, capability);
}
return line.str();
}
} // namespace
void FormatCapabilityCache::Clear() {
for (auto& row : FullCaps) {
row.fill(FormatCapabilityFlags{});
}
for (auto& row : CaveatCaps) {
row.fill(FormatCapabilityFlags{});
}
for (auto& row : SampleCounts) {
for (auto& counts : row) {
counts.clear();
}
}
}
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability) {
return static_cast<Bool>(caps & capability);
}
SizeT GetFormatCapabilityTargetIndex(TextureTarget target) {
if (target == TextureTarget::Unknown || static_cast<Int>(target) < 0 ||
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
return static_cast<SizeT>(target);
}
SizeT GetRenderbufferFormatCapabilityTargetIndex() {
return kFormatCapabilityRenderbufferTargetIndex;
}
const char* GetFormatCapabilityName(FormatCapability capability) {
switch (capability) {
case FormatCapability::Creatable:
return "Creatable";
case FormatCapability::Sampled:
return "Sampled";
case FormatCapability::LinearFilter:
return "LinearFilter";
case FormatCapability::GenerateMipmap:
return "GenerateMipmap";
case FormatCapability::TextureGather:
return "TextureGather";
case FormatCapability::TextureShadow:
return "TextureShadow";
case FormatCapability::FramebufferRenderable:
return "FramebufferRenderable";
case FormatCapability::FramebufferLayered:
return "FramebufferLayered";
case FormatCapability::MultisampleTexture:
return "MultisampleTexture";
case FormatCapability::MultisampleRenderbuffer:
return "MultisampleRenderbuffer";
case FormatCapability::ColorAttachment:
return "ColorAttachment";
case FormatCapability::DepthAttachment:
return "DepthAttachment";
case FormatCapability::StencilAttachment:
return "StencilAttachment";
case FormatCapability::TextureBuffer:
return "TextureBuffer";
}
return "Unknown";
}
String GetFormatCapabilityTargetName(SizeT targetIndex) {
if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) {
return "Renderbuffer";
}
if (targetIndex >= kFormatCapabilityTextureTargetCount) {
return "Unknown";
}
return MG_Util::ConvertTextureTargetToString(static_cast<TextureTarget>(targetIndex));
}
void PrintFormatCapabilities(const FormatCapabilityCache& cache) {
const SizeT formatNameWidth = GetPrintedFormatNameWidth();
MGLOG_D("Backend format capabilities:");
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTargetCount; ++targetIndex) {
MGLOG_D("");
const String targetName = GetFormatCapabilityTargetName(targetIndex);
MGLOG_D("- %s", targetName.c_str());
const String header = BuildFormatCapabilityHeader(formatNameWidth);
MGLOG_D("%s", header.c_str());
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const String row = BuildFormatCapabilityRow(cache, targetIndex, formatIndex, formatNameWidth);
MGLOG_D("%s", row.c_str());
}
}
}
Bool BackendObject::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (dpy == EGL_NO_DISPLAY) {
MGLOG_E("InitializeEGLDisplay failed: invalid EGLDisplay");
return false;
}
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
*major = 1;
}
if (minor) {
*minor = 5;
}
return true;
}
Bool BackendObject::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLWindowSurface(surface, handle) && ActivateEGLSurface(surface);
}
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
}
return true;
}
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
}
Bool BackendObject::RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("RegisterEGLWindowSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("RegisterEGLWindowSurface failed: invalid EGLSurface");
return false;
}
if (handle.Backend == WindowBackend::Unknown || !handle.Handle) {
MGLOG_E("RegisterEGLWindowSurface failed: invalid native window handle");
return false;
}
auto& state = m_eglSurfaces[surface];
state = EGLSurfaceState{
.Kind = SurfaceKind::Window,
.Window = handle,
.Width = static_cast<EGLint>(std::max<Uint32>(handle.Width, 1)),
.Height = static_cast<EGLint>(std::max<Uint32>(handle.Height, 1)),
};
return true;
}
Bool BackendObject::RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("RegisterEGLPbufferSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("RegisterEGLPbufferSurface failed: invalid EGLSurface");
return false;
}
if (width <= 0 || height <= 0) {
MGLOG_E("RegisterEGLPbufferSurface failed: invalid size %dx%d", width, height);
return false;
}
m_eglSurfaces[surface] = EGLSurfaceState{
.Kind = SurfaceKind::Pbuffer,
.Width = width,
.Height = height,
};
return true;
}
const BackendObject::EGLSurfaceState* BackendObject::GetRegisteredEGLSurface(EGLSurface surface) const {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
return surfaceIt == m_eglSurfaces.end() ? nullptr : &surfaceIt->second;
}
Bool BackendObject::ActivateEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto* surfaceState = GetRegisteredEGLSurface(surface);
if (!surfaceState) {
MGLOG_E("ActivateEGLSurface failed: EGL surface is not registered");
return false;
}
if (m_eglSurfaceInitialized && m_eglSurface == surface) {
return true;
}
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
MGLOG_E("ActivateEGLSurface failed: backend InitWindowSurface failed");
return false;
}
} else if (surfaceState->Kind == SurfaceKind::Pbuffer) {
if (!InitPbufferSurface(surfaceState->Width, surfaceState->Height)) {
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
return false;
}
} else {
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
return false;
}
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
return true;
}
Bool BackendObject::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto threadKey = CurrentThreadKey();
if (IsReleaseCurrentRequest(dpy, draw, read, ctx)) {
ReleaseEGLCurrentThread(threadKey);
return true;
}
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
if (!m_eglSurfaceInitialized) {
if (draw != read || !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
}
}
if (!GetRegisteredEGLSurface(draw) || !GetRegisteredEGLSurface(read)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not registered");
return false;
}
if (draw != read) {
MGLOG_E("MakeEGLCurrent failed: separate draw/read surfaces are not supported");
return false;
}
if (draw != m_eglSurface && !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not backed by this backend");
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
return false;
}
if (!m_backendCapabilitiesInitialized) {
if (!InitCapabilities()) {
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
return false;
}
m_backendCapabilitiesInitialized = true;
}
ReleaseEGLCurrentThread(threadKey);
m_eglCurrentThreads[threadKey] = EGLCurrentState{
.Display = dpy,
.DrawSurface = draw,
.ReadSurface = read,
.Context = ctx,
};
return true;
}
void BackendObject::ResetEGLRuntimeState() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglSurfaceInitialized = false;
m_backendCapabilitiesInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
m_eglCurrentThreads.clear();
}
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
MGLOG_E("SwapEGLBuffers failed: EGL display mismatch or not initialized");
return false;
}
const auto currentIt = m_eglCurrentThreads.find(CurrentThreadKey());
if (currentIt == m_eglCurrentThreads.end()) {
MGLOG_E("SwapEGLBuffers failed: no current context attached");
return false;
}
if (currentIt->second.Display != dpy || currentIt->second.DrawSurface != draw ||
currentIt->second.Context == EGL_NO_CONTEXT) {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
MGLOG_E("SwapEGLBuffers failed: backend Present function is null");
return false;
}
backendFunctions.Present();
return true;
}
void BackendObject::SetEGLSwapInterval(Int interval) {
const auto& backendFunctions = GetBackendFunctions();
if (backendFunctions.SetSwapInterval) {
backendFunctions.SetSwapInterval(interval);
}
}
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
if (surface == EGL_NO_SURFACE) {
return false;
}
for (const auto& current : m_eglCurrentThreads) {
if (current.second.DrawSurface == surface || current.second.ReadSurface == surface) {
return true;
}
}
return false;
}
void BackendObject::DestroyPendingEGLSurfaceIfUnused(EGLSurface surface) {
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || !surfaceIt->second.DestroyPending ||
IsEGLSurfaceCurrent(surface)) {
return;
}
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
}
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
auto currentIt = m_eglCurrentThreads.find(threadKey);
if (currentIt == m_eglCurrentThreads.end()) {
return;
}
const EGLSurface drawSurface = currentIt->second.DrawSurface;
const EGLSurface readSurface = currentIt->second.ReadSurface;
m_eglCurrentThreads.erase(currentIt);
DestroyPendingEGLSurfaceIfUnused(drawSurface);
DestroyPendingEGLSurfaceIfUnused(readSurface);
}
void BackendObject::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end()) {
return;
}
if (IsEGLSurfaceCurrent(surface)) {
surfaceIt->second.DestroyPending = true;
return;
}
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
}
}
void BackendObject::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
ResetEGLRuntimeState();
m_eglSurfaces.clear();
m_eglDisplay = EGL_NO_DISPLAY;
m_eglDisplayInitialized = false;
}
void BackendObject::SetWindowHandle(const WindowHandle& handle) { void BackendObject::SetWindowHandle(const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_windowHandle = handle; m_windowHandle = handle;
} }
const FormatCapabilityCache& BackendObject::GetFormatCapabilities() const {
return m_formatCapabilities;
}
FormatCapabilityCache& BackendObject::MutableFormatCapabilities() {
return m_formatCapabilities;
}
Bool BackendObject::InitPbufferSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
return false;
}
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend } // namespace MobileGL::MG_Backend
+4 -391
View File
@@ -8,104 +8,21 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
namespace MobileGL { namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
}
enum class BackendType { enum class BackendType {
DirectGLES, DirectGLES,
DirectVulkan, DirectVulkanTMP,
BackendTypeCount, BackendTypeCount,
Unknown = -1 Unknown = -1
}; };
namespace MG_Backend { namespace MG_Backend {
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
Sampled = 1ull << 1,
LinearFilter = 1ull << 2,
GenerateMipmap = 1ull << 3,
TextureGather = 1ull << 4,
TextureShadow = 1ull << 5,
FramebufferRenderable = 1ull << 6,
FramebufferLayered = 1ull << 7,
MultisampleTexture = 1ull << 8,
MultisampleRenderbuffer = 1ull << 9,
ColorAttachment = 1ull << 10,
DepthAttachment = 1ull << 11,
StencilAttachment = 1ull << 12,
TextureBuffer = 1ull << 13
};
using FormatCapabilityFlags = Flags<FormatCapability>;
inline constexpr Array<FormatCapability, 14> kReportedFormatCapabilities = {
FormatCapability::Creatable,
FormatCapability::Sampled,
FormatCapability::LinearFilter,
FormatCapability::GenerateMipmap,
FormatCapability::TextureGather,
FormatCapability::TextureShadow,
FormatCapability::FramebufferRenderable,
FormatCapability::FramebufferLayered,
FormatCapability::MultisampleTexture,
FormatCapability::MultisampleRenderbuffer,
FormatCapability::ColorAttachment,
FormatCapability::DepthAttachment,
FormatCapability::StencilAttachment,
FormatCapability::TextureBuffer,
};
inline constexpr SizeT kFormatCapabilityTextureTargetCount =
static_cast<SizeT>(TextureTarget::TextureTargetCount);
inline constexpr SizeT kFormatCapabilityRenderbufferTargetIndex = kFormatCapabilityTextureTargetCount;
inline constexpr SizeT kFormatCapabilityTargetCount = kFormatCapabilityTextureTargetCount + 1;
inline constexpr SizeT kFormatCapabilityFormatCount =
static_cast<SizeT>(TextureInternalFormat::TextureInternalFormatCount);
using FormatCapabilityTable =
Array<Array<FormatCapabilityFlags, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
using FormatSampleCountTable =
Array<Array<Vector<Int>, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
struct FormatCapabilityCache {
FormatCapabilityTable FullCaps{};
FormatCapabilityTable CaveatCaps{};
FormatSampleCountTable SampleCounts{};
void Clear();
};
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability);
SizeT GetFormatCapabilityTargetIndex(TextureTarget target);
SizeT GetRenderbufferFormatCapabilityTargetIndex();
const char* GetFormatCapabilityName(FormatCapability capability);
String GetFormatCapabilityTargetName(SizeT targetIndex);
void PrintFormatCapabilities(const FormatCapabilityCache& cache);
// Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync
// and released by GLFunctionsTable::DeleteSync.
using BackendSyncHandle = void*;
// Opaque backend timer-query handle, created by
// GLFunctionsTable::BeginTimeElapsedQuery / QueryCounterTimestamp and
// released by GLFunctionsTable::DeleteBackendQuery.
using BackendQueryHandle = void*;
struct GLFunctionsTable { struct GLFunctionsTable {
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count); void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices); void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
void (*DrawElementsBaseVertex)(GLenum mode, GLsizei count, GLenum type, const void* indices, void (*DrawElementsBaseVertex)(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLint basevertex); GLint basevertex);
void (*MultiDrawArrays)(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void (*MultiDrawElements)(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void (*MultiDrawElements)(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount); GLsizei drawcount);
void (*MultiDrawElementsBaseVertex)(GLenum mode, const GLsizei* count, GLenum type, void (*MultiDrawElementsBaseVertex)(GLenum mode, const GLsizei* count, GLenum type,
@@ -114,10 +31,6 @@ namespace MobileGL {
void (*MultiDrawElementsIndirect)(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, void (*MultiDrawElementsIndirect)(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride); GLsizei stride);
void (*MultiDrawArraysIndirect)(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); void (*MultiDrawArraysIndirect)(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void (*MultiDrawElementsIndirectCount)(GLenum mode, GLenum type, const void* indirect,
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);
void (*MultiDrawArraysIndirectCount)(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void (*DrawRangeElementsBaseVertex)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void (*DrawRangeElementsBaseVertex)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex); const void* indices, GLint basevertex);
void (*DrawRangeElements)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void (*DrawRangeElements)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
@@ -141,269 +54,29 @@ namespace MobileGL {
void (*ClearBufferfv)(GLenum buffer, GLint drawbuffer, const GLfloat* value); void (*ClearBufferfv)(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearBufferuiv)(GLenum buffer, GLint drawbuffer, const GLuint* value); void (*ClearBufferuiv)(GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*ClearBufferiv)(GLenum buffer, GLint drawbuffer, const GLint* value); void (*ClearBufferiv)(GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferfv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void (*CopyTexImage2D)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void (*CopyTexImage2D)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border); GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target); void (*GenerateMipmap)(GLenum target);
void (*ReadPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void (*ReadPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels); void* pixels);
void (*GetTexImage)(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void (*GetTexImage)(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void (*GetTextureImage)(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
GLsizei bufSize, GLvoid* pixels);
void (*DispatchCompute)(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void (*DispatchComputeIndirect)(GLintptr indirect);
void (*MemoryBarrier)(GLbitfield barriers);
void (*MemoryBarrierByRegion)(GLbitfield barriers);
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
GLenum access, GLenum format);
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
// the application passes is the frontend interface-query enumeration's, and no
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
// The name is the one coordinate all three agree on, so the frontend resolves the
// index against its own enumeration and each backend maps the name to its own.
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
GLuint storageBlockBinding);
// GL fence sync objects. All entries are optional (may be null); the
// frontend then falls back to always-signaled sync semantics.
// FenceSync may itself return null when the backend cannot create a
// fence right now (e.g. the calling thread does not own the backend
// context); the frontend treats such a sync as always signaled.
BackendSyncHandle (*FenceSync)();
GLenum (*ClientWaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void (*DeleteSync)(BackendSyncHandle sync);
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
// GL timer-query objects (GL_ARB_timer_query). All entries are
// optional (may be null); the frontend then falls back to zero
// results and reports GL_QUERY_COUNTER_BITS == 0.
// BeginTimeElapsedQuery / QueryCounterTimestamp may themselves
// return null when the backend cannot create a query right now;
// the frontend treats such a query as immediately available with
// a zero result.
// Dynamic support check: true only when the live backend can
// actually time at the moment of the call (extension / entry
// points / timestamp valid bits are known then, not at table
// init). Gates the advertised GL_QUERY_COUNTER_BITS.
Bool (*IsTimerQuerySupported)();
BackendQueryHandle (*BeginTimeElapsedQuery)(); // starts a TIME_ELAPSED span
void (*EndTimeElapsedQuery)(BackendQueryHandle query); // ends the span
BackendQueryHandle (*QueryCounterTimestamp)(); // glQueryCounter(GL_TIMESTAMP) one-shot
Bool (*IsQueryResultAvailable)(BackendQueryHandle query); // non-blocking
// Returns true when a final value was produced (*outNanoseconds
// written; the frontend may cache it and release the handle).
// Returns false when the result could not be obtained YET - e.g.
// a Vulkan wait that refuses to block on a not-yet-submitted
// frame serial - in which case the frontend must keep the handle
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
// called while the frontend capture state is still active, so the backend
// can still see the capture program and buffer bindings.
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
void (*PatchParameteri)(GLenum pname, GLint value);
void (*BeginTransformFeedback)(GLenum primitiveMode);
void (*EndTransformFeedback)();
// ARB_transform_feedback2. A backend that leaves these null keeps the single
// implicit capture span the frontend has always modelled; the frontend state
// (paused flag, per-object bindings) is tracked either way.
void (*PauseTransformFeedback)();
void (*ResumeTransformFeedback)();
void (*BindTransformFeedback)(GLuint name);
void (*DeleteTransformFeedback)(GLuint name);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
}; };
struct GlobalBackendFunctionsTable { struct GlobalBackendFunctionsTable {
GLFunctionsTable GL; GLFunctionsTable GL;
void (*Present)(); void (*Present)();
// Optional: applies the app-requested eglSwapInterval to the native
// presentation path (null = backend keeps its own pacing policy).
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 { struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256; 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;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
Float PointSizeRangeMin = 1.0f;
Float PointSizeRangeMax = 1.0f;
Float PointSizeGranularity = 1.0f;
Int Max3DTextureSize = 16384;
Int MaxArrayTextureLayers = 2048;
Int MaxCubeMapTextureSize = 16384;
Int MaxFramebufferWidth = 16384;
Int MaxFramebufferHeight = 16384;
Int MaxFramebufferLayers = 2048;
Int MaxRenderbufferSize = 16384;
Int MaxTextureSize = 16384;
Int MaxColorTextureSamples = 1;
Int MaxDepthTextureSamples = 1;
Int MaxFramebufferSamples = 1;
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
Int MaxCombinedTextureImageUnits = 192;
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
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;
Int MaxClipDistances = 8;
Int MaxViewports = 16;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
Bool SupportsFloat64VertexAttributes = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
}; };
enum class WindowBackend { enum class WindowBackend {
Android, Android,
X11, // TODO: X11, Wayland, Windows, macOS, etc.
MetalLayer,
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount, WindowBackendCount,
Unknown = -1 Unknown = -1
}; };
@@ -411,8 +84,6 @@ namespace MobileGL {
struct WindowHandle { struct WindowHandle {
WindowBackend Backend = WindowBackend::Unknown; WindowBackend Backend = WindowBackend::Unknown;
void* Handle = nullptr; void* Handle = nullptr;
Uint32 Width = 0;
Uint32 Height = 0;
}; };
class BackendObject { class BackendObject {
@@ -420,20 +91,8 @@ namespace MobileGL {
virtual ~BackendObject() = default; virtual ~BackendObject() = default;
virtual void Initialize() = 0; virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0; virtual void InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0; virtual void InitWindowSurface() = 0;
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// Forwards the app-requested eglSwapInterval to the backend's native
// presentation path (no-op for backends without a SetSwapInterval hook).
virtual void SetEGLSwapInterval(Int interval);
virtual void ReleaseEGLSurface(EGLSurface surface);
virtual void ReleaseEGLResources();
void SetWindowHandle(const WindowHandle& handle); void SetWindowHandle(const WindowHandle& handle);
@@ -441,56 +100,10 @@ namespace MobileGL {
virtual String GetBackendAPIVersionString() const = 0; virtual String GetBackendAPIVersionString() const = 0;
virtual const GlobalBackendFunctionsTable& GetBackendFunctions() const = 0; virtual const GlobalBackendFunctionsTable& GetBackendFunctions() const = 0;
virtual const DynamicBackendParameters& GetDynamicParameters() const = 0; virtual const DynamicBackendParameters& GetDynamicParameters() const = 0;
const FormatCapabilityCache& GetFormatCapabilities() const;
virtual BackendType GetBackendType() const = 0; virtual BackendType GetBackendType() const = 0;
protected: protected:
enum class SurfaceKind {
None,
Window,
Pbuffer
};
struct EGLCurrentState {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface DrawSurface = EGL_NO_SURFACE;
EGLSurface ReadSurface = EGL_NO_SURFACE;
EGLContext Context = EGL_NO_CONTEXT;
};
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
FormatCapabilityCache m_formatCapabilities;
WindowHandle m_windowHandle; WindowHandle m_windowHandle;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLSurface m_eglSurface = EGL_NO_SURFACE;
Bool m_eglDisplayInitialized = false;
Bool m_eglSurfaceInitialized = false;
Bool m_backendCapabilitiesInitialized = false;
SurfaceKind m_eglSurfaceKind = SurfaceKind::None;
UnorderedMap<std::thread::id, EGLCurrentState> m_eglCurrentThreads;
UnorderedMap<EGLSurface, EGLSurfaceState> m_eglSurfaces;
private:
Bool IsEGLSurfaceCurrent(EGLSurface surface) const;
void DestroyPendingEGLSurfaceIfUnused(EGLSurface surface);
void ReleaseEGLCurrentThread(const std::thread::id& threadKey);
}; };
} // namespace MG_Backend } // namespace MG_Backend
} // namespace MobileGL } // namespace MobileGL
+4 -2
View File
@@ -10,9 +10,11 @@
#include <Includes.h> #include <Includes.h>
#include "BackendObject.h" #include "BackendObject.h"
#include "DirectGLES/BackendObject_DirectGLES.h" #include "DirectGLES/BackendObject_DirectGLES.h"
#include "DirectVulkan/BackendObject_DirectVulkan.h" #include "DirectVulkanTMP/BackendObject_DirectVulkanTMP.h"
namespace MobileGL::MG_Backend { namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject>& pActiveBackendObject; extern UniquePtr<BackendObject> pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable; extern GlobalBackendFunctionsTable gBackendFunctionsTable;
void Init();
} // namespace MobileGL::MG_Backend } // namespace MobileGL::MG_Backend
File diff suppressed because it is too large Load Diff
@@ -12,26 +12,13 @@
#include <MG_Util/BackendLoaders/OpenGL/Loader.h> #include <MG_Util/BackendLoaders/OpenGL/Loader.h>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
// Populates the same format-capability cache used by backend startup. The caller
// must keep the supplied GLES context current for the duration of this call.
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
class BackendObject_DirectGLES : public BackendObject { class BackendObject_DirectGLES : public BackendObject {
public: public:
~BackendObject_DirectGLES() override; ~BackendObject_DirectGLES() override;
void Initialize() override; void Initialize() override;
Bool InitCapabilities() override; void InitCapabilities() override;
Bool InitWindowSurface() override; void InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
const RendererInfo& GetRendererInfo() const override; const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override; String GetBackendAPIVersionString() const override;
@@ -41,12 +28,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESFunctionsTable& GetGLESFunctions() const; const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
const MG_External::EGLFunctionsTable& GetEGLFunctions() const; const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
private: private:
void UpdateDynamicBackendParameters(); void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
Bool m_initialized = false; Bool m_initialized = false;
MG_External::EGLFunctionsTable m_EGLFunctions; MG_External::EGLFunctionsTable m_EGLFunctions;
@@ -54,25 +38,4 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_External::GLESCapabilities m_GLESCapabilities; MG_External::GLESCapabilities m_GLESCapabilities;
DynamicBackendParameters m_dynamicParameters; DynamicBackendParameters m_dynamicParameters;
}; };
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Espryt renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor). The Extensions vector inside is live backend state that
// is reconciled after capability init; callers that need the advertised list for
// a known capability set must use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// 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
// inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& glesRendererString, Int glesMajor, Int glesMinor);
} // namespace MobileGL::MG_Backend::DirectGLES } // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
-145
View File
@@ -8,8 +8,6 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h> #include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_State/GLState/SamplerState/SamplerObject.h> #include <MG_State/GLState/SamplerState/SamplerObject.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h> #include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -19,10 +17,6 @@
operation Utils::CheckGLESError(); operation Utils::CheckGLESError();
namespace MobileGL::MG_Backend::DirectGLES { 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 ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value); void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value); void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -31,14 +25,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices); void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count); void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex); void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount); GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex); GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex); const void* indices, GLint basevertex);
@@ -55,159 +46,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint baseinstance); GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount); void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect); void DrawArraysIndirect(GLenum mode, const void* indirect);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter); GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
const GLubyte* GetString(GLenum name); const GLubyte* GetString(GLenum name);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window); Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
Bool MakeCurrent();
Bool ReleaseCurrent();
// True when the backend ES context is current on the calling thread, i.e.
// immediate buffer ops may issue GL calls right now.
Bool IsBackendContextCurrentOnThisThread();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
// the same situation, and handles created under a since-destroyed ES context
// are always treated as signaled.
BackendSyncHandle FenceSync();
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// True when GL_EXT_disjoint_timer_query and every entry point the timer
// hooks below need are present. Also gates the E_GL_ARB_timer_query
// advertisement in BackendObject_DirectGLES::InitCapabilities, and is
// registered as the GLFunctionsTable::IsTimerQuerySupported hook: a pure
// capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in.
Bool AreTimerQueriesSupported();
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES
// context or the extension/entry points are missing, and handles created
// under a since-destroyed ES context are always treated as complete with
// a zero result (mirrors the fence-sync handles above).
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
// which case the frontend falls back to counting primitives from the draw calls.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
// and release the handle). Returns false only when the calling thread
// does not own the ES context, so the value is genuinely unobtainable
// right now; the handle stays alive and readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
Int64 GetGpuTimestampNs();
void Present(); void Present();
// Frame-completion watermarks for the buffer-storage pool: CurrentFrameSerial()
// is bumped once per Present(); CompletedFrameSerial() is the newest frame whose
// GPU work has provably finished (advanced by polling a one-fence-per-frame ring).
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
Uint64 CurrentFrameSerial();
Uint64 CompletedFrameSerial();
// Block (up to timeoutNs) until the given frame serial provably retired on the
// GPU, using the per-frame fence ring. False when no usable fence covers the
// serial (fence-less context, foreign thread, or the slot was recycled);
// completion state is untouched in that case.
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
void SetSwapInterval(Int interval);
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities); void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
void DestroyEGLContext(); void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
// capture set is declared on the backend program at link time; the driver-side
// begin is deferred to the first draw of the span (ES needs the capturing
// program current and the capture buffers bound), and the end also mirrors the
// captured bytes back into the frontend buffer shadows.
void PatchParameteri(GLenum pname, GLint value);
namespace XfbImpl {
Bool AreTransformFeedbacksSupported();
// True while a capture span is open on the current transform feedback object
// (frontend Begin seen and not paused), whether or not the deferred driver-side
// Begin has been issued yet. Draw paths that would restructure the primitive
// stream, or that need to dispatch compute mid-draw, decline while it is set.
Bool IsCaptureSpanOpen();
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback();
void PauseTransformFeedback();
void ResumeTransformFeedback();
void BindTransformFeedback(GLuint name);
void DeleteTransformFeedback(GLuint name);
void OnBackendContextDestroyed();
} // namespace XfbImpl
namespace RenderStateImpl {
// Pushes the frontend's render-state block to the ES driver, diffed against what was
// last pushed.
//
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
// mask handed to the driver. A draw into a colour attachment the backend widened from
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
// move the stored alpha away from the 1.0 the application's three-channel format
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
// gets there in the first place, so it must be allowed to write alpha - hence the flag
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
// frontend version.
//
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
extern MG_External::EGLFunctionsTable g_EGLFuncs; extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs; extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities; extern MG_External::GLESCapabilities g_GLESCapabilities;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,894 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <cstring>
#include <limits>
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
using MG_Config::GLESMultiDrawMode;
namespace {
// ---------------------------------------------------------------------------
// Batch shape
// ---------------------------------------------------------------------------
SizeT IndexTypeSize(GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE: return 1;
case GL_UNSIGNED_SHORT: return 2;
case GL_UNSIGNED_INT: return 4;
default: return 0;
}
}
// The all-ones value of an index type, which is what GL restarts on once
// primitive restart is in play. CheckPrimitiveRestartSupported has already
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
// restart always restarts here and nowhere else.
Uint32 RestartSentinelFor(GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE: return 0xFFu;
case GL_UNSIGNED_SHORT: return 0xFFFFu;
default: return 0xFFFFFFFFu;
}
}
Bool RestartActive() {
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
}
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
// single draw without changing the primitive stream. Zero for strip/loop/fan modes
// (concatenation would weld one sub-draw's last primitive to the next sub-draw's
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
switch (mode) {
case GL_POINTS: return 1;
case GL_LINES: return 2;
case GL_TRIANGLES: return 3;
case GL_LINES_ADJACENCY: return 4;
case GL_TRIANGLES_ADJACENCY: return 6;
default: return 0;
}
}
// Beyond this an emulated batch would ask for a scratch allocation measured in
// hundreds of megabytes (and the scratch ring never shrinks again); decline and let
// a per-sub-draw tier handle it instead of trying and failing inside the driver.
constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24;
// The flattening dispatch is one invocation per output index. ES 3.1 only
// guarantees 65535 work groups per dimension, and exceeding it makes
// glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an
// uninitialised index buffer rather than failing visibly. Cap the tier there
// instead of querying: 4.19M indices is far past any real multi-draw batch, and
// beyond it the per-sub-draw tiers are the better answer anyway.
constexpr SizeT kComputeWorkGroupSize = 64;
constexpr SizeT kMaxComputeWorkGroups = 65535;
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
Uint BoundDrawIndirectBufferId() {
const auto& indirect =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!indirect) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
return resource ? resource->id : 0;
}
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject();
}
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
// that swaps in a scratch index buffer has to put back. Restoring the exact name
// matters beyond tidiness: the VAO twin memoises that it already synced this
// index binding and will not re-issue it on the next draw.
Uint BoundIndexBufferId() {
const auto& ibo = BoundIndexBuffer();
if (!ibo) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
return resource ? resource->id : 0;
}
// ---------------------------------------------------------------------------
// Scratch GL objects
//
// All of them belong to the ES context and are abandoned (not deleted) when it
// dies, exactly like XfbImpl's scatter buffer: the names are the dead context's
// to reclaim, and deleting them would target whatever the successor context
// handed out for the same name.
// ---------------------------------------------------------------------------
struct ScratchBuffer {
Uint id = 0;
SizeT capacity = 0;
SizeT cursor = 0; // ring buffers only: next free byte
};
ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array
ScratchBuffer g_rebasedIndices; // CPU-rebased index stream
ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors
ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream
Uint g_computeProgram = 0;
Bool g_computeProgramFailed = false;
GLint g_uElementSize = -1;
GLint g_uDrawCount = -1;
GLint g_uTotalIndices = -1;
// Reused staging, so a steady stream of batches allocates nothing.
Vector<DrawElementsIndirectCommand> g_commandStaging;
Vector<Uint32> g_indexStaging;
Vector<Uint32> g_drawInfoStaging;
Vector<GLint> g_zeroBaseVertices;
// Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target
// (BufferImpl::TempBufferTarget); binding it disturbs no VAO state.
Bool EnsureScratchName(ScratchBuffer& buffer) {
if (buffer.id != 0) return true;
GLuint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) return false;
buffer.id = id;
buffer.capacity = 0;
buffer.cursor = 0;
return true;
}
// Whole-buffer upload, for the two buffers that are read from offset 0 because they
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
// orphans the previous store, so the upload never waits on a dispatch still reading
// the old contents out of the same name.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
// Grow in powers of two so a batch that creeps up in size stops respecifying.
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
while (capacity < bytes) capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
buffer.cursor = 0;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
}
return true;
}
// Ring upload, for the buffers whose consumers can address a byte offset (indirect
// commands and rewritten index streams). Respecifying per batch is what an
// orphan-every-time scheme costs, and on a desktop-class driver that allocation
// dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages
// 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means
// one respecify per wrap; every byte between two wraps is written exactly once, so
// nothing in flight is overwritten, and the wrap itself orphans.
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
constexpr SizeT kMinRingBytes = 1u << 16;
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
outOffset = 0;
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
if (buffer.capacity < aligned) {
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
while (capacity < aligned) capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
buffer.cursor = 0;
} else if (buffer.cursor + aligned > buffer.capacity) {
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(buffer.capacity),
nullptr, GL_STREAM_DRAW);
buffer.cursor = 0;
}
outOffset = buffer.cursor;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
static_cast<GLsizeiptr>(bytes), data);
}
buffer.cursor += aligned;
return true;
}
// ---------------------------------------------------------------------------
// Tier resolution
// ---------------------------------------------------------------------------
// Best-first, and measured rather than assumed. MobileGlues orders its own Auto
// multiindirect -> indirect -> basevertex; on both ES drivers available here that
// is backwards, because staging a command buffer per batch costs more than the
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
// ns/op, median of three:
//
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
// ext n/a 19300
// basevertex 2500 25200
// multiindirect 5700 27600
// drawelements 5600 28700
// indirect 5800 31000
//
// Ring-allocating the command staging (instead of respecifying per batch) was
// tried first and moved the indirect tiers by less than noise, so the cost is the
// indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry
// point rather than an indirect one - actually beats replaying the sub-draws.
//
// The compute tier is deliberately absent from the ladder: it rewrites the
// primitive stream rather than replaying it, and it measured slowest of all here,
// so it stays opt-in behind the env knob (the same call MobileGlues makes - its
// Auto never selects Compute either).
constexpr GLESMultiDrawMode kAutoLadder[] = {
GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect,
GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements,
};
Bool SupportsTier(GLESMultiDrawMode tier) {
return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier);
}
GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto;
Bool g_tierResolved = false;
String g_tierResolution;
void ResolveTierOnce() {
if (g_tierResolved) return;
g_tierResolved = true;
g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
}
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
// The resolution line above says which tier was CHOSEN; this says which one a
// batch actually went through, and the two differ whenever a batch's shape
// demotes it. Worth a line each: a multi-draw path that resolves to a tier and
// then quietly runs a different one is exactly how "the batch drew nothing"
// hides.
Uint32 g_announcedTiers = 0;
void NoteTierExecuted(GLESMultiDrawMode tier) {
const Uint32 bit = 1u << static_cast<Uint32>(tier);
if (g_announcedTiers & bit) return;
g_announcedTiers |= bit;
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
}
// The tier this particular batch can actually take. A tier is demoted here when
// the batch's own shape - not the driver - rules it out; the compute tier keeps
// its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier;
// Batched tiers issue one driver entry for the whole batch, so the emulated
// gl_DrawID uniform can only hold one value across every sub-draw. A program
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
// own index (the spec's value); nothing else observes the difference.
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
tier == GLESMultiDrawMode::Compute;
if (batched && programReadsDrawID) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
// The indirect tiers describe each sub-draw as an element offset into the
// bound element array buffer. A client-memory index array has no such buffer,
// and indirect draws are not defined without one.
if (!hasIndexBuffer &&
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
return tier;
}
// ---------------------------------------------------------------------------
// Index rewriting, shared by the two tiers that fold base vertices into indices
// ---------------------------------------------------------------------------
// Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping
// the source width would be wrong, not merely tight: GL adds baseVertex to the
// index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past
// 65535 addresses a vertex the source type cannot spell. Widening also gives the
// rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase.
void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex,
Bool restartActive, Uint32 restartSentinel, Uint32* out) {
const Uint32 baseVertexBits = static_cast<Uint32>(baseVertex);
for (SizeT i = 0; i < sourceIndexCount; ++i) {
Uint32 value = 0;
switch (indexSize) {
case 1: value = source[i]; break;
case 2: {
Uint16 narrow = 0;
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
value = narrow;
break;
}
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
}
// Unsigned wraparound is the defined behaviour for a negative base vertex.
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
}
}
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
// bound index buffer or straight from the client array. Null when the sub-draw
// would read outside the buffer.
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
SizeT indexCount, SizeT indexSize) {
if (!indexBuffer) {
return static_cast<const Uint8*>(indices);
}
if (!indexBufferBytes) return nullptr;
const SizeT byteOffset = reinterpret_cast<SizeT>(indices);
const SizeT byteEnd = byteOffset + indexCount * indexSize;
if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr;
return indexBufferBytes + byteOffset;
}
// ---------------------------------------------------------------------------
// Tier: Ext - one glMultiDrawElementsBaseVertexEXT
// ---------------------------------------------------------------------------
Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount,
const GLint* basevertex) {
if (!SupportsTier(GLESMultiDrawMode::Ext)) return false;
const GLint* baseVertices = basevertex;
if (!baseVertices) {
// glMultiDrawElements: every base vertex is 0, but the entry point still
// wants an array. One permanently-zero vector serves every such batch.
if (g_zeroBaseVertices.size() < static_cast<SizeT>(drawcount)) {
g_zeroBaseVertices.resize(static_cast<SizeT>(drawcount), 0);
}
baseVertices = g_zeroBaseVertices.data();
}
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices);
NoteTierExecuted(GLESMultiDrawMode::Ext);
return true;
}
// ---------------------------------------------------------------------------
// Tiers: MultiIndirect / Indirect - synthesized indirect commands
// ---------------------------------------------------------------------------
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
// Indirect commands address indices as an element offset into the bound element
// array buffer, and an indirect draw is not defined without one.
const auto& indexBuffer = BoundIndexBuffer();
if (!indexBuffer) return false;
g_commandStaging.resize(static_cast<SizeT>(drawcount));
for (GLsizei i = 0; i < drawcount; ++i) {
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
// firstIndex counts elements, so an offset that is not a whole number of
// them cannot be expressed as a command at all.
if (byteOffset % indexSize != 0) return false;
auto& command = g_commandStaging[static_cast<SizeT>(i)];
command.count = count[i] > 0 ? static_cast<Uint32>(count[i]) : 0u;
command.instanceCount = 1;
command.firstIndex = static_cast<Uint32>(byteOffset / indexSize);
command.baseVertex = basevertex ? basevertex[i] : 0;
command.baseInstance = 0;
}
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
SizeT commandBase = 0;
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
return false;
}
// Every synthesized command carries baseInstance 0. Say so through the direct
// path, which also clears the indirect-params word index a preceding real
// indirect draw may have left pointing into its own command buffer.
SetCurrentBaseInstance(0);
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
}
if (feedDrawID) SetCurrentDrawID(0);
}
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
return true;
}
// ---------------------------------------------------------------------------
// Tier: BaseVertex - the per-sub-draw replay
// ---------------------------------------------------------------------------
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
}
if (feedDrawID) SetCurrentDrawID(0);
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
return true;
}
// ---------------------------------------------------------------------------
// Tier: DrawElements - base vertices folded into a scratch index stream
// ---------------------------------------------------------------------------
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
SizeT total = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] > 0) total += static_cast<SizeT>(count[i]);
}
if (total == 0) return true;
if (total > kMaxFlattenedIndices) return false;
const auto& indexBuffer = BoundIndexBuffer();
const Uint8* indexBufferBytes = nullptr;
SizeT indexBufferSize = 0;
if (indexBuffer) {
// The shadow is the source of truth for CPU reads, but a persistent map or
// a shader write may have moved past it since the last sync.
indexBuffer->SyncPersistentMappedRange();
indexBuffer->SyncGpuWrites();
indexBufferBytes = indexBuffer->MappedData();
indexBufferSize = indexBuffer->GetSize();
}
const Bool restartActive = RestartActive();
const Uint32 restartSentinel = RestartSentinelFor(type);
g_indexStaging.resize(total);
SizeT cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
subDrawCount, indexSize);
if (!source) {
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch",
i);
return false;
}
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
restartSentinel, g_indexStaging.data() + cursor);
cursor += subDrawCount;
}
SizeT indexBase = 0;
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
return false;
}
const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id);
cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
cursor += static_cast<SizeT>(count[i]);
}
if (feedDrawID) SetCurrentDrawID(0);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
return true;
}
// ---------------------------------------------------------------------------
// Tier: Compute - the whole batch flattened into one rebased index stream
// ---------------------------------------------------------------------------
// One index per invocation. The sub-draw an output slot belongs to is found by
// binary search over the inclusive prefix sums of the sub-draw counts, which is
// why the descriptors are sorted by construction. Sub-draws with a zero count
// repeat the previous prefix sum and are therefore skipped by the search.
//
// Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees
// four per compute stage, so the per-sub-draw descriptors share one buffer.
constexpr const char* kFlattenComputeSource = R"(#version 310 es
layout(local_size_x = 64) in;
uniform uint uElementSize;
uniform uint uDrawCount;
uniform uint uTotalIndices;
layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; };
layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; };
layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; };
uint ReadSourceIndex(uint element) {
if (uElementSize == 4u) {
return sourceWords[element];
}
if (uElementSize == 2u) {
uint word = sourceWords[element >> 1u];
return (word >> ((element & 1u) * 16u)) & 0xFFFFu;
}
uint word = sourceWords[element >> 2u];
return (word >> ((element & 3u) * 8u)) & 0xFFu;
}
void main() {
uint outIndex = gl_GlobalInvocationID.x;
if (outIndex >= uTotalIndices) {
return;
}
uint low = 0u;
uint high = uDrawCount - 1u;
while (low < high) {
uint mid = low + (high - low) / 2u;
if (drawInfo[mid * 3u + 2u] > outIndex) {
high = mid;
} else {
low = mid + 1u;
}
}
uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]);
// Unsigned wraparound is the defined behaviour for a negative base vertex. No
// restart sentinel handling: the tier declines outright while restart is enabled.
flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u];
}
)";
struct FlattenedStream {
Uint bufferId = 0;
SizeT indexCount = 0;
};
Bool EnsureComputeProgram() {
if (g_computeProgram != 0) return true;
if (g_computeProgramFailed) return false;
g_computeProgramFailed = true; // cleared again only on a complete success
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
return false;
}
const char* source = kFlattenComputeSource;
g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr);
g_GLESFuncs.glCompileShader(shader);
GLint status = GL_FALSE;
g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
g_GLESFuncs.glDeleteShader(shader);
return false;
}
const GLuint program = g_GLESFuncs.glCreateProgram();
if (program == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
g_GLESFuncs.glDeleteShader(shader);
return false;
}
g_GLESFuncs.glAttachShader(program, shader);
g_GLESFuncs.glLinkProgram(program);
g_GLESFuncs.glDeleteShader(shader);
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
g_GLESFuncs.glDeleteProgram(program);
return false;
}
g_computeProgram = program;
g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize");
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
g_computeProgramFailed = false;
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
return true;
}
// Builds the flattened stream, or leaves `out` empty when this batch's shape rules
// the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave
// the compute program current and the first storage points unbound; the
// preparation that follows re-establishes both.
void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) {
if (!SupportsTier(GLESMultiDrawMode::Compute)) return;
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return;
// Merging sub-draws into a single draw only reproduces the original primitive
// stream for list-shaped modes: a strip, loop or fan would gain primitives
// spanning the seam between two sub-draws.
const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode);
if (primitiveSize == 0) return;
// Primitive restart defeats the whole-multiple-of-a-primitive argument below,
// even for a list mode. A restart ends the current primitive, so a sub-draw of
// six GL_TRIANGLES indices with a restart after the third emits ONE triangle
// and drops the two leftover vertices - and once concatenated those leftovers
// find a third vertex in the next sub-draw and become a triangle that GL never
// draws. Splicing separator sentinels into the flattened stream could fix it,
// at the cost of a per-sub-draw offset the prefix-sum layout does not carry;
// declining is the honest trade for a tier that is already opt-in.
if (RestartActive()) return;
// The shader reads the source indices as a storage buffer, so there has to be
// a real buffer to read - a client-memory index array has none.
const auto& indexBuffer = BoundIndexBuffer();
if (!indexBuffer) return;
// A dispatch inside an open capture span is not legal, and the span would also
// observe one merged draw rather than the batch it asked for.
if (XfbImpl::IsCaptureSpanOpen()) return;
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
if (!sourceResource || sourceResource->id == 0) return;
const SizeT sourceSize = indexBuffer->GetSize();
// std430 addresses the source as uint[]; a tail shorter than a word is not
// reachable, so a narrow index type needs a word-multiple buffer.
if (indexSize < 4 && (sourceSize % 4) != 0) return;
g_drawInfoStaging.resize(3 * static_cast<SizeT>(drawcount));
SizeT total = 0;
for (GLsizei i = 0; i < drawcount; ++i) {
const SizeT subDrawCount = count[i] > 0 ? static_cast<SizeT>(count[i]) : 0;
// GL drops a trailing partial primitive per sub-draw; concatenation would
// instead splice it onto the next sub-draw's first vertices.
if (subDrawCount % primitiveSize != 0) return;
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
if (byteOffset % indexSize != 0) return;
if (subDrawCount != 0) {
const SizeT byteEnd = byteOffset + subDrawCount * indexSize;
if (byteEnd > sourceSize || byteEnd < byteOffset) return;
}
total += subDrawCount;
if (total > kMaxComputeFlattenedIndices) return;
const SizeT slot = 3 * static_cast<SizeT>(i);
g_drawInfoStaging[slot] = static_cast<Uint32>(byteOffset / indexSize);
g_drawInfoStaging[slot + 1] = static_cast<Uint32>(basevertex ? basevertex[i] : 0);
g_drawInfoStaging[slot + 2] = static_cast<Uint32>(total);
}
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
if (!EnsureComputeProgram()) return;
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
return;
}
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
g_GLESFuncs.glUseProgram(g_computeProgram);
PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram;
if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast<GLuint>(indexSize));
if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast<GLuint>(drawcount));
if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast<GLuint>(total));
g_GLESFuncs.glDispatchCompute(
static_cast<GLuint>((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT);
// Hand the storage points back to their GL default. PrepareForDraw re-syncs
// only the points the app has actually touched, so leaving a scratch buffer on
// an untouched point would keep it visible to the next shader that declares one.
for (Uint point = 0; point < 3; ++point) {
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0);
}
NoteTierExecuted(GLESMultiDrawMode::Compute);
out.bufferId = g_flattenedIndices.id;
out.indexCount = total;
}
} // namespace
// -------------------------------------------------------------------------------
// Public surface
// -------------------------------------------------------------------------------
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
GLESMultiDrawMode tier) {
const Bool esAtLeast31 =
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
switch (tier) {
case GLESMultiDrawMode::Ext:
return caps.SupportsMultiDrawElementsBaseVertex;
case GLESMultiDrawMode::MultiIndirect:
return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
case GLESMultiDrawMode::Indirect:
return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
case GLESMultiDrawMode::BaseVertex:
return caps.SupportsDrawElementsBaseVertex;
case GLESMultiDrawMode::DrawElements:
// Plain glDrawElements over a rewritten index stream: ES 2 core, so this is
// the floor every other tier can fall back to.
return true;
case GLESMultiDrawMode::Compute:
// Three storage blocks, which is inside the four ES 3.1 guarantees per stage.
return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 &&
funcs.glBindBufferBase != nullptr;
case GLESMultiDrawMode::Auto:
break;
}
return false;
}
GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested,
String* explanation) {
const auto bestAuto = [&]() {
for (const GLESMultiDrawMode tier : kAutoLadder) {
if (IsTierSupported(caps, funcs, tier)) return tier;
}
return GLESMultiDrawMode::DrawElements;
};
GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements;
String line;
if (requested == GLESMultiDrawMode::Auto) {
resolved = bestAuto();
line = String("auto -> ") + TierName(resolved);
} else if (IsTierSupported(caps, funcs, requested)) {
resolved = requested;
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved);
} else {
resolved = bestAuto();
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) +
" requested but unsupported by this driver -> " + TierName(resolved);
}
if (explanation) {
String supported;
for (const GLESMultiDrawMode tier : kAutoLadder) {
if (!IsTierSupported(caps, funcs, tier)) continue;
if (!supported.empty()) supported += ", ";
supported += TierName(tier);
}
if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) {
supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)";
}
*explanation = line + " (driver supports: " + supported + ")";
}
return resolved;
}
const char* TierName(GLESMultiDrawMode tier) {
switch (tier) {
case GLESMultiDrawMode::Auto: return "auto";
case GLESMultiDrawMode::Ext: return "ext";
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
case GLESMultiDrawMode::Indirect: return "indirect";
case GLESMultiDrawMode::BaseVertex: return "basevertex";
case GLESMultiDrawMode::DrawElements: return "drawelements";
case GLESMultiDrawMode::Compute: return "compute";
}
return "unknown";
}
GLESMultiDrawMode ResolvedTier() {
ResolveTierOnce();
return g_resolvedTier;
}
String DescribeTierResolution() {
ResolveTierOnce();
return g_tierResolution;
}
void OnBackendContextDestroyed() {
g_indirectCommands = {};
g_rebasedIndices = {};
g_drawInfo = {};
g_flattenedIndices = {};
g_computeProgram = 0;
g_computeProgramFailed = false;
g_uElementSize = -1;
g_uDrawCount = -1;
g_uTotalIndices = -1;
}
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return;
// State-independent and possibly throwing, so it runs before any GL work.
CheckPrimitiveRestartSupported(type);
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
// The compute tier dispatches BEFORE the draw state is established: doing it
// afterwards would mean unpicking the program, SSBO and index bindings
// PrepareForDraw just made, and a dispatch inside an open transform feedback
// span is not legal at all. On success it hands back a flattened index stream.
FlattenedStream flattened;
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
}
PrepareForDraw(DrawSyncBit::IndexBuffer);
if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
return;
}
const Bool feedDrawID = CurrentProgramReadsDrawID();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
Bool drawn = false;
switch (tier) {
case GLESMultiDrawMode::Ext:
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
break;
case GLESMultiDrawMode::MultiIndirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
break;
case GLESMultiDrawMode::Indirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
break;
case GLESMultiDrawMode::BaseVertex:
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
break;
case GLESMultiDrawMode::DrawElements:
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
break;
case GLESMultiDrawMode::Compute:
// Its pre-pass ran above; reaching here means it declined this batch's shape.
break;
case GLESMultiDrawMode::Auto:
break; // resolution never yields Auto
}
// Every tier above may decline a batch whose shape it cannot express. The two
// below are the floor: a base-vertex replay where the driver has one, and the
// rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive.
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
if (!drawn) {
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped",
drawcount, mode, type);
}
}
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
@@ -1,64 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <Config.h>
#include "DirectGLES.h"
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
// points on OpenGL ES, which has neither in core.
//
// Every strategy below is an emulation; they differ only in which driver capability
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
//
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
// Indirect N x glDrawElementsIndirect N + 1 upload
// BaseVertex N x glDrawElementsBaseVertex N
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
// rebased index stream
//
// Which one runs is resolved once per ES context from the driver's capabilities,
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// The tier this ES context resolved to, computed on first use and stable after.
MG_Config::GLESMultiDrawMode ResolvedTier();
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
const char* TierName(MG_Config::GLESMultiDrawMode tier);
// One line naming the resolved tier, the tiers the driver can support, and the env
// clamp if one applied. For DriverPost and the startup log.
String DescribeTierResolution();
// The resolution itself, as a pure function of a capability set: the backend feeds
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
// standalone, and both therefore report the same tier. `explanation`, when non-null,
// receives the "requested -> resolved (driver supports: ...)" line.
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode requested, String* explanation);
// Whether one tier is runnable on the given capability set, for per-row POST output.
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode tier);
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
// base vertex is 0). Owns the whole draw, preparation included: callers must not
// have run PrepareForDraw, because the compute tier has to dispatch before the
// draw state is established.
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
// The ES context is gone: every scratch buffer and the compute program belonged to
// it, so drop the names without deleting them (the dead context reclaims them).
void OnBackendContextDestroyed();
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
File diff suppressed because it is too large Load Diff
+4 -103
View File
@@ -9,22 +9,20 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
namespace DebugImpl { namespace DebugImpl {
class ErrorLopper { class ErrorLopper {
public: public:
static void Loop(const std::function<void(GLenum)>&); void Loop(std::function<void(GLenum)>);
static void Clear(); void Clear();
ErrorLopper(); ErrorLopper();
~ErrorLopper(); ~ErrorLopper();
}; };
class OpenGLScopeMarker { class OpenGLScopeMarker {
public: public:
explicit OpenGLScopeMarker(const String& scopeName); explicit OpenGLScopeMarker(String scopeName);
~OpenGLScopeMarker(); ~OpenGLScopeMarker();
}; };
} // namespace DebugImpl } // namespace DebugImpl
@@ -36,113 +34,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace VertexArrayImpl } // namespace VertexArrayImpl
namespace TextureImpl { namespace TextureImpl {
// Whether images on this format-capability target can back a colour attachment, and so
// need a colour-renderable storage format even when the frontend asked for a
// three-channel one ES never renders to. Shared by the capability probe (which passes the
// capabilities it has just queried, before the globals are published) and by the
// allocation path (which reads the active backend's), so the format the cache was probed
// with is always the format the image is created with.
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, GLenum* outFormat, GLenum* outType);
TextureTarget target = TextureTarget::Unknown);
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the image is actually created with has an alpha channel the
// frontend format does not (the three-channel colour-renderable widening). GL reads such
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
// any readback of the image has to overwrite the alpha the draw happened to leave there.
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl 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 { namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode); String ProcessOutColorLocations(const String& glslCode);
String ForceSupporterOutput(const String& glslCode); String ForceSupporterOutput(const String& glslCode);
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
String RemoveLayoutBinding(const String& glslCode); String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
// shader as a uniform and be folded into every lookup's level of detail. Declares
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
// the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite.
String EmulateTextureLodBias(const String& glslCode);
} // namespace PrgramImpl } // namespace PrgramImpl
namespace Utils { namespace Utils {
@@ -1,893 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.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 "BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include "MG_Util/Async/ShaderCompilePool.h"
#include <Config.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool IsR11G11B10FFallbackEnabled() {
return MG_Config::Features.MagmaR11G11B10FFallback;
}
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
(void)dpy;
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
Bool IsFormatIndexValid(TextureInternalFormat format) {
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
}
Bool IsLayeredTarget(TextureTarget target) {
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsMultisampleTarget(TextureTarget target) {
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
}
Bool IsTextureBufferTarget(TextureTarget target) {
return target == TextureTarget::TextureBuffer;
}
Bool IsIntegerInternalFormat(TextureInternalFormat format) {
const GLenum glFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedInternalFormat = glFormat;
GLenum imageFormat = GL_RGBA;
GLenum imageType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &imageFormat, &imageType);
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
imageFormat == GL_RGBA_INTEGER;
}
FormatCapabilityFlags GetAttachmentCaps(TextureInternalFormat format) {
FormatCapabilityFlags caps = FormatCapability::FramebufferRenderable;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format);
if (!isDepth && !isStencil) {
caps |= FormatCapability::ColorAttachment;
}
if (isDepth) {
caps |= FormatCapability::DepthAttachment;
}
if (isStencil) {
caps |= FormatCapability::StencilAttachment;
}
return caps;
}
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
VkFormatFeatureFlags features) {
FormatCapabilityFlags caps;
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
if (IsTextureBufferTarget(target)) {
if ((features & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) != 0) {
caps |= FormatCapability::Creatable;
caps |= FormatCapability::Sampled;
caps |= FormatCapability::TextureBuffer;
}
return caps;
}
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
if (sampled || renderable) {
caps |= FormatCapability::Creatable;
}
if (sampled) {
caps |= FormatCapability::Sampled;
if (linearFilter && !isInteger && !isStencil) {
caps |= FormatCapability::LinearFilter;
}
if (!isStencil && (features & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0 &&
(features & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0) {
caps |= FormatCapability::GenerateMipmap;
}
if (!isInteger && !isDepth && !isStencil) {
caps |= FormatCapability::TextureGather;
}
if (isDepth && !isStencil) {
caps |= FormatCapability::TextureShadow;
}
}
if (renderable) {
caps |= GetAttachmentCaps(logicalFormat);
if (IsLayeredTarget(target)) {
caps |= FormatCapability::FramebufferLayered;
}
}
if (IsMultisampleTarget(target)) {
caps |= FormatCapability::MultisampleTexture;
}
return caps;
}
Optional<TextureInternalFormat> ResolveVulkanFallbackLogicalFormat(TextureInternalFormat format) {
switch (format) {
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:
return TextureInternalFormat::RGBA8Snorm;
case TextureInternalFormat::RGB16:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::RGB16Snorm:
return TextureInternalFormat::RGBA16Snorm;
case TextureInternalFormat::RGB16F:
return TextureInternalFormat::RGBA16F;
case TextureInternalFormat::R11FG11FB10F:
if (IsR11G11B10FFallbackEnabled()) {
return TextureInternalFormat::RGBA16F;
}
return Nullopt;
case TextureInternalFormat::RGB32F:
return TextureInternalFormat::RGBA32F;
case TextureInternalFormat::RGB8I:
return TextureInternalFormat::RGBA8I;
case TextureInternalFormat::RGB8UI:
return TextureInternalFormat::RGBA8UI;
case TextureInternalFormat::RGB16I:
return TextureInternalFormat::RGBA16I;
case TextureInternalFormat::RGB16UI:
return TextureInternalFormat::RGBA16UI;
case TextureInternalFormat::RGB32I:
return TextureInternalFormat::RGBA32I;
case TextureInternalFormat::RGB32UI:
return TextureInternalFormat::RGBA32UI;
default:
return Nullopt;
}
}
Optional<VkFormat> ResolveVulkanFallbackFormat(TextureInternalFormat format) {
const Optional<TextureInternalFormat> fallbackLogicalFormat = ResolveVulkanFallbackLogicalFormat(format);
if (!fallbackLogicalFormat) {
return Nullopt;
}
return MG_Util::ConvertTextureInternalFormatToVkEnum(*fallbackLogicalFormat);
}
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
for (FormatCapability capability : kReportedFormatCapabilities) {
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
return true;
}
}
return false;
}
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
TextureInternalFormat fallbackFormat) {
MGLOG_D(
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
GetFormatCapabilityTargetName(targetIndex).c_str(),
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
}
Vector<Int> BuildSampleCounts(Int maxSamples) {
Vector<Int> counts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
counts.push_back(samples);
}
counts.push_back(1);
return counts;
}
void PopulateFormatCapabilitiesImpl(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache) {
cache.Clear();
if (physicalDevice == VK_NULL_HANDLE || getFormatProperties == nullptr) {
return;
}
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
const auto logicalFormat = static_cast<TextureInternalFormat>(formatIndex);
if (!IsFormatIndexValid(logicalFormat)) {
continue;
}
VkFormat nativeFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(logicalFormat);
const Optional<TextureInternalFormat> fallbackLogicalFormat =
ResolveVulkanFallbackLogicalFormat(logicalFormat);
VkFormat fallbackFormat = ResolveVulkanFallbackFormat(logicalFormat).value_or(VK_FORMAT_UNDEFINED);
VkFormatProperties nativeProperties{};
if (nativeFormat != VK_FORMAT_UNDEFINED) {
getFormatProperties(physicalDevice, nativeFormat, &nativeProperties);
}
VkFormatProperties fallbackProperties{};
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
getFormatProperties(physicalDevice, fallbackFormat, &fallbackProperties);
}
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
? nativeProperties.bufferFeatures
: nativeProperties.optimalTilingFeatures;
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
? fallbackProperties.bufferFeatures
: fallbackProperties.optimalTilingFeatures;
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(nativeCaps, fallbackCaps)) {
LogVulkanFormatCaveat(logicalFormat, targetIndex, *fallbackLogicalFormat);
}
}
if (HasFormatCapability(nativeCaps | fallbackCaps, FormatCapability::MultisampleTexture)) {
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
Int maxSamples = capabilities.MaxColorTextureSamples;
if (isDepth || isStencil) {
maxSamples = capabilities.MaxDepthTextureSamples;
} else if (isInteger) {
maxSamples = capabilities.MaxIntegerSamples;
}
cache.SampleCounts[targetIndex][formatIndex] = BuildSampleCounts(maxSamples);
}
}
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
FormatCapabilityFlags renderbufferCaps =
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D, nativeProperties.optimalTilingFeatures);
renderbufferCaps &= FormatCapability::Creatable;
if ((nativeProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) != 0) {
renderbufferCaps |= GetAttachmentCaps(logicalFormat);
renderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
fallbackRenderbufferCaps &= FormatCapability::Creatable;
if ((fallbackProperties.optimalTilingFeatures &
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
0) {
fallbackRenderbufferCaps |= GetAttachmentCaps(logicalFormat);
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
}
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
}
}
const FormatCapabilityFlags rbCaps = cache.FullCaps[renderbufferTargetIndex][formatIndex] |
cache.CaveatCaps[renderbufferTargetIndex][formatIndex];
if (HasFormatCapability(rbCaps, FormatCapability::MultisampleRenderbuffer)) {
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
BuildSampleCounts(capabilities.MaxFramebufferSamples);
}
}
}
} // namespace
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
}
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
if (!m_windowHandle.Handle) {
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
return false;
}
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
}
Bool BackendObject_DirectVulkan::InitPbufferSurface(EGLint width, EGLint height) {
VulkanRendererConfig config;
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
}
void BackendObject_DirectVulkan::Initialize() {
m_initialized = true;
}
Bool BackendObject_DirectVulkan::InitCapabilities() {
if (!m_initialized) {
MGLOG_E("Cannot initialize capabilities before backend is initialized");
return false;
}
if (!pVulkanRenderer) {
MGLOG_E("Cannot initialize capabilities: Vulkan renderer has not been created");
return false;
}
const auto& physicalDevice = pVulkanRenderer->GetPhysicalDevice();
if (!MG_Util::BackendLoader::QueryVulkanCapabilities(m_vulkanCaps, pVulkanRenderer->GetInstance(),
physicalDevice.handle)) {
MGLOG_W("DirectVulkan: failed to query extended Vulkan capabilities, using basic properties");
MG_Util::BackendLoader::FillInVulkanCapabilities(m_vulkanCaps, physicalDevice.properties);
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
return true;
}
Bool BackendObject_DirectVulkan::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
return BackendObject::InitializeEGLDisplay(dpy, major, minor);
}
Bool BackendObject_DirectVulkan::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
return RegisterEGLWindowSurface(surface, handle);
}
Bool BackendObject_DirectVulkan::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
return false;
}
if (pVulkanRenderer && m_eglSurface == surface) {
pVulkanRenderer->RequestSwapchainResize(width, height);
}
return true;
}
Bool BackendObject_DirectVulkan::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("DirectVulkan backend not initialized");
return false;
}
return RegisterEGLPbufferSurface(surface, width, height);
}
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!pVulkanRenderer) {
MGLOG_E("DirectVulkan renderer is not initialized");
return false;
}
return BackendObject::SwapEGLBuffers(dpy, draw);
}
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
}
void BackendObject_DirectVulkan::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
BackendObject::ReleaseEGLResources();
}
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
}
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
return m_rendererInfo;
}
String BackendObject_DirectVulkan::GetBackendAPIVersionString() const {
if (!m_initialized) {
return "<uninitialized DirectVulkan backend>";
}
return FormatBackendAPIVersionString(m_vulkanCaps.DeviceName, m_vulkanCaps.VulkanAPIVersion.toString(),
m_vulkanCaps.DriverVersionString);
}
const RendererInfo& GetRendererIdentity() {
static const RendererInfo rendererInfo = {
.RendererName = "Magma",
.BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt,
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
.TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, 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_texture_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
// device's: the compiler threads belong to MobileGL's shader pool and
// glCompileShader/glLinkProgram are serviced entirely inside the frontend, so there
// is no device feature to condition this on.
//
// Gated on the async flag deliberately, and this is the whole reason the gate
// exists. Advertising the string is the one part of asynchronous compilation that a
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
// the moment they see it - they enqueue whole pipeline batches and poll
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
// change as well as the threading, or the kill switch would only be half a switch.
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
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;
}
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString) {
// Format:
// <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version>
return deviceName + ", Vulkan " + vulkanApiVersionString + ", Driver " + driverVersionString;
}
BackendType BackendObject_DirectVulkan::GetBackendType() const {
return BackendType::DirectVulkan;
}
const GlobalBackendFunctionsTable& BackendObject_DirectVulkan::GetBackendFunctions() const {
static GlobalBackendFunctionsTable funcsTable;
static Bool funcsTableInitialized = false;
if (!funcsTableInitialized) {
funcsTable.Present = Present;
funcsTable.GL.DrawArrays = DrawArrays;
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
funcsTable.GL.DrawRangeElements = DrawRangeElements;
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
funcsTable.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
funcsTable.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
funcsTable.GL.DrawElementsInstanced = DrawElementsInstanced;
funcsTable.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
funcsTable.GL.DrawArraysInstanced = DrawArraysInstanced;
funcsTable.GL.DrawElementsIndirect = DrawElementsIndirect;
funcsTable.GL.DrawArraysIndirect = DrawArraysIndirect;
funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.GetTextureImage = GetTextureImage;
funcsTable.GL.DispatchCompute = DispatchCompute;
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
funcsTable.GL.MemoryBarrier = MemoryBarrier;
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
funcsTable.GL.WaitSync = WaitSync;
funcsTable.GL.DeleteSync = DeleteSync;
funcsTable.GL.GetSyncStatus = GetSyncStatus;
// Optional timer-query group: left null (the frontend then falls
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
// themselves additionally degrade to null handles when the device
// lacks timestamp support.
if (!MG_Config::Features.DisableTimerQuery) {
funcsTable.GL.IsTimerQuerySupported = IsTimerQuerySupported;
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries share the handle-based result/delete entries, which must
// exist even when timer queries are disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
}
const DynamicBackendParameters& BackendObject_DirectVulkan::GetDynamicParameters() const {
return m_dynamicParameters;
}
void BackendObject_DirectVulkan::ApplyVulkanCapabilitiesForTesting(
const MG_External::VulkanCapabilities& capabilities) {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
MutableFormatCapabilities().Clear();
}
void BackendObject_DirectVulkan::UpdateAdvertisedExtensions() {
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
// string). InitCapabilities runs after InitWindowSurface has created
// and initialized the renderer, so the advertisement can be gated on
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// 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(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
const auto mapShaderStages = [](Uint32 vkStages) {
Uint32 glStages = 0;
if ((vkStages & VK_SHADER_STAGE_VERTEX_BIT) != 0) glStages |= GL_VERTEX_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0) glStages |= GL_TESS_CONTROL_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0) {
glStages |= GL_TESS_EVALUATION_SHADER_BIT;
}
if ((vkStages & VK_SHADER_STAGE_GEOMETRY_BIT) != 0) glStages |= GL_GEOMETRY_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0) glStages |= GL_FRAGMENT_SHADER_BIT;
if ((vkStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0) glStages |= GL_COMPUTE_SHADER_BIT;
return glStages;
};
const auto mapSubgroupFeatures = [](Uint32 vkFeatures) {
Uint32 glFeatures = 0;
if ((vkFeatures & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_BASIC_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_VOTE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_VOTE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_CLUSTERED_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR;
}
if ((vkFeatures & VK_SUBGROUP_FEATURE_QUAD_BIT) != 0) {
glFeatures |= GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
}
return glFeatures;
};
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
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;
m_dynamicParameters.PointSizeRangeMin = m_vulkanCaps.PointSizeRangeMin;
m_dynamicParameters.PointSizeRangeMax = m_vulkanCaps.PointSizeRangeMax;
m_dynamicParameters.PointSizeGranularity = m_vulkanCaps.PointSizeGranularity;
m_dynamicParameters.Max3DTextureSize = m_vulkanCaps.Max3DTextureSize;
m_dynamicParameters.MaxArrayTextureLayers = m_vulkanCaps.MaxArrayTextureLayers;
m_dynamicParameters.MaxCubeMapTextureSize = m_vulkanCaps.MaxCubeMapTextureSize;
m_dynamicParameters.MaxFramebufferWidth = m_vulkanCaps.MaxFramebufferWidth;
m_dynamicParameters.MaxFramebufferHeight = m_vulkanCaps.MaxFramebufferHeight;
m_dynamicParameters.MaxFramebufferLayers = m_vulkanCaps.MaxFramebufferLayers;
m_dynamicParameters.MaxRenderbufferSize = m_vulkanCaps.MaxRenderbufferSize;
m_dynamicParameters.MaxTextureSize = m_vulkanCaps.MaxTextureSize;
m_dynamicParameters.MaxColorTextureSamples = m_vulkanCaps.MaxColorTextureSamples;
m_dynamicParameters.MaxDepthTextureSamples = m_vulkanCaps.MaxDepthTextureSamples;
m_dynamicParameters.MaxFramebufferSamples = m_vulkanCaps.MaxFramebufferSamples;
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
// an array bound: Minecraft's Blaze3D GlStateManager.TEXTURES[] holds 128 entries and Iris
// iterates [0, GL_MAX_TEXTURE_IMAGE_UNITS) over it (CompositeRenderer.renderAll), so any value
// > 128 throws ArrayIndexOutOfBoundsException. Match desktop drivers (32) for the per-stage
// limits while keeping the combined limit at our texture-unit array capacity.
constexpr Int maxPerStageTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxVertexTextureImageUnits =
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxComputeTextureImageUnits =
std::min(m_vulkanCaps.MaxComputeTextureImageUnits, maxPerStageTextureUnits);
m_dynamicParameters.MaxCombinedTextureImageUnits =
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
// Never advertise more attributes than the state layer can store: the current-value array and
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
m_dynamicParameters.MaxVertexAttribs = std::min(
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
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);
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
? m_vulkanCaps.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
// layered targets are declared separately as their own machinery lands.
{
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
// image can name. Optimistic: a format that refuses the flag is caught at image creation
// and declines the slice view there, which the clear path handles as a soft miss.
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
}
if (m_vulkanCaps.SupportsImageCubeArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else {
m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0;
m_dynamicParameters.SubgroupSupportedFeatures = 0;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
}
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
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
@@ -1,84 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "../BackendObject.h"
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Populates the same format-capability cache used by backend startup. Passing the
// instance-resolved function keeps standalone callers independent of global loader
// initialization; the physical device must remain valid for the duration of the call.
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
const MG_External::VulkanCapabilities& capabilities,
FormatCapabilityCache& cache);
class BackendObject_DirectVulkan : public BackendObject {
public:
BackendObject_DirectVulkan();
~BackendObject_DirectVulkan() override;
void Initialize() override;
Bool InitWindowSurface() override;
Bool InitCapabilities() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
const DynamicBackendParameters& GetDynamicParameters() const override;
BackendType GetBackendType() const override;
void ApplyVulkanCapabilitiesForTesting(const MG_External::VulkanCapabilities& capabilities);
private:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void UpdateAdvertisedExtensions();
void UpdateDynamicBackendParameters();
Bool m_initialized = false;
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
};
// Single-source-of-truth helpers shared with the driver POST
// (MG_Util/SelfTest/DriverPost.cpp), so the identity strings and extension list
// MobileGL reports to applications on this backend cannot drift from what the
// POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no shader
// subgroup, no timer queries). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// 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,
Bool anisotropicFilteringSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
// ends up inside the application-visible GL_RENDERER string).
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
const String& driverVersionString);
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -1,140 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
// pVulkanRenderer is reset or recreated; fence and timer-query handles
// stamped with an older generation are stale and resolve as signaled /
// available with zero results instead of dereferencing the destroyed
// renderer's frame serials and query-pool slots.
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
// safe because GL calls are serialized in this codebase, and any still-live
// program rebuilds its entry from the retained generated SPIR-V on demand.
void ClearProgramResourceCaches();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance);
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex);
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance);
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect);
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
// GL fence sync objects, mapped onto the renderer's frame-serial busy
// tracking: a fence captures the frame serial current at creation and is
// signaled once every command recorded under that serial has completed on
// the GPU.
BackendSyncHandle FenceSync();
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(BackendSyncHandle sync);
Bool GetSyncStatus(BackendSyncHandle sync);
// GPU timer queries (GL_TIME_ELAPSED spans and GL_TIMESTAMP one-shots),
// backed by per-frame VkQueryPool timestamp slots. All hooks degrade
// gracefully: null handles when the renderer is absent, the device lacks
// timestamp support, or the frame's pool is exhausted.
// Dynamic support check (GLFunctionsTable::IsTimerQuerySupported): true
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value was produced (outNanoseconds set; the
// frontend may cache it and release the handle), false when the result
// cannot be obtained yet (e.g. a wait refused because the records' frame
// serial is the current unsubmitted frame) - the handle then stays
// readable later.
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void DeleteBackendQuery(BackendQueryHandle query);
// Always 0: Vulkan cannot synchronously sample the GPU clock (timestamps
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,20 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkanResourceState.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_State::GLState {
class ProgramObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name);
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex);
}
@@ -1,138 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.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 "BufferArena.h"
namespace MobileGL::MG_Backend::DirectVulkan {
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
Shutdown();
MOBILEGL_ASSERT(desc.allocator != nullptr, "BufferArena::Initialize requires valid allocator");
MOBILEGL_ASSERT(desc.frameCount > 0, "BufferArena::Initialize requires non-zero frame count");
MOBILEGL_ASSERT(desc.usage != 0, "BufferArena::Initialize requires non-zero buffer usage");
m_desc = desc;
m_frames.clear();
m_frames.resize(desc.frameCount);
m_deferredReleases.resize(desc.frameCount);
return true;
}
void BufferArena::Shutdown() {
for (auto& frame : m_frames) {
frame.buffer.Destroy();
frame.writeCursor = 0;
}
m_frames.clear();
m_deferredReleases.clear();
m_desc = {};
}
void BufferArena::BeginFrame(Uint32 frameIndex) {
CollectDeferredReleases(frameIndex);
ResetFrame(frameIndex);
}
void BufferArena::ResetFrame(Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
m_frames[frameIndex].writeCursor = 0;
}
void BufferArena::CollectDeferredReleases(Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
m_deferredReleases[frameIndex].clear();
}
Bool BufferArena::Allocate(Uint32 frameIndex, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
AssertValidFrameIndex(frameIndex);
MOBILEGL_ASSERT(size > 0, "BufferArena::Allocate requires non-zero size");
auto& frame = m_frames[frameIndex];
const VkDeviceSize resolvedAlignment = alignment > 0 ? alignment : 1;
const VkDeviceSize offset = (frame.writeCursor + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
const VkDeviceSize endOffset = offset + size;
if (!EnsureCapacity(frameIndex, endOffset)) {
return false;
}
frame.writeCursor = endOffset;
outSlice = frame.buffer.GetSlice(offset, size);
return outSlice.IsValid();
}
Bool BufferArena::Upload(Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment,
BufferSlice& outSlice) {
MOBILEGL_ASSERT(data != nullptr || size == 0, "BufferArena::Upload data pointer is null");
if (!Allocate(frameIndex, size, alignment, outSlice)) {
return false;
}
if (outSlice.mapped != nullptr) {
Memcpy(outSlice.mapped, data, static_cast<SizeT>(size));
return true;
}
return m_frames[frameIndex].buffer.Upload(data, size, outSlice.offset);
}
VkDeviceSize BufferArena::GetWriteCursor(Uint32 frameIndex) const {
AssertValidFrameIndex(frameIndex);
return m_frames[frameIndex].writeCursor;
}
Uint32 BufferArena::GetFrameCount() const {
return static_cast<Uint32>(m_frames.size());
}
Bool BufferArena::EnsureCapacity(Uint32 frameIndex, VkDeviceSize requiredEndOffset) {
AssertValidFrameIndex(frameIndex);
auto& frame = m_frames[frameIndex];
auto& buffer = frame.buffer;
if (buffer.IsValid() && buffer.GetSize() >= requiredEndOffset) {
return true;
}
VkDeviceSize newCapacity = buffer.IsValid() ? buffer.GetSize() : 0;
if (newCapacity < m_desc.minBufferSize) {
newCapacity = m_desc.minBufferSize;
}
if (newCapacity == 0) {
newCapacity = requiredEndOffset;
}
while (newCapacity < requiredEndOffset) {
newCapacity *= 2;
}
if (buffer.IsValid()) {
m_deferredReleases[frameIndex].push_back(std::move(buffer));
}
VkBufferObjectDesc bufferDesc{};
bufferDesc.allocator = m_desc.allocator;
bufferDesc.size = newCapacity;
bufferDesc.usage = m_desc.usage;
bufferDesc.memoryUsage = m_desc.memoryUsage;
bufferDesc.allocationFlags = m_desc.allocationFlags;
if (!buffer.Create(bufferDesc)) {
return false;
}
if (m_desc.persistentlyMapped && buffer.Map() == nullptr) {
buffer.Destroy();
return false;
}
frame.writeCursor = 0;
return true;
}
void BufferArena::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "BufferArena frame index out of range");
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,56 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferArena.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 "BufferSlice.h"
#include "VkBufferObject.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
struct BufferArenaDesc {
VmaAllocator allocator = nullptr;
Uint32 frameCount = 0;
VkBufferUsageFlags usage = 0;
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags allocationFlags = 0;
VkDeviceSize minBufferSize = 0;
Bool persistentlyMapped = false;
};
class BufferArena {
public:
Bool Initialize(const BufferArenaDesc& desc);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
void ResetFrame(Uint32 frameIndex);
void CollectDeferredReleases(Uint32 frameIndex);
Bool Allocate(Uint32 frameIndex, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice);
Bool Upload(Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice);
VkDeviceSize GetWriteCursor(Uint32 frameIndex) const;
Uint32 GetFrameCount() const;
private:
struct FrameResources {
VkBufferObject buffer;
VkDeviceSize writeCursor = 0;
};
Bool EnsureCapacity(Uint32 frameIndex, VkDeviceSize requiredEndOffset);
void AssertValidFrameIndex(Uint32 frameIndex) const;
BufferArenaDesc m_desc{};
Vector<FrameResources> m_frames;
Vector<Vector<VkBufferObject>> m_deferredReleases;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,23 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/BufferSlice.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 "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
struct BufferSlice {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize size = 0;
void* mapped = nullptr;
Bool IsValid() const { return buffer != VK_NULL_HANDLE; }
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,454 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.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 "FrameContext.h"
namespace MobileGL::MG_Backend::DirectVulkan {
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
Destroy(device, commandPool);
m_frames.assign(frameCount, {});
currentFrameIndex = 0;
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount * 2;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (Uint32 i = 0; i < frameCount; ++i) {
result = CreateSyncObjectsForFrame(device, i, semaphoreInfo, fenceInfo);
if (result != VK_SUCCESS) {
Destroy(device, commandPool);
return result;
}
}
return VK_SUCCESS;
}
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
DestroySyncObjectsForFrame(device, i);
}
DestroySwapchainSemaphores(device);
if (device != VK_NULL_HANDLE && commandPool != VK_NULL_HANDLE && !m_frames.empty()) {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
m_commandPool = VK_NULL_HANDLE;
}
FrameContext::FrameData& FrameContext::GetCurrent() {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
return m_frames[currentFrameIndex];
}
const FrameContext::FrameData& FrameContext::GetCurrent() const {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
return m_frames[currentFrameIndex];
}
Bool FrameContext::IsCommandRecording() const {
return GetCurrent().isCommandRecording;
}
void FrameContext::AdvanceToNext() {
MOBILEGL_ASSERT(!m_frames.empty(), "FrameContext is not initialized");
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
const VkCommandBufferInheritanceInfo* pInheritanceInfo) {
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "BeginCommandRecording called while command buffer is already recording");
frame.hasCommandBufferRecorded = false;
VK_VERIFY(vkResetCommandBuffer(frame.commandBuffer, 0), "BeginCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = flags;
beginInfo.pInheritanceInfo = pInheritanceInfo;
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
frame.isCommandRecording = true;
if (m_recordingObserver != nullptr) {
m_recordingObserver->OnFrameCommandRecordingBegan(frame.commandBuffer);
}
return frame.commandBuffer;
}
void FrameContext::EndCommandRecording() {
auto& frame = GetCurrent();
MOBILEGL_ASSERT(frame.isCommandRecording, "EndCommandRecording called without active command buffer recording");
VK_VERIFY(vkEndCommandBuffer(frame.commandBuffer), "EndCommandRecording, vkEndCommandBuffer");
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = true;
}
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
return frame.preCommandBuffer;
}
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
return frame.preCommandBuffer;
}
void FrameContext::EndPreCommandRecordingIfOpen() {
auto& frame = GetCurrent();
if (!frame.isPreCommandRecording) {
return;
}
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = true;
}
void FrameContext::AbandonPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
}
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = false;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
return VK_SUCCESS;
}
m_swapchainImageRenderFinishedSemaphores.assign(swapchainImageCount, VK_NULL_HANDLE);
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
for (Uint32 imageIndex = 0; imageIndex < swapchainImageCount; ++imageIndex) {
VkResult result =
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &m_swapchainImageRenderFinishedSemaphores[imageIndex]);
if (result != VK_SUCCESS) {
DestroySwapchainSemaphores(device);
return result;
}
}
return VK_SUCCESS;
}
void FrameContext::DestroySwapchainSemaphores(VkDevice device) {
if (device != VK_NULL_HANDLE) {
for (auto semaphore : m_swapchainImageRenderFinishedSemaphores) {
if (semaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(device, semaphore, nullptr);
}
}
}
m_swapchainImageRenderFinishedSemaphores.clear();
}
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent();
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false;
}
// The barrier belongs in the frame's own recording. Bailing out because
// something was already recorded (the previous behaviour) dropped the
// transition entirely for every frame that never ran a default-framebuffer
// render pass - the only other thing that carries the image to
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
// handed to the WSI still in the layout it was acquired in.
// A closed-but-unsubmitted buffer can only come from a submit that already
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
// appending to it is illegal while reopening would reset the frame's own
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
// Reopening a recording here would vkResetCommandBuffer this frame's own
// commands away, so append to the open one and let the caller close it.
const Bool openedRecording = !frame.isCommandRecording;
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
presentBarrier.srcAccessMask = 0;
presentBarrier.dstAccessMask = 0;
presentBarrier.oldLayout = oldLayout;
presentBarrier.newLayout = presentLayout;
presentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
presentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
presentBarrier.image = image;
presentBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
presentBarrier.subresourceRange.baseMipLevel = 0;
presentBarrier.subresourceRange.levelCount = 1;
presentBarrier.subresourceRange.baseArrayLayer = 0;
presentBarrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
if (openedRecording) {
EndCommandRecording();
}
return true;
}
FrameContext::SubmitInfoPacket FrameContext::GetSubmitInfo(Bool shouldSubmitCommandBuffer,
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"GetSubmitInfo called while the pre-pass stream is still recording");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = commandBufferCount;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
}
FrameContext::PresentInfoPacket FrameContext::GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const {
AssertValidSwapchainImageIndex(imageIndex);
PresentInfoPacket packet{};
packet.waitSemaphore = m_swapchainImageRenderFinishedSemaphores[imageIndex];
packet.swapchain = swapchain;
packet.imageIndex = imageIndex;
packet.presentInfo.waitSemaphoreCount = 1;
packet.presentInfo.pWaitSemaphores = &packet.waitSemaphore;
packet.presentInfo.swapchainCount = 1;
packet.presentInfo.pSwapchains = &packet.swapchain;
packet.presentInfo.pImageIndices = &packet.imageIndex;
packet.presentInfo.pResults = nullptr;
return packet;
}
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout, VkFence acquireFence) {
auto& frame = GetCurrent();
VkResult result = vkWaitForFences(device, 1, &frame.imageInFlightFence, VK_TRUE, timeout);
if (result != VK_SUCCESS) {
return result;
}
// The slot's fence has been waited: every command buffer this slot
// submitted (including mid-frame flushes) has finished executing.
FreeRetiredCommandBuffers(frame);
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
// the consumed-flag reset (leaving a stale "already consumed", so the next
// submit never waited on the pending signal) and the fence reset (leaving
// the slot's fence signaled for the next submit to reuse). Only a genuine
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
// and nothing is signaled - skips the bookkeeping.
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
return result;
}
frame.imageAvailableSemaphoreConsumed = false;
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
return currentFrameIndex;
}
Uint32 FrameContext::GetFrameCount() const {
return static_cast<Uint32>(m_frames.size());
}
void FrameContext::SetRecordingObserver(IRecordingObserver* observer) {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
if (result != VK_SUCCESS) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
frame.preCommandBuffer = preReplacement;
}
// lastSubmitIndex was just written by the renderer for the submission
// that carried this command buffer.
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
void FrameContext::FreeRetiredCommandBuffers(FrameData& frame) {
if (frame.retiredCommandBuffers.empty()) {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
for (const auto& retired : frame.retiredCommandBuffers) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
}
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
return;
}
for (auto& frame : m_frames) {
// Retired buffers are appended in submit order, so the completed
// ones form a prefix.
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
vkFreeCommandBuffers(m_device, m_commandPool, 1,
&frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
frame.retiredCommandBuffers.begin() + completedCount);
}
}
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
void FrameContext::AssertValidSwapchainImageIndex(Uint32 imageIndex) const {
MOBILEGL_ASSERT(imageIndex < m_swapchainImageRenderFinishedSemaphores.size(),
"FrameContext swapchain image index out of range");
}
VkResult FrameContext::CreateSyncObjectsForFrame(VkDevice device, Uint32 frameIndex,
const VkSemaphoreCreateInfo& semaphoreInfo,
const VkFenceCreateInfo& fenceInfo) {
AssertValidFrameIndex(frameIndex);
DestroySyncObjectsForFrame(device, frameIndex);
auto& frame = m_frames[frameIndex];
VkResult result =
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &frame.imageAvailableSemaphore);
if (result != VK_SUCCESS) {
return result;
}
result = vkCreateFence(device, &fenceInfo, nullptr, &frame.imageInFlightFence);
if (result != VK_SUCCESS) {
vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr);
frame.imageAvailableSemaphore = VK_NULL_HANDLE;
return result;
}
frame.hasCommandBufferRecorded = false;
frame.isCommandRecording = false;
frame.imageAvailableSemaphoreConsumed = false;
return VK_SUCCESS;
}
void FrameContext::DestroySyncObjectsForFrame(VkDevice device, Uint32 frameIndex) {
AssertValidFrameIndex(frameIndex);
auto& frame = m_frames[frameIndex];
if (device != VK_NULL_HANDLE && frame.imageInFlightFence != VK_NULL_HANDLE) {
vkDestroyFence(device, frame.imageInFlightFence, nullptr);
}
frame.imageInFlightFence = VK_NULL_HANDLE;
if (device != VK_NULL_HANDLE && frame.imageAvailableSemaphore != VK_NULL_HANDLE) {
vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr);
}
frame.imageAvailableSemaphore = VK_NULL_HANDLE;
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
frame.imageAvailableSemaphoreConsumed = false;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,147 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.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 "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
class FrameContext {
public:
// Notified immediately after a frame command buffer begins recording
// (before any render pass has been begun); every BeginCommandRecording
// caller funnels through this single seam. Implemented by the renderer
// to prepare per-frame timer-query pools (vkCmdResetQueryPool must be
// recorded outside a render pass).
class IRecordingObserver {
public:
virtual ~IRecordingObserver() = default;
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
};
struct SubmitInfoPacket {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
};
struct PresentInfoPacket {
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
Uint32 imageIndex = 0;
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
};
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
// with the submit-tracker index it was submitted under so it can be
// freed as soon as that submission is observed complete - without
// waiting for the slot's fence to be waited again (present-less flush
// loops never wait it).
struct RetiredCommandBuffer {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Uint64 submitIndex = 0;
};
struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// Pre-pass work stream: out-of-pass commands (deferred clear
// materialization, sampled-layout transitions) for resources the
// frame's recording has not touched yet. Submitted immediately
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
// into it never has to split the frame's active render pass.
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
// complete (fence wait or completion poll).
Vector<RetiredCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
};
VkResult Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount);
void Destroy(VkDevice device, VkCommandPool commandPool);
// Lifecycle functions
FrameData& GetCurrent();
const FrameData& GetCurrent() const;
Bool IsCommandRecording() const;
void AdvanceToNext();
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording();
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
VkCommandBuffer BeginPreCommandRecording();
// Closes the pre stream if open, marking it for submission ahead of the
// frame command buffer. Safe to call when it never opened.
void EndPreCommandRecordingIfOpen();
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
// frame recordings, swapchain recreation).
void AbandonPreCommandRecording();
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const;
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const;
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
// Frees every retired command buffer whose tagged submission index is
// known complete. Driven by the renderer's submit tracker on completion
// events (fence waits and non-blocking polls), so present-less flush
// loops reclaim their buffers without any extra wait.
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
// Frees every slot's retired command buffers. Only valid when the
// caller has proven every queue submission complete.
void FreeAllRetiredCommandBuffers();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
// Observer may be null (no notifications). Not owned.
void SetRecordingObserver(IRecordingObserver* observer);
private:
void AssertValidFrameIndex(Uint32 frameIndex) const;
void AssertValidSwapchainImageIndex(Uint32 imageIndex) const;
VkResult CreateSyncObjectsForFrame(VkDevice device, Uint32 frameIndex,
const VkSemaphoreCreateInfo& semaphoreInfo,
const VkFenceCreateInfo& fenceInfo);
void DestroySyncObjectsForFrame(VkDevice device, Uint32 frameIndex);
void FreeRetiredCommandBuffers(FrameData& frame);
Vector<FrameData> m_frames;
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
Uint32 currentFrameIndex = 0;
IRecordingObserver* m_recordingObserver = nullptr;
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,527 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.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 "PipelineFactory.h"
#include <algorithm>
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_POINT_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY)
ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST)
default:
return "VK_PRIMITIVE_TOPOLOGY_UNKNOWN";
}
}
static const char* SampleCountToString(VkSampleCountFlagBits sampleCount) {
switch (sampleCount) {
ENUM_STR_CASE(VK_SAMPLE_COUNT_1_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_2_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_4_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_8_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_16_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_32_BIT)
ENUM_STR_CASE(VK_SAMPLE_COUNT_64_BIT)
default:
return "VK_SAMPLE_COUNT_UNKNOWN";
}
}
static const char* CullModeToString(VkCullModeFlags cullMode) {
switch (cullMode) {
case VK_CULL_MODE_NONE:
return "VK_CULL_MODE_NONE";
case VK_CULL_MODE_FRONT_BIT:
return "VK_CULL_MODE_FRONT_BIT";
case VK_CULL_MODE_BACK_BIT:
return "VK_CULL_MODE_BACK_BIT";
case VK_CULL_MODE_FRONT_AND_BACK:
return "VK_CULL_MODE_FRONT_AND_BACK";
default:
return "VK_CULL_MODE_UNKNOWN";
}
}
static const char* CompareOpToString(VkCompareOp compareOp) {
switch (compareOp) {
ENUM_STR_CASE(VK_COMPARE_OP_NEVER)
ENUM_STR_CASE(VK_COMPARE_OP_LESS)
ENUM_STR_CASE(VK_COMPARE_OP_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_LESS_OR_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_GREATER)
ENUM_STR_CASE(VK_COMPARE_OP_NOT_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_GREATER_OR_EQUAL)
ENUM_STR_CASE(VK_COMPARE_OP_ALWAYS)
default:
return "VK_COMPARE_OP_UNKNOWN";
}
}
static const char* LogicOpToString(VkLogicOp logicOp) {
switch (logicOp) {
ENUM_STR_CASE(VK_LOGIC_OP_CLEAR)
ENUM_STR_CASE(VK_LOGIC_OP_AND)
ENUM_STR_CASE(VK_LOGIC_OP_AND_REVERSE)
ENUM_STR_CASE(VK_LOGIC_OP_COPY)
ENUM_STR_CASE(VK_LOGIC_OP_AND_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_NO_OP)
ENUM_STR_CASE(VK_LOGIC_OP_XOR)
ENUM_STR_CASE(VK_LOGIC_OP_OR)
ENUM_STR_CASE(VK_LOGIC_OP_NOR)
ENUM_STR_CASE(VK_LOGIC_OP_EQUIVALENT)
ENUM_STR_CASE(VK_LOGIC_OP_INVERT)
ENUM_STR_CASE(VK_LOGIC_OP_OR_REVERSE)
ENUM_STR_CASE(VK_LOGIC_OP_COPY_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_OR_INVERTED)
ENUM_STR_CASE(VK_LOGIC_OP_NAND)
ENUM_STR_CASE(VK_LOGIC_OP_SET)
default:
return "VK_LOGIC_OP_UNKNOWN";
}
}
PipelineFactory::PipelineFactory(VkDevice device, const VulkanRendererConfig& config):
m_device(device), m_config(config) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "PipelineFactory: device is null");
if (m_config.DisablePipelineCache) {
MGLOG_I("DirectVulkan: pipeline cache disabled");
return;
}
VkPipelineCacheCreateInfo pipelineCacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
VK_VERIFY(vkCreatePipelineCache(m_device, &pipelineCacheInfo, nullptr, &m_pipelineCache),
"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) {
vkDestroyPipelineCache(m_device, m_pipelineCache, nullptr);
m_pipelineCache = VK_NULL_HANDLE;
}
}
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
XXHASH_VERIFY(
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,
payload.colorBlendAttachments.data(),
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
}
return XXH64_digest(m_hashState);
}
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
const HashType hash = ComputeHash(payload);
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
it->second.lastUsedFrame = m_frameCounter;
return it->second.pipeline;
}
VkPipeline pipeline = CreatePipeline(payload);
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
m_frameCounter});
return pipeline;
}
void PipelineFactory::DestroyAll() {
for (auto& pair : m_cache) {
if (pair.second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
}
}
m_cache.clear();
}
Uint32 PipelineFactory::OnFrameBoundary() {
++m_frameCounter;
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
// pipeline" memo when this returns non-zero: the memo can return a cached
// handle without touching this cache, so an evicted pipeline may still be
// memoized (present-less flush loops never reset the memo per frame).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return 0;
}
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
m_cache.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
if (renderPasses.empty() || m_cache.empty()) {
return 0;
}
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
// dimension exit) at one O(cache * log batch) scan instead of one full scan
// per dying pass.
Vector<VkRenderPass> sortedPasses = renderPasses;
std::sort(sortedPasses.begin(), sortedPasses.end());
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
evicted, sortedPasses.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (it->second.programHash == programHash) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
evicted, static_cast<unsigned long long>(programHash));
}
return evicted;
}
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineCreatePayload::kMaxColorAttachments,
"PipelineFactory: colorAttachmentCount=%u is unexpectedly large",
payload.colorAttachmentCount);
MGLOG_D("PipelineFactory::CreatePipeline: programHash=0x%llx vertexInputHash=0x%llx colorAttachmentCount=%u subpass=%u",
static_cast<unsigned long long>(payload.programHash),
static_cast<unsigned long long>(payload.vertexInputHash),
payload.colorAttachmentCount,
payload.subpass);
static constexpr VkDynamicState kDynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
VK_DYNAMIC_STATE_BLEND_CONSTANTS,
VK_DYNAMIC_STATE_DEPTH_BIAS,
VK_DYNAMIC_STATE_LINE_WIDTH,
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
VK_DYNAMIC_STATE_STENCIL_REFERENCE
};
VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(std::size(kDynamicStates));
dynamicState.pDynamicStates = kDynamicStates;
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
ia.topology = payload.topology;
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
// otherwise is what the spec expects.
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1;
vpci.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = payload.polygonMode;
raster.cullMode = payload.cullMode;
raster.frontFace = payload.frontFace;
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
raster.lineWidth = 1.0f;
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
// selector already collapses to FIRST in exactly that case - so a device without the
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
provokingVertexState.pNext = raster.pNext;
raster.pNext = &provokingVertexState;
}
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
depthStencil.depthWriteEnable = payload.depthWriteEnable ? VK_TRUE : VK_FALSE;
depthStencil.depthCompareOp = payload.depthCompareOp;
depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.stencilTestEnable = payload.stencilTestEnable ? VK_TRUE : VK_FALSE;
if (payload.stencilTestEnable) {
depthStencil.front.failOp = payload.frontStencilFailOp;
depthStencil.front.passOp = payload.frontStencilPassOp;
depthStencil.front.depthFailOp = payload.frontStencilDepthFailOp;
depthStencil.front.compareOp = payload.frontStencilCompareOp;
depthStencil.front.compareMask = 0xffffffffu;
depthStencil.front.writeMask = 0xffffffffu;
depthStencil.front.reference = 0;
depthStencil.back.failOp = payload.backStencilFailOp;
depthStencil.back.passOp = payload.backStencilPassOp;
depthStencil.back.depthFailOp = payload.backStencilDepthFailOp;
depthStencil.back.compareOp = payload.backStencilCompareOp;
depthStencil.back.compareMask = 0xffffffffu;
depthStencil.back.writeMask = 0xffffffffu;
depthStencil.back.reference = 0;
}
Vector<VkPipelineColorBlendAttachmentState> colorAttachments(payload.colorAttachmentCount);
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;
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
gpi.pStages = payload.stages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
gpi.pViewportState = &vpci;
gpi.pRasterizationState = &raster;
gpi.pMultisampleState = &ms;
gpi.pDepthStencilState = &depthStencil;
gpi.pColorBlendState = &blend;
gpi.pDynamicState = &dynamicState;
gpi.layout = payload.pipelineLayout;
gpi.renderPass = payload.renderPass;
gpi.subpass = payload.subpass;
VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
result,
static_cast<unsigned long long>(payload.programHash),
static_cast<unsigned long long>(payload.vertexInputHash),
gpi.stageCount,
PrimitiveTopologyToString(payload.topology),
payload.topology,
payload.colorAttachmentCount,
SampleCountToString(payload.rasterizationSamples),
payload.rasterizationSamples,
payload.subpass);
MGLOG_F("PipelineFactory::CreatePipeline state: cullMode=%s(0x%x) frontFace=%d depthTest=%d depthWrite=%d depthCompare=%s(%d) depthBias=%d rasterizerDiscard=%d stencilTest=%d logicOpEnable=%d logicOp=%s(%d)",
CullModeToString(payload.cullMode),
static_cast<Uint32>(payload.cullMode),
payload.frontFace,
payload.depthTestEnable ? 1 : 0,
payload.depthWriteEnable ? 1 : 0,
CompareOpToString(payload.depthCompareOp),
payload.depthCompareOp,
payload.depthBiasEnable ? 1 : 0,
payload.rasterizerDiscardEnable ? 1 : 0,
payload.stencilTestEnable ? 1 : 0,
payload.logicOpEnable ? 1 : 0,
LogicOpToString(payload.logicOp),
payload.logicOp);
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
payload.vertexInputState->vertexBindingDescriptionCount,
payload.vertexInputState->vertexAttributeDescriptionCount);
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const auto& attachment = payload.colorBlendAttachments[i];
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
i,
attachment.blendEnable == VK_TRUE ? 1 : 0,
static_cast<Uint32>(attachment.colorWriteMask),
attachment.srcColorBlendFactor,
attachment.dstColorBlendFactor,
attachment.colorBlendOp,
attachment.srcAlphaBlendFactor,
attachment.dstAlphaBlendFactor,
attachment.alphaBlendOp);
}
}
VK_VERIFY(result, "vkCreateGraphicsPipelines");
return pipeline;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,140 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.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 "Config.h"
#include "../VkIncludes.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
class PipelineFactory {
public:
using HashType = Uint64;
struct PipelineCreatePayload {
static constexpr Uint32 kMaxColorAttachments = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
HashType programHash = 0;
HashType vertexInputHash = 0;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
// which vertex a flat varying takes AND the vertex order transform feedback records for
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
// Vulkan's own convention, which is what a device without the extension gets.
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
Bool depthTestEnable = false;
Bool depthWriteEnable = false;
Bool depthBiasEnable = false;
Bool rasterizerDiscardEnable = false;
Bool logicOpEnable = false;
Bool stencilTestEnable = false;
VkCompareOp depthCompareOp = VK_COMPARE_OP_ALWAYS;
VkLogicOp logicOp = VK_LOGIC_OP_COPY;
VkStencilOp frontStencilFailOp = VK_STENCIL_OP_KEEP;
VkStencilOp frontStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp frontStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp frontStencilCompareOp = VK_COMPARE_OP_ALWAYS;
VkStencilOp backStencilFailOp = VK_STENCIL_OP_KEEP;
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;
};
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
~PipelineFactory();
PipelineFactory(const PipelineFactory&) = delete;
HashType ComputeHash(const PipelineCreatePayload& payload) const;
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
// destroyed so the caller can drop any memoized VkPipeline handle.
Uint32 OnFrameBoundary();
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
// when the caller guarantees GPU idleness for them - the render-pass manager
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
// just evicted, and a pipeline hashed on those handles is only ever bound by
// draws that also hit the render-pass entries. Also closes the handle-recycling
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
// Batched: one cache scan regardless of how many passes died in the sweep.
// Returns the number destroyed (callers invalidate memos when non-zero).
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
// Destroys every cached pipeline built from the program with content hash
// `programHash`. Called from the ProgramFactory eviction path, which proves the
// same >1024-boundary idleness (the program's pipelines are only bound by draws
// that stamp its factory entry). Returns the number destroyed.
Uint32 EvictByProgramHash(HashType programHash);
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
// depth-equality rendering (a blended prepass writes depth that later passes re-test
// with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// only depth-writing shape the chain actually uses - so every other blend
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
struct PipelineCacheEntry {
VkPipeline pipeline = VK_NULL_HANDLE;
// The hashed inputs the eviction paths key on: programHash ties the entry to
// its ProgramFactory entry, renderPass records the exact handle the hash
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
HashType programHash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
};
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config;
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -1,331 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.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 "../VkIncludes.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#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 {
None = 0,
UniformBufferDynamic,
CombinedImageSampler,
UniformTexelBuffer,
StorageBuffer,
StorageImage
};
enum class CompileOptionBit : Uint {
None = 0,
PositionYFlip = 1 << 0,
PositionZRemap = 1 << 1,
SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4,
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
// Only ever set for a draw whose every sampler binding is clamped to a single mip
// level, which makes the two forms produce identical texels (the implicit lambda is
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
ExplicitLod0Sampling = 1 << 5,
// Decorates the last vertex-processing stage's captured varyings with
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
// recorded while GL transform feedback is active, so plain draws keep the
// undecorated variant.
XfbCapture = 1 << 6,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32;
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules;
// Layout data (previously in separate VkProgramLayout)
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds;
// The bindings this program actually declares, ascending. bindingKinds is sized to the
// 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was
// scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes
// pDynamicOffsets in binding order and the writer pushes them in iteration order, so an
// unordered list would silently mis-pair dynamic offsets with their uniform blocks.
Vector<Uint32> activeBindings;
Vector<Uint32> dynamicBindings;
Vector<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{};
Uint32 activeFragmentOutputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> fragmentOutputTypes{};
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse).
mutable Uint64 lastUsedFrame = 0;
static inline VkDevice s_device = VK_NULL_HANDLE;
VkProgramObject() = default;
VkProgramObject(const VkProgramObject&) = delete;
VkProgramObject& operator=(const VkProgramObject&) = delete;
VkProgramObject(VkProgramObject&& other) noexcept {
hash = other.hash;
stages = std::move(other.stages);
modules = std::move(other.modules);
descriptorSetLayout = other.descriptorSetLayout;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
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;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) {
return *this;
}
Destroy();
hash = other.hash;
stages = std::move(other.stages);
modules = std::move(other.modules);
descriptorSetLayout = other.descriptorSetLayout;
pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings);
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;
activeFragmentOutputLocationMask = other.activeFragmentOutputLocationMask;
fragmentOutputTypes = other.fragmentOutputTypes;
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
return *this;
}
~VkProgramObject() {
Destroy();
}
private:
void Destroy() {
if (s_device != VK_NULL_HANDLE) {
if (pipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
pipelineLayout = VK_NULL_HANDLE;
}
if (descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
descriptorSetLayout = VK_NULL_HANDLE;
}
for (auto module : modules) {
if (module != VK_NULL_HANDLE) {
vkDestroyShaderModule(s_device, module, nullptr);
}
}
}
modules.clear();
stages.clear();
}
};
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
// layout handle value may be recycled for an unrelated layout, and the program
// hash may be re-inserted by a later rebuild of the same content.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device;
}
~ProgramFactory() = default;
ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
// an open-addressing map holding entries by value, so both moves existing entries.
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
// this is unchanged; on a bump it must re-run GetOrCreateProgram.
Uint64 GetCacheStructureEpoch() const { return m_cacheStructureEpoch; }
// A memoised entry pointer bypasses GetOrCreateProgram, whose per-lookup stamp is
// what keeps an in-use entry out of OnFrameBoundary's idle sweep - so such a
// caller must re-stamp the entry itself, at least once per frame boundary.
void StampProgramUse(const VkProgramObject& entry) const { entry.lastUsedFrame = m_frameCounter; }
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
// Frame boundary hook: ages the program cache and evicts long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is
// stripped conservatively and forfeits its depth write.
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
private:
struct ProgramLookupCache {
const MG_State::GLState::ProgramObject* program = nullptr;
Uint32 backendStateVersion = 0;
CompileOptionFlags flags{};
HashType hash = 0;
};
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0;
UnorderedMap<HashType, VkProgramObject> m_cache;
const VulkanRendererConfig& m_config;
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
Bool m_shaderDrawParametersEnabled = false;
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,533 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.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 "SwapchainObject.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
#if defined(__has_include)
#if __has_include(<vulkan/vk_enum_string_helper.h>)
#include <vulkan/vk_enum_string_helper.h>
#define MOBILEGL_HAS_VK_ENUM_STRING_HELPER 1
#else
#define MOBILEGL_HAS_VK_ENUM_STRING_HELPER 0
#endif
#else
#define MOBILEGL_HAS_VK_ENUM_STRING_HELPER 0
#endif
#if !MOBILEGL_HAS_VK_ENUM_STRING_HELPER
static const char* string_VkFormat(VkFormat) {
return "VkFormat(unknown)";
}
static const char* string_VkColorSpaceKHR(VkColorSpaceKHR) {
return "VkColorSpaceKHR(unknown)";
}
static const char* string_VkPresentModeKHR(VkPresentModeKHR presentMode) {
switch (presentMode) {
case VK_PRESENT_MODE_IMMEDIATE_KHR:
return "VK_PRESENT_MODE_IMMEDIATE_KHR";
case VK_PRESENT_MODE_MAILBOX_KHR:
return "VK_PRESENT_MODE_MAILBOX_KHR";
case VK_PRESENT_MODE_FIFO_KHR:
return "VK_PRESENT_MODE_FIFO_KHR";
case VK_PRESENT_MODE_FIFO_RELAXED_KHR:
return "VK_PRESENT_MODE_FIFO_RELAXED_KHR";
default:
return "VkPresentModeKHR(unknown)";
}
}
static const char* string_VkSurfaceTransformFlagBitsKHR(VkSurfaceTransformFlagBitsKHR) {
return "VkSurfaceTransformFlagBitsKHR(unknown)";
}
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool HasStencilComponent(VkFormat format) {
return format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT;
}
VkFormat FindSupportedDepthStencilFormat(VkPhysicalDevice physicalDevice) {
const VkFormat candidates[] = {VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_D32_SFLOAT};
for (VkFormat format : candidates) {
VkFormatProperties props{};
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0) {
return format;
}
}
return VK_FORMAT_UNDEFINED;
}
Uint32 FindMemoryType(VkPhysicalDevice physicalDevice, Uint32 typeFilter, VkMemoryPropertyFlags properties) {
VkPhysicalDeviceMemoryProperties memProperties{};
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
for (Uint32 i = 0; i < memProperties.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) &&
(memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
return i;
}
}
MOBILEGL_ASSERT(false, "Failed to find suitable memory type.");
return 0;
}
} // namespace
SwapchainObject::SwapchainCapabilities SwapchainObject::GetSwapchainCapabilities(VkPhysicalDevice physicalDevice,
VkSurfaceKHR surface) {
SwapchainCapabilities swapchainCapabilities{};
VK_VERIFY(
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &swapchainCapabilities.capabilities));
Uint32 formatCount = 0;
VK_VERIFY(vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr));
if (formatCount != 0) {
swapchainCapabilities.surfaceFormats.resize(formatCount);
VK_VERIFY(vkGetPhysicalDeviceSurfaceFormatsKHR(
physicalDevice, surface, &formatCount, swapchainCapabilities.surfaceFormats.data()));
}
Uint32 presentModeCount = 0;
VK_VERIFY(
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, nullptr));
if (presentModeCount != 0) {
swapchainCapabilities.presentModes.resize(presentModeCount);
VK_VERIFY(vkGetPhysicalDeviceSurfacePresentModesKHR(
physicalDevice, surface, &presentModeCount, swapchainCapabilities.presentModes.data()));
}
return swapchainCapabilities;
}
VkSurfaceFormatKHR SwapchainObject::ChooseSwapchainSurfaceFormat(
const Vector<VkSurfaceFormatKHR>& availableFormats) {
for (const auto& availableFormat : availableFormats) {
if ((availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM ||
availableFormat.format == VK_FORMAT_R8G8B8A8_UNORM) &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
for (const auto& availableFormat : availableFormats) {
if ((availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB ||
availableFormat.format == VK_FORMAT_R8G8B8A8_SRGB) &&
availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
// TODO: Properly rank other formats
return availableFormats[0];
}
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
const Vector<VkPresentModeKHR>& availablePresentModes) {
for (auto desiredPresentMode : s_desiredPresentModes) {
for (const auto& presentMode : availablePresentModes) {
if (presentMode == desiredPresentMode) {
return presentMode;
}
}
}
// TODO: Properly rank other modes
return availablePresentModes[0];
}
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
}
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
MGLOG_I("Picked surface format: [%s, %s]", string_VkFormat(pickedSurfaceFormat.format),
string_VkColorSpaceKHR(pickedSurfaceFormat.colorSpace));
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
for (const auto& pm : swapchainCapabilities.presentModes) {
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
const auto& swapchainCaps = swapchainCapabilities.capabilities;
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
if (swapchainCaps.maxImageCount != 0) {
targetImageCount = std::min(targetImageCount, swapchainCaps.maxImageCount);
}
MGLOG_I("Set minImageCount = %u", targetImageCount);
MGLOG_I("Swapchain currentTransform = %s",
string_VkSurfaceTransformFlagBitsKHR(swapchainCaps.currentTransform));
VkSwapchainCreateInfoKHR createInfo{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
createInfo.surface = surface;
createInfo.minImageCount = targetImageCount;
createInfo.imageFormat = pickedSurfaceFormat.format;
createInfo.imageColorSpace = pickedSurfaceFormat.colorSpace;
createInfo.imageExtent = swapchainCaps.currentExtent;
if (createInfo.imageExtent.width == UINT32_MAX || createInfo.imageExtent.height == UINT32_MAX) {
createInfo.imageExtent.width = std::clamp(desiredExtent.width,
swapchainCaps.minImageExtent.width,
swapchainCaps.maxImageExtent.width);
createInfo.imageExtent.height = std::clamp(desiredExtent.height,
swapchainCaps.minImageExtent.height,
swapchainCaps.maxImageExtent.height);
}
const VkExtent2D defaultFramebufferExtent = createInfo.imageExtent;
if (swapchainCaps.currentTransform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
swapchainCaps.currentTransform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
std::swap(createInfo.imageExtent.width, createInfo.imageExtent.height);
}
createInfo.imageArrayLayers = 1;
const VkImageUsageFlags requiredImageUsage =
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
MOBILEGL_ASSERT((swapchainCaps.supportedUsageFlags & requiredImageUsage) == requiredImageUsage,
"Swapchain does not support required usage flags (COLOR_ATTACHMENT | TRANSFER_DST). "
"supportedUsageFlags=0x%x",
static_cast<Uint32>(swapchainCaps.supportedUsageFlags));
VkImageUsageFlags imageUsage = requiredImageUsage;
if ((swapchainCaps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0) {
imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
createInfo.imageUsage = imageUsage;
MGLOG_I("Swapchain imageUsage = 0x%x (supportedUsageFlags = 0x%x)", static_cast<Uint32>(createInfo.imageUsage),
static_cast<Uint32>(swapchainCaps.supportedUsageFlags));
Uint32 queueFamilyIndices[] = {graphicsQueueFamily, presentQueueFamily};
if (graphicsQueueFamily != presentQueueFamily) {
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 0;
createInfo.pQueueFamilyIndices = nullptr;
}
createInfo.preTransform = swapchainCaps.currentTransform;
MGLOG_I("Set swapchain preTransform = %s", string_VkSurfaceTransformFlagBitsKHR(createInfo.preTransform));
const VkCompositeAlphaFlagBitsKHR compositeAlphaCandidates[] = {
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR
};
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
for (auto candidate : compositeAlphaCandidates) {
if ((swapchainCaps.supportedCompositeAlpha & candidate) != 0) {
createInfo.compositeAlpha = candidate;
break;
}
}
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
m_extent = createInfo.imageExtent;
// The surface-space extent this swapchain was built from, i.e. before the
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
// and makes the comparison alternate forever.
m_surfaceExtent = defaultFramebufferExtent;
m_preTransform = createInfo.preTransform;
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
Uint32 imageCount = 0;
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, nullptr));
m_images.resize(imageCount, VK_NULL_HANDLE);
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
// Fresh swapchain images hold garbage until a render pass stores into them.
m_imageContentDefined.assign(imageCount, false);
m_depthStencilContentDefined.assign(imageCount, false);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height,
imageCount);
// Properly initialize Default FBO here
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
const Int extentWidth = static_cast<Int>(defaultFramebufferExtent.width);
const Int extentHeight = static_cast<Int>(defaultFramebufferExtent.height);
const SizeT defaultAttachmentByteSize =
static_cast<SizeT>(defaultFramebufferExtent.width) *
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
colorTex->AllocateStorage(
TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D24_UNORM_S8_UINT:
depthFormat = TextureInternalFormat::Depth24Stencil8;
break;
case VK_FORMAT_D32_SFLOAT_S8_UINT:
depthFormat = TextureInternalFormat::Depth32FStencil8;
break;
case VK_FORMAT_D32_SFLOAT:
depthFormat = TextureInternalFormat::DepthComponent32F;
break;
default:
depthFormat = TextureInternalFormat::Depth24Stencil8;
break;
}
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
depthTex->SetInternalFormat(depthFormat);
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
// The default FBO's stencil attachment must track the swapchain extent:
// FramebufferObject::CheckCompleteness requires every valid attachment
// to share the same dimensions, and Init.cpp leaves a 512x512 placeholder.
// Without this the retrace-layer glReadPixels snapshot fails with
// GL_INVALID_FRAMEBUFFER_OPERATION on DirectVulkan.
TextureInternalFormat stencilFormat = TextureInternalFormat::Depth24Stencil8;
switch (m_depthStencilFormat) {
case VK_FORMAT_D32_SFLOAT_S8_UINT:
stencilFormat = TextureInternalFormat::Depth32FStencil8;
break;
case VK_FORMAT_D24_UNORM_S8_UINT:
stencilFormat = TextureInternalFormat::Depth24Stencil8;
break;
default:
// No stencil plane; mirror the depth format for consistency.
stencilFormat = depthFormat;
break;
}
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
stencilTex->SetInternalFormat(stencilFormat);
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
{extentWidth, extentHeight, 1},
defaultAttachmentByteSize}); // TODO: 4 is format size
}
void SwapchainObject::CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice) {
DestroyDepthStencilResources(device);
const auto imageCount = static_cast<Uint32>(m_images.size());
if (imageCount == 0) {
return;
}
m_depthStencilFormat = FindSupportedDepthStencilFormat(physicalDevice);
MOBILEGL_ASSERT(m_depthStencilFormat != VK_FORMAT_UNDEFINED, "No supported depth/stencil format found.");
m_depthStencilImages.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageMemories.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageViews.assign(imageCount, VK_NULL_HANDLE);
m_depthStencilImageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
for (Uint32 i = 0; i < imageCount; ++i) {
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = m_extent.width;
imageInfo.extent.height = m_extent.height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = m_depthStencilFormat;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_VERIFY(vkCreateImage(device, &imageInfo, nullptr, &m_depthStencilImages[i]), "vkCreateImage(depth)");
VkMemoryRequirements memRequirements{};
vkGetImageMemoryRequirements(device, m_depthStencilImages[i], &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_VERIFY(vkAllocateMemory(device, &allocInfo, nullptr, &m_depthStencilImageMemories[i]),
"vkAllocateMemory(depth)");
VK_VERIFY(vkBindImageMemory(device, m_depthStencilImages[i], m_depthStencilImageMemories[i], 0),
"vkBindImageMemory(depth)");
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = m_depthStencilImages[i];
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = m_depthStencilFormat;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (HasStencilComponent(m_depthStencilFormat)) {
viewInfo.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(device, &viewInfo, nullptr, &m_depthStencilImageViews[i]),
"vkCreateImageView(depth)");
}
}
void SwapchainObject::DestroyDepthStencilResources(VkDevice device) {
for (auto view : m_depthStencilImageViews) {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(device, view, nullptr);
}
}
m_depthStencilImageViews.clear();
for (auto image : m_depthStencilImages) {
if (image != VK_NULL_HANDLE) {
vkDestroyImage(device, image, nullptr);
}
}
m_depthStencilImages.clear();
for (auto memory : m_depthStencilImageMemories) {
if (memory != VK_NULL_HANDLE) {
vkFreeMemory(device, memory, nullptr);
}
}
m_depthStencilImageMemories.clear();
m_depthStencilImageLayouts.clear();
m_depthStencilFormat = VK_FORMAT_UNDEFINED;
}
void SwapchainObject::Shutdown(VkDevice device) {
DestroyDepthStencilResources(device);
for (auto imageView : m_imageViews) {
vkDestroyImageView(device, imageView, nullptr);
}
m_imageViews.clear();
if (m_swapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device, m_swapchain, nullptr);
m_swapchain = VK_NULL_HANDLE;
}
m_images.clear();
m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
return m_imageContentDefined[index];
}
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
m_imageContentDefined[index] = defined;
}
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
return m_depthStencilContentDefined[index];
}
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
m_depthStencilContentDefined[index] = defined;
}
void SwapchainObject::SetAllDepthStencilContentUndefined() {
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
m_depthStencilContentDefined[i] = false;
}
}
VkImage SwapchainObject::GetImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
return m_images[index];
}
VkImageLayout SwapchainObject::GetImageLayout(Uint32 index) const {
MOBILEGL_ASSERT(index < m_imageLayouts.size(), "Swapchain image layout index out of range");
return m_imageLayouts[index];
}
void SwapchainObject::SetImageLayout(Uint32 index, VkImageLayout layout) {
MOBILEGL_ASSERT(index < m_imageLayouts.size(), "Swapchain image layout index out of range");
m_imageLayouts[index] = layout;
}
VkImage SwapchainObject::GetDepthStencilImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImages.size(), "Swapchain depth/stencil image index out of range");
return m_depthStencilImages[index];
}
VkImageView SwapchainObject::GetDepthStencilImageView(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImageViews.size(),
"Swapchain depth/stencil image view index out of range");
return m_depthStencilImageViews[index];
}
VkImageLayout SwapchainObject::GetDepthStencilImageLayout(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilImageLayouts.size(),
"Swapchain depth/stencil image layout index out of range");
return m_depthStencilImageLayouts[index];
}
void SwapchainObject::SetDepthStencilImageLayout(Uint32 index, VkImageLayout layout) {
MOBILEGL_ASSERT(index < m_depthStencilImageLayouts.size(),
"Swapchain depth/stencil image layout index out of range");
m_depthStencilImageLayouts[index] = layout;
}
void SwapchainObject::CreateImageViews(VkDevice device) {
m_imageViews.resize(m_images.size(), VK_NULL_HANDLE);
for (SizeT i = 0; i < m_imageViews.size(); i++) {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = m_images[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = m_surfaceFormat.format;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(device, &createInfo, nullptr, &m_imageViews[i]));
}
MGLOG_I("Swapchain image views created");
}
#undef VK_VERIFY
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,98 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "../VkIncludes.h"
namespace MobileGL::MG_Backend::DirectVulkan {
class SwapchainObject {
public:
struct SwapchainCapabilities {
VkSurfaceCapabilitiesKHR capabilities;
Vector<VkSurfaceFormatKHR> surfaceFormats;
Vector<VkPresentModeKHR> presentModes;
Bool IsComplete() const {
return !surfaceFormats.empty() && !presentModes.empty();
}
};
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
void Shutdown(VkDevice device);
VkSwapchainKHR GetHandle() const { return m_swapchain; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
// created from - the value to compare a freshly queried currentExtent against.
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
VkFormat GetDepthStencilFormat() const { return m_depthStencilFormat; }
const Vector<VkImageView>& GetDepthStencilImageViews() const { return m_depthStencilImageViews; }
VkImage GetDepthStencilImage(Uint32 index) const;
VkImageView GetDepthStencilImageView(Uint32 index) const;
VkImageLayout GetDepthStencilImageLayout(Uint32 index) const;
void SetDepthStencilImageLayout(Uint32 index, VkImageLayout layout);
VkImage GetImage(Uint32 index) const;
VkImageLayout GetImageLayout(Uint32 index) const;
void SetImageLayout(Uint32 index, VkImageLayout layout);
SizeT GetImageCount() const { return m_images.size(); }
// EGL content-validity tracking for the default framebuffer. A color
// buffer's content is undefined once its image has been presented
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
// and every ancillary (depth/stencil) buffer's content is undefined
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
// render-pass manager turns an undefined attachment's tile load into
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
// garbage) and a render pass storing into an attachment sets it back
// to defined.
Bool IsImageContentDefined(Uint32 index) const;
void SetImageContentDefined(Uint32 index, Bool defined);
Bool IsDepthStencilContentDefined(Uint32 index) const;
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
void SetAllDepthStencilContentUndefined();
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
void DestroyDepthStencilResources(VkDevice device);
static constexpr VkPresentModeKHR s_desiredPresentModes[] {
VK_PRESENT_MODE_MAILBOX_KHR,
VK_PRESENT_MODE_IMMEDIATE_KHR,
VK_PRESENT_MODE_FIFO_RELAXED_KHR,
VK_PRESENT_MODE_FIFO_KHR
};
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews;
Vector<VkImageLayout> m_imageLayouts;
VkFormat m_depthStencilFormat = VK_FORMAT_UNDEFINED;
Vector<VkImage> m_depthStencilImages;
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -1,363 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "ProgramFactory.h"
#include "VkBufferManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
class ProgramObject;
class SamplerObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class UniformManager {
public:
struct SamplerBindingOverride {
Uint32 binding = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE;
};
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
VkTextureManager* textureManager = nullptr, VkSamplerManager* samplerManager = nullptr);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
// vkFreeDescriptorSets'd back to their pools (created with
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler binding (post fallback substitution,
// in binding order): the resolved texture and effective sampler, as never-reused
// lifetime ids so a freed-and-reallocated object at the same heap address can only
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo).
struct SampledBindingRecord {
Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
};
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures,
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
// walk and reports whether every visited binding still resolves to the recorded
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
// redundant glBindSampler, which always bumps it) does not prove the sampled set
// moved; this walk does, without rebuilding the set or falling off the fast path.
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
const Vector<SampledBindingRecord>& previousRecords) const;
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params
// sum, sampling-resolution generation (sampler params + texture shape), image
// epochs AND per-resource layout values - so the per-binding cached
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
// format and never silently fall back to the backing image format.
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
};
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
// owning bucket's accounting.
struct CachedDescriptorSet {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
struct DescriptorSetCacheEntry {
Vector<CachedDescriptorSet> sets;
Uint32 cursor = 0;
};
struct FrameResources {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
Vector<VkBufferView> texelBufferViews;
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
};
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
// Shared per-binding resolution for CollectSampledTextures and
// SampledBindingsUnchanged, so membership and comparison can never diverge:
// texture after the fallback substitution (may still be null when no fallback
// exists), effective sampler = unit override else the texture's own sampler.
// False = the binding is skipped (unbound with a non-2D fallback target).
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const;
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
// the bound texture stays alive through the draw via GL binding state, so callers that
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries).
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const;
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorBufferInfo& outBufferInfo) const;
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const;
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
struct UboBindResult {
Bool directBindable = false;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize range = 0; // reflected block size; constant across draws (hashed)
VkDeviceSize dynamicOffset = 0; // block range start; moves per draw (NOT hashed)
const void* payload = nullptr; // fallback UploadTransient path
VkDeviceSize payloadSize = 0;
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const;
// Shared resolution of one dynamic-UBO binding element into the
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
// bind, global-slice reuse, or transient upload. Used by the full walk
// and by the dynamic-offset-only rebind (see FastRebindMemo).
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
VkDeviceSize& outRange, Uint32& outDynamicOffset);
// The vkCmdBindDescriptorSets tail shared by the full walk and the
// dynamic-offset-only rebind: skips the driver call when this exact
// binding is already live on the command buffer (see the bind-dedup
// shadow below), otherwise binds and refreshes the shadow.
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
const ProgramFactory::VkProgramObject& programObj,
VkDescriptorSet& outDescriptorSet);
VkDevice m_device = VK_NULL_HANDLE;
VkBufferManager* m_bufferManager = nullptr;
ProgramFactory* m_programFactory = nullptr;
Vector<FrameResources> m_frames;
VkDeviceSize m_minDynamicOffsetAlignment = 1;
Uint32 m_frameCount = 0;
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;
Uint32 m_peakDescriptorSetsObserved = 0;
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
// capacity) so the descriptor-write path stops allocating on every draw.
Vector<VkWriteDescriptorSet> m_writesScratch;
Vector<VkDescriptorBufferInfo> m_bufferInfosScratch;
Vector<VkDescriptorImageInfo> m_imageInfosScratch;
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to one memoized
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
// entries with round-robin replacement rather than one: draws alternating
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
// because the frame's descriptor sets are recycled there.
struct DescriptorReuseEntry {
Uint64 signature = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Bool valid = false;
};
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
// descriptor set selected by the last cacheable full walk of a program
// whose active bindings are exactly one dynamic UBO (single descriptor)
// plus combined-image samplers. When the next call proves every sampler
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
// moved, the per-draw glUniform case - the walk collapses to: resolve one
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
// rebinding the same set with different dynamic offsets).
// Invalidation inventory: BeginFrame clears it (the frame's sets are
// recycled) and the frameIndex field guards cross-frame confusion on top;
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
// program relink bumps the backend state version and thus programObj.hash
// so the key misses; the program lifetime id is never reused, so a
// deleted-and-recreated program misses; a texture/sampler/binding change
// drops the hint upstream; an arena wrap or growth resolves a different
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
// advances, so the recorded set is never re-written within its frame.
struct FastRebindMemo {
Bool valid = false;
Uint32 frameIndex = 0;
Uint64 programLifetimeId = 0;
ProgramFactory::HashType programHash = 0;
Uint32 uboBinding = 0;
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
};
FastRebindMemo m_fastRebindMemo;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
// layout+bind point, so a pipeline-layout switch always rebinds.
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
Bool m_lastBindValid = false;
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint32 m_lastBindOffsetCount = 0;
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
// Global-UBO transient-slice reuse: MC leaves the default uniform block
// untouched across long GUI/terrain runs, so the per-draw re-upload of
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
// serial guards arena recycling; the content version guards writes).
struct GlobalUboSliceMemo {
Uint64 programLifetimeId = 0;
Uint64 frameSerial = 0;
Uint32 uboContentVersion = 0;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize range = 0;
};
static constexpr Uint32 kGlobalUboMemoSize = 4;
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
Uint32 m_globalUboMemoNext = 0;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
// sampler objects with identical state still resolve to one VkSampler. This memo only
// skips recomputing that hash. Across a draw batch the bound sampler set is stable, so a
// binding whose sampler (lifetime id + version, bumped on every setter) and texture
// (lifetime id + params version, bumped on the format/border-color setters that feed the
// key) are unchanged recycles the VkSampler it resolved last draw; a param change bumps
// a version and forces a re-resolve. Both objects are keyed by a never-reused monotonic
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
// reset each frame alongside the descriptor-set cache. Indexed by binding.
struct SamplerResolveMemo {
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false;
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
// so a reinterpreted sampler pays that scan once, not once per draw.
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool viewFormatValid = false;
// Whole resolved descriptor from this binding's last full resolve. Reused
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
// proves every resolve input unchanged; cleared with the per-frame reset
// (the cached VkSampler outlives a frame only via a fresh resolve, which
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
VkDescriptorImageInfo info{};
Bool infoValid = false;
};
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
// an entry's *Valid flags on raises this mark first, so entries at or above it are
// provably still in their constructed (all-invalid) state and clearing them is a
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
mutable Uint32 m_samplerResolveMemoHighWater = 0;
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
if (binding >= m_samplerResolveMemoHighWater) {
m_samplerResolveMemoHighWater = binding + 1;
}
}
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,64 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateBuilder.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 "VertexInputStateBuilder.h"
namespace MobileGL::MG_Backend::DirectVulkan {
VertexInputStateBuilder::VertexInputStateBuilder() {
Reset();
}
void VertexInputStateBuilder::Reset() {
m_bindings.clear();
m_attributes.clear();
m_state = {};
m_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
m_state.vertexBindingDescriptionCount = 0;
m_state.pVertexBindingDescriptions = nullptr;
m_state.vertexAttributeDescriptionCount = 0;
m_state.pVertexAttributeDescriptions = nullptr;
}
VertexInputStateBuilder& VertexInputStateBuilder::AddBinding(
Uint32 binding, Uint32 stride, VkVertexInputRate inputRate) {
VkVertexInputBindingDescription desc{};
desc.binding = binding;
desc.stride = stride;
desc.inputRate = inputRate;
m_bindings.push_back(desc);
return *this;
}
VertexInputStateBuilder& VertexInputStateBuilder::AddAttribute(
Uint32 location, Uint32 binding, VkFormat format, Uint32 offset) {
VkVertexInputAttributeDescription desc{};
desc.location = location;
desc.binding = binding;
desc.format = format;
desc.offset = offset;
m_attributes.push_back(desc);
return *this;
}
const VkPipelineVertexInputStateCreateInfo& VertexInputStateBuilder::Build() {
m_state.vertexBindingDescriptionCount = static_cast<Uint32>(m_bindings.size());
m_state.pVertexBindingDescriptions = m_bindings.empty() ? nullptr : m_bindings.data();
m_state.vertexAttributeDescriptionCount = static_cast<Uint32>(m_attributes.size());
m_state.pVertexAttributeDescriptions = m_attributes.empty() ? nullptr : m_attributes.data();
return m_state;
}
const Vector<VkVertexInputBindingDescription>& VertexInputStateBuilder::GetBindings() const {
return m_bindings;
}
const Vector<VkVertexInputAttributeDescription>& VertexInputStateBuilder::GetAttributes() const {
return m_attributes;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,31 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateBuilder.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
class VertexInputStateBuilder {
public:
VertexInputStateBuilder();
void Reset();
VertexInputStateBuilder& AddBinding(
Uint32 binding, Uint32 stride, VkVertexInputRate inputRate = VK_VERTEX_INPUT_RATE_VERTEX);
VertexInputStateBuilder& AddAttribute(Uint32 location, Uint32 binding, VkFormat format, Uint32 offset);
const VkPipelineVertexInputStateCreateInfo& Build();
const Vector<VkVertexInputBindingDescription>& GetBindings() const;
const Vector<VkVertexInputAttributeDescription>& GetAttributes() const;
private:
VkPipelineVertexInputStateCreateInfo m_state{};
Vector<VkVertexInputBindingDescription> m_bindings;
Vector<VkVertexInputAttributeDescription> m_attributes;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,503 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.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 "VertexInputStateFactory.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
const auto& attr = vao.GetAttribute(i);
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
if (!attr.Enabled) {
continue;
}
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
// The bound buffer's IDENTITY is a component of the key, and it has to be the
// buffer's never-reused lifetime id - NOT its heap address, which this used to
// hash. An address is recycled by the allocator, so a deleted-and-recreated
// buffer reproduces it; combined with a byte-identical attribute layout that
// reproduces the WHOLE content hash, and the hash is what
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
// slice was bound for its successor's draw, which is how a transform-feedback
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
// test's positions) instead of its own.
// Zero for client memory (no buffer), which is a distinct identity of its own.
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
}
return XXH64_digest(m_hashState);
}
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const {
HashType hash = 0;
if (!vao.GetBackendHashMemo(hash)) {
hash = ComputeHash(vao);
vao.SetBackendHashMemo(hash);
}
return hash;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
// memo (pure VALUES derived from the VAO configuration, so config-version
// guarding alone is sound). The draw fast path reads them from the VAO object it
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
vao.SetBackendAuxMemo(entry.layoutHash,
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
return entry;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
const auto& attr = vao.GetAttribute(location);
if (!attr.Enabled) {
continue;
}
const VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
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);
unsupportedAttribMask |= (1u << location);
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 "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
continue;
}
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;
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
const Uint32 binding = static_cast<Uint32>(bindingBufferKeys.size());
bindingBufferKeys.push_back(bufferKey);
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);
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
// else needs the extension to say it.
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
}
}
const auto& state = builder.Build();
auto& slot = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
for (const auto& divisor : entry.bindingDivisors) {
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
if (!entry.bindingDivisors.empty()) {
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
entry.state.pNext = &entry.divisorState;
} else {
entry.state.pNext = nullptr;
}
return entry;
}
void VertexInputStateFactory::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; evict entries whose last hit is far in the past.
// Erasure happens only here, never mid-frame: the draw path holds a
// reference into the current entry across its setup, and unordered_map
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
// proof is needed; an evicted entry that is used again is simply rebuilt
// from the VAO state (same hash, same content).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert.
++m_evictionEpoch;
} else {
++it;
}
}
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra, Bool isLong) {
if (isBgra) {
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
// components back into R,G,B,A order for the shader.
switch (type) {
case DataType::Uint8:
return VK_FORMAT_B8G8R8A8_UNORM;
case DataType::Uint2101010Rev:
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
case DataType::Int2101010Rev:
return VK_FORMAT_A2R10G10B10_SNORM_PACK32;
default:
return VK_FORMAT_UNDEFINED;
}
}
switch (type) {
case DataType::Uint2101010Rev:
// Packed 2_10_10_10 travels the float-normalizing path only; size is always 4. SNORM/UNORM
// normalize, SSCALED/USCALED cast the packed field to float.
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_USCALED_PACK32;
case DataType::Int2101010Rev:
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
case DataType::Float64:
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
// so they always agree without extra plumbing.
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R32G32_UINT;
case 2: return VK_FORMAT_R32G32B32A32_UINT;
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
// over two attribute locations, which the location-per-VAO-index model here does not
// express. Declined rather than fetched wrong.
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Float32:
switch (size) {
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;
}
case DataType::Float16:
// GL_HALF_FLOAT is a floating-point array type: it is never an integer attribute, and
// GL_TRUE for `normalized` is ignored for float types rather than selecting a *NORM format.
if (isInteger) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R16_SFLOAT;
case 2: return VK_FORMAT_R16G16_SFLOAT;
case 3: return VK_FORMAT_R16G16B16_SFLOAT;
case 4: return VK_FORMAT_R16G16B16A16_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Int32:
if (!isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R32_SINT;
case 2: return VK_FORMAT_R32G32_SINT;
case 3: return VK_FORMAT_R32G32B32_SINT;
case 4: return VK_FORMAT_R32G32B32A32_SINT;
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Uint32:
if (!isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) {
case 1: return VK_FORMAT_R32_UINT;
case 2: return VK_FORMAT_R32G32_UINT;
case 3: return VK_FORMAT_R32G32B32_UINT;
case 4: return VK_FORMAT_R32G32B32A32_UINT;
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Int16:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R16_SINT : (normalized ? VK_FORMAT_R16_SNORM : VK_FORMAT_R16_SSCALED);
case 2:
return isInteger ? VK_FORMAT_R16G16_SINT
: (normalized ? VK_FORMAT_R16G16_SNORM : VK_FORMAT_R16G16_SSCALED);
case 3:
return isInteger ? VK_FORMAT_R16G16B16_SINT
: (normalized ? VK_FORMAT_R16G16B16_SNORM : VK_FORMAT_R16G16B16_SSCALED);
case 4:
return isInteger ? VK_FORMAT_R16G16B16A16_SINT
: (normalized ? VK_FORMAT_R16G16B16A16_SNORM : VK_FORMAT_R16G16B16A16_SSCALED);
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Uint16:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R16_UINT : (normalized ? VK_FORMAT_R16_UNORM : VK_FORMAT_R16_USCALED);
case 2:
return isInteger ? VK_FORMAT_R16G16_UINT
: (normalized ? VK_FORMAT_R16G16_UNORM : VK_FORMAT_R16G16_USCALED);
case 3:
return isInteger ? VK_FORMAT_R16G16B16_UINT
: (normalized ? VK_FORMAT_R16G16B16_UNORM : VK_FORMAT_R16G16B16_USCALED);
case 4:
return isInteger ? VK_FORMAT_R16G16B16A16_UINT
: (normalized ? VK_FORMAT_R16G16B16A16_UNORM : VK_FORMAT_R16G16B16A16_USCALED);
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Int8:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R8_SINT : (normalized ? VK_FORMAT_R8_SNORM : VK_FORMAT_R8_SSCALED);
case 2:
return isInteger ? VK_FORMAT_R8G8_SINT
: (normalized ? VK_FORMAT_R8G8_SNORM : VK_FORMAT_R8G8_SSCALED);
case 3:
return isInteger ? VK_FORMAT_R8G8B8_SINT
: (normalized ? VK_FORMAT_R8G8B8_SNORM : VK_FORMAT_R8G8B8_SSCALED);
case 4:
return isInteger ? VK_FORMAT_R8G8B8A8_SINT
: (normalized ? VK_FORMAT_R8G8B8A8_SNORM : VK_FORMAT_R8G8B8A8_SSCALED);
default: return VK_FORMAT_UNDEFINED;
}
case DataType::Uint8:
switch (size) {
case 1:
return isInteger ? VK_FORMAT_R8_UINT : (normalized ? VK_FORMAT_R8_UNORM : VK_FORMAT_R8_USCALED);
case 2:
return isInteger ? VK_FORMAT_R8G8_UINT
: (normalized ? VK_FORMAT_R8G8_UNORM : VK_FORMAT_R8G8_USCALED);
case 3:
return isInteger ? VK_FORMAT_R8G8B8_UINT
: (normalized ? VK_FORMAT_R8G8B8_UNORM : VK_FORMAT_R8G8B8_USCALED);
case 4:
return isInteger ? VK_FORMAT_R8G8B8A8_UINT
: (normalized ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_R8G8B8A8_USCALED);
default: return VK_FORMAT_UNDEFINED;
}
default:
return VK_FORMAT_UNDEFINED;
}
}
SizeT VertexInputStateFactory::GetComponentSize(DataType type) {
switch (type) {
case DataType::Int8:
case DataType::Uint8:
return 1;
case DataType::Int16:
case DataType::Uint16:
case DataType::Float16:
return 2;
case DataType::Int32:
case DataType::Uint32:
case DataType::Float32:
case DataType::Fixed32:
return 4;
case DataType::Float64:
return 8;
default:
return 0;
}
}
SizeT VertexInputStateFactory::GetAttributeByteSize(DataType type, Int size, Bool isBgra) {
// The packed 2_10_10_10 types are a single 32-bit word for all 4 components; GL_BGRA is always
// 4 components (GL_UNSIGNED_BYTE x4 = 4 bytes, or a packed word = 4 bytes) -- both are 4 bytes.
if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) {
return 4;
}
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
@@ -1,129 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.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 "Config.h"
#include "VertexInputStateBuilder.h"
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
#include <Includes.h>
#include "../VkIncludes.h"
namespace MobileGL::MG_Backend::DirectVulkan {
class VertexInputStateFactory {
public:
using HashType = Uint64;
enum class VertexStreamConversion : Uint8 {
None = 0,
Repack,
ScaledIntegerToFloat32,
};
struct BackendVertexInputState {
HashType hash = 0;
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
// unsupported mask) - NO buffer identities. `hash` mixes each bound
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
// identity per buffer; keying pipelines on that minted one VkPipeline per
// chunk section for an identical layout, defeating pipeline reuse and the
// per-draw memo. Pipelines depend only on the layout, so they key on this
// instead.
HashType layoutHash = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age are evicted (CPU heap only).
// Mutable: the VAO's state-pointer memo fast path stamps it through
// a const entry reference.
mutable Uint64 lastUsedFrameBoundary = 0;
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0;
// Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time.
Uint32 attributeLocationMask = 0;
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
// rate advances once per instance and nothing else, so anything else has to be
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
// binding uses divisor 1, which is what the plain input rate already means.
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
};
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
};
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
m_config(config), m_physicalDevice(physicalDevice) {}
~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
// Readers that find the aux memo valid can use these without resolving the entry.
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
}
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths.
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
// Frame boundary hook: ages the cache and evicts entries not hit for many
// frames. The key mixes each bound buffer's never-reused lifetime id, so
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
// not by luck: a recreated buffer can no longer land back on its dead
// predecessor's key. Without eviction the map grows for the whole session.
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
// and the draw path's entry reference never spans a frame boundary, so
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
// compare except on sweep boundaries.
void OnFrameBoundary();
static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
// an unknown/unsupported type.
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
Bool isLong = 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;
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
// so INSERT invalidates references to stored values. The draw path (and
// the VAOs' state-pointer memos) hold entry pointers across inserts;
// only the unique_ptr cell moves, never the pointee.
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
// their heap-allocated entry (stable across map insert/rehash by
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
Uint64 m_evictionEpoch = 1;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,726 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.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 "VkBufferManager.h"
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
constexpr SizeT kLiveResourcePruneThreshold = 256;
// A zero-copy persistent buffer is created once and never recreated (the app holds
// its mapped pointer), and may be bound to any role, so it carries every usage.
// TRANSFER_DST is added by CreateResidentStorage.
constexpr VkBufferUsageFlags kPersistentBackedUsage =
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
// The app writes into the persistent map with no explicit flush, so its memory must
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
using MG_State::GLState::BackendBufferResource;
using MG_State::GLState::BufferBackendOps;
using MG_State::GLState::BufferObject;
// The manager owned by the active VulkanRenderer; immediate ops route here.
VkBufferManager* g_activeBufferManager = nullptr;
void Ops_Respecify(BufferObject& bufferObject) {
if (g_activeBufferManager) {
g_activeBufferManager->OnRespecify(bufferObject);
}
}
void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
if (g_activeBufferManager) {
g_activeBufferManager->OnSubData(bufferObject, offset, size);
}
}
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
if (g_activeBufferManager) {
g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
}
}
// The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
// host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
// bound as a shader storage buffer), so nothing needs copying - but coherence only
// says the writes are visible once they have happened, so the work has to retire
// first.
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
(void)bufferObject;
if (pVulkanRenderer) {
pVulkanRenderer->FinishPendingGpuWork();
}
}
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (g_activeBufferManager) {
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
}
return nullptr;
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (g_activeBufferManager) {
g_activeBufferManager->OnResourceDestroyed(std::move(resource));
}
// No active manager: the device/allocator is gone or going away and
// Shutdown() already destroyed the storage; dropping the handle here
// must not touch Vulkan. VkBufferResource's dtor destroys via VMA only
// when the allocation is still valid, which Shutdown() cleared.
}
const BufferBackendOps g_vulkanBufferBackendOps = {
.Respecify = Ops_Respecify,
.SubData = Ops_SubData,
.FlushMappedRange = Ops_FlushMappedRange,
.OnDestroy = Ops_OnDestroy,
.AcquirePersistentMap = Ops_AcquirePersistentMap,
.ReadbackFromGpu = Ops_ReadbackFromGpu,
};
} // namespace
Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) {
Shutdown();
MOBILEGL_ASSERT(initInfo.allocator != nullptr, "VkBufferManager::Initialize requires valid allocator");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkBufferManager::Initialize requires non-zero frame count");
m_initInfo = initInfo;
m_deferredBufferReleases.resize(initInfo.frameCount);
m_deferredResourceReleases.resize(initInfo.frameCount);
m_currentFrameIndex = 0;
m_frameSerial = 1;
m_completedSerialFloor = 0;
if (!InitializeTransientArenas()) {
return false;
}
g_activeBufferManager = this;
MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
return true;
}
void VkBufferManager::Shutdown() {
if (g_activeBufferManager == this) {
g_activeBufferManager = nullptr;
if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {
MG_State::GLState::SetBufferBackendOps(nullptr);
}
}
m_transientUploadArena.Shutdown();
DestroyAllDeferredReleases();
ReleaseAllLiveResources();
m_copyProvider = nullptr;
m_initInfo = {};
m_currentFrameIndex = 0;
m_frameSerial = 1;
m_completedSerialFloor = 0;
}
Bool VkBufferManager::RecreateTransientArenas(Uint32 frameCount) {
MOBILEGL_ASSERT(m_initInfo.allocator != nullptr,
"VkBufferManager::RecreateTransientArenas requires initialized manager");
MOBILEGL_ASSERT(frameCount > 0, "VkBufferManager::RecreateTransientArenas requires non-zero frame count");
// Callers guarantee the device is idle around arena recreation.
NotifyDeviceIdle();
m_transientUploadArena.Shutdown();
m_initInfo.frameCount = frameCount;
DestroyAllDeferredReleases();
m_deferredBufferReleases.resize(frameCount);
m_deferredResourceReleases.resize(frameCount);
m_currentFrameIndex = 0;
return InitializeTransientArenas();
}
void VkBufferManager::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::BeginFrame frame index out of range");
m_currentFrameIndex = frameIndex;
++m_frameSerial;
CollectDeferredReleases(frameIndex);
m_transientUploadArena.BeginFrame(frameIndex);
}
void VkBufferManager::CollectAllDeferredReleases() {
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
CollectDeferredReleases(frameIndex);
}
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
m_transientUploadArena.CollectDeferredReleases(frameIndex);
}
}
void VkBufferManager::NotifyDeviceIdle() {
// Everything submitted so far has completed. Work recorded for the
// current frame has not been submitted yet, so the current serial
// remains busy.
if (m_frameSerial > 0) {
m_completedSerialFloor = m_frameSerial - 1;
}
}
void VkBufferManager::NotifyFrameSerialComplete(Uint64 serial) {
// The current serial's work is still being recorded; a completion
// report for it (or beyond) can only come from a stale caller.
if (serial >= m_frameSerial) {
return;
}
m_completedSerialFloor = std::max(m_completedSerialFloor, serial);
}
void VkBufferManager::SetCopyCommandProvider(IBufferCopyCommandProvider* provider) {
m_copyProvider = provider;
}
Uint64 VkBufferManager::GetCompletedSerial() const {
const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1;
const Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0;
return std::max(completed, m_completedSerialFloor);
}
Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const {
return resource.lastUseSerial > GetCompletedSerial();
}
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
(void)kind;
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
}
Bool VkBufferManager::InitializeTransientArenas() {
return m_transientUploadArena.Initialize({
.allocator = m_initInfo.allocator,
.frameCount = m_initInfo.frameCount,
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
.memoryUsage = m_initInfo.transientMemoryUsage,
.allocationFlags = m_initInfo.transientAllocationFlags,
.minBufferSize = m_initInfo.minUploadBytes,
.persistentlyMapped = m_initInfo.transientPersistentMapping,
});
}
VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) {
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
}
VkBufferResource* VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
// 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 static_cast<VkBufferResource*>(existing.get());
}
auto resource = MakeShared<VkBufferResource>();
VkBufferResource* raw = resource.get();
bufferObject->SetBackendResource(resource);
TrackLiveResource(resource);
return raw;
}
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
// Sweep on a doubling watermark rather than on every insert past the threshold. The old
// form walked the whole vector for each new buffer once the list passed 256, and when the
// buffers are all live the walk removes nothing and the list grows by one - so creating N
// live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep
// still removes exactly the expired entries, just less often and with the same bound on how
// much dead weight can accumulate (at most as many entries as were live at the last sweep).
if (m_liveResources.size() >= std::max<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
m_liveResourcesLastPruned = m_liveResources.size();
}
m_liveResources.push_back(resource);
}
void VkBufferManager::ReleaseAllLiveResources() {
for (auto& weak : m_liveResources) {
if (auto resource = weak.lock()) {
BumpSliceEpoch(*resource);
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
resource->lastUseSerial = 0;
resource->pendingFullUpload = true;
resource->transientSlice = {};
resource->transientFrameSerial = 0;
}
}
m_liveResources.clear();
}
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
// The only place a resident VkBuffer handle is minted, so every resident slice
// change funnels through here (callers release the old handle first).
BumpSliceEpoch(resource);
// Staged range copies write resident storage with vkCmdCopyBuffer.
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
const Bool created = resource.buffer.Create({
.allocator = m_initInfo.allocator,
.size = size,
.usage = usage,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = kResidentBufferAllocationFlags,
.requiredFlags = requiredFlags,
});
if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size));
resource.buffer.Destroy();
resource.storageSize = 0;
resource.usageFlags = 0;
return false;
}
resource.storageSize = size;
resource.usageFlags = usage;
return true;
}
Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource,
MG_State::GLState::BufferObject& bufferObject) {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
const VkBufferUsageFlags usage = resource.usageFlags;
DeferRelease(std::move(resource.buffer));
if (!CreateResidentStorage(resource, size, usage)) {
resource.pendingFullUpload = true;
return false;
}
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true;
return false;
}
resource.pendingFullUpload = false;
return true;
}
Bool VkBufferManager::StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
SizeT offset, SizeT size) {
if (!m_copyProvider) {
return false;
}
BufferSlice staging{};
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), 16, staging)) {
return false;
}
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
if (commandBuffer == VK_NULL_HANDLE) {
return false;
}
// Order the copy after every prior read/write of this buffer, both from
// in-flight frames (submission order) and from commands already recorded
// in this frame's command buffer.
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&beforeBarrier, 0, nullptr, 0, nullptr);
VkBufferCopy region{};
region.srcOffset = staging.offset;
region.dstOffset = static_cast<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
&afterBarrier, 0, nullptr, 0, nullptr);
resource.lastUseSerial = m_frameSerial;
return true;
}
void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return; // lazy: AcquireResidentSlice performs a full upload on creation
}
// A respecify can change the size, the usage hint (so the resident/streamed
// route), and the contents at once; retire every memo before deciding what to
// do about the storage.
BumpSliceEpoch(*resource);
// Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid()) {
return; // streaming-only resource: shadow + serial are enough
}
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
if (size == 0) {
DeferRelease(std::move(resource->buffer));
resource->storageSize = 0;
resource->pendingFullUpload = false;
return;
}
if (size != resource->storageSize || IsResourceBusy(*resource)) {
// Conditional orphan: only swap the storage when the old one is
// still referenced by the GPU (or no longer fits).
SwapStorageAndUploadAll(*resource, bufferObject);
return;
}
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
}
}
void VkBufferManager::OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return;
}
// Drops the streaming memo below and may end in a storage swap or a deferred
// full re-upload, so no memoised slice survives this.
BumpSliceEpoch(*resource);
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
}
if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
resource->pendingFullUpload = true;
return;
}
if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
}
return;
}
// Busy partial write: stage + GPU copy preserves GL ordering within the
// frame and leaves bytes outside the range (possibly GPU-written, e.g.
// SSBO) intact. Fall back to a storage swap if staging is unavailable.
if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
SwapStorageAndUploadAll(*resource, bufferObject);
}
}
void VkBufferManager::OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
auto* resource = ResourceOf(bufferObject);
if (!resource) {
return;
}
BumpSliceEpoch(*resource);
resource->transientFrameSerial = 0;
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
return;
}
if (static_cast<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
resource->pendingFullUpload = true;
return;
}
const SizeT offset = range.start;
const SizeT size = range.end - range.start;
// GL_MAP_UNSYNCHRONIZED_BIT: the app guarantees it does not overwrite
// data the GPU is still reading; honour it with a direct host write.
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
}
return;
}
if (!StagedRangeCopy(*resource, bufferObject, offset, size)) {
SwapStorageAndUploadAll(*resource, bufferObject);
}
}
void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {
if (!resource) {
return;
}
auto vkResource = std::static_pointer_cast<VkBufferResource>(std::move(resource));
if (!vkResource->buffer.IsValid()) {
return;
}
if (m_deferredResourceReleases.empty()) {
vkResource->buffer.Destroy();
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResourceReleases.size(),
"VkBufferManager::OnResourceDestroyed current frame index out of range");
// Keep the whole resource alive until this frame slot's fence has been
// waited, then the storage is destroyed with it.
m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource));
}
void* VkBufferManager::AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject.GetSize());
if (size == 0) {
return nullptr;
}
auto resource = std::static_pointer_cast<VkBufferResource>(bufferObject.GetBackendResource());
if (!resource) {
resource = MakeShared<VkBufferResource>();
bufferObject.SetBackendResource(resource);
TrackLiveResource(resource);
}
// Bumped for the request, not just for the storage it may create. This is the
// one call the frontend makes when a buffer becomes persistently mapped for
// writing (BufferObject::AcquireMemoryRange), and a map the backend declines
// keeps mutating its shadow with no further API call - so it is what lets
// GetSliceEpochCounter stand for "no buffer needs a persistent-map range push".
BumpSliceEpoch(*resource);
// Idempotent: an already-backed buffer returns the same mapped base.
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
return resource->buffer.GetMappedData();
}
// One-time creation of HOST_VISIBLE + HOST_COHERENT, persistently mapped storage
// carrying every usage (never recreated, so the app's pointer never dangles). Seed
// it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer));
const VkBufferUsageFlags persistentUsage =
kPersistentBackedUsage |
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false;
resource->storageSize = 0;
resource->usageFlags = 0;
return nullptr;
}
const Uint8* seed = bufferObject.MappedData();
if (seed != nullptr) {
resource->buffer.Upload(seed, size, 0);
}
resource->persistentMapped = true;
resource->pendingFullUpload = false;
resource->storageSize = size;
resource->lastUseSerial = 0;
return resource->buffer.GetMappedData();
}
Bool VkBufferManager::AcquireResidentSlice(BufferKind kind,
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice) {
const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind);
MOBILEGL_ASSERT(requiredUsage != 0, "VkBufferManager::AcquireResidentSlice unsupported buffer kind");
MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireResidentSlice requires valid buffer object");
auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange();
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false;
}
// Zero-copy persistent buffers already hold the app's live coherent writes in
// host-visible storage carrying every usage; bind directly, no re-upload/staging.
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
resource->lastUseSerial = m_frameSerial;
outSlice = resource->buffer.GetSlice(0, size);
return outSlice.IsValid();
}
const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size ||
((resource->usageFlags & requiredUsage) != requiredUsage) ||
resource->pendingFullUpload;
if (needsRecreate) {
const VkBufferUsageFlags usage = resource->usageFlags | requiredUsage;
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, usage)) {
return false;
}
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy();
resource->storageSize = 0;
resource->usageFlags = 0;
return false;
}
resource->pendingFullUpload = false;
}
resource->lastUseSerial = m_frameSerial;
outSlice = resource->buffer.GetSlice(0, size);
return true;
}
Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind,
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice) {
(void)kind;
MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireStreamedSlice requires valid buffer object");
auto resource = GetOrCreateResource(bufferObject);
bufferObject->SyncPersistentMappedRange();
// A persistently mapped resource's storage IS the application's copy of the bytes -
// the frontend adopted it in place of the shadow and hands out pointers into it, and
// a shader can have written bytes the shadow never saw (a transform feedback
// capture). Streaming a second copy would feed this draw the stale shadow, and the
// downgrade below would release the storage the application still points at,
// breaking the "never recreated" promise AcquirePersistentMap makes.
if (resource->persistentMapped) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false;
}
const Uint64 changeSerial = bufferObject->GetChangeSerial();
if (resource->transientFrameSerial == m_frameSerial && resource->transientChangeSerial == changeSerial &&
resource->transientSize == size && resource->transientSlice.IsValid()) {
outSlice = resource->transientSlice;
return true;
}
// Idle-content promotion: see the field comments in VkBufferResource. The
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
// above swallows repeat draws), so a promotion needs the content stable
// for kStreamedPromotionStreak whole frames - one no-op frame does not
// trigger the resident round-trip, whose creation upload is itself a
// staged copy worth avoiding for content that is about to change again.
constexpr Uint32 kStreamedPromotionStreak = 2;
if (resource->promotedResident) {
if (resource->promotedChangeSerial == changeSerial &&
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
resource->promotedResident = false;
resource->unchangedStreak = 0;
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
resource->transientFrameSerial != 0) {
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
// Promotion moves the buffer off the arena and onto resident storage.
resource->promotedResident = true;
resource->promotedChangeSerial = changeSerial;
BumpSliceEpoch(*resource);
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
return true;
}
resource->promotedResident = false; // resident creation failed: stream as before
}
} else {
resource->unchangedStreak = 0;
}
// A fresh arena allocation: a different slice than the last call handed back,
// and (below) the point where a promoted buffer's resident storage is released.
// The stable-promotion exit above returns before this, so a buffer the app has
// stopped touching keeps one slice for as long as it keeps its resident storage.
BumpSliceEpoch(*resource);
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
outSlice)) {
return false;
}
resource->transientSlice = outSlice;
resource->transientFrameSerial = m_frameSerial;
resource->transientChangeSerial = changeSerial;
resource->transientSize = size;
// Streaming path is authoritative now; release resident storage so we do
// not keep a second, stale copy alive (downgrade).
if (resource->buffer.IsValid()) {
DeferRelease(std::move(resource->buffer));
resource->storageSize = 0;
}
return true;
}
void VkBufferManager::DeferRelease(VkBufferObject&& buffer) {
if (!buffer.IsValid()) {
return;
}
if (m_deferredBufferReleases.empty()) {
buffer.Destroy();
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::DeferRelease current frame index out of range");
m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(buffer));
}
void VkBufferManager::CollectDeferredReleases(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(),
"VkBufferManager::CollectDeferredReleases frame index out of range");
m_deferredBufferReleases[frameIndex].clear();
m_deferredResourceReleases[frameIndex].clear();
}
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
switch (kind) {
case BufferKind::Vertex:
case BufferKind::Index:
// A GL buffer can be rebound between ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER,
// and may even be used as both within the same draw setup. Keep resident
// vertex/index buffers compatible with both roles from the start so we
// never need to recreate a buffer after it has already been bound.
return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect:
return VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
default:
return 0;
}
}
void VkBufferManager::DestroyAllDeferredReleases() {
for (auto& releases : m_deferredBufferReleases) {
for (auto& buffer : releases) {
buffer.Destroy();
}
releases.clear();
}
m_deferredBufferReleases.clear();
for (auto& releases : m_deferredResourceReleases) {
for (auto& resource : releases) {
resource->buffer.Destroy();
}
releases.clear();
}
m_deferredResourceReleases.clear();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,196 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.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 "BufferArena.h"
#include "MG_State/GLState/BufferState/BufferObject.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class BufferKind : Uint8 {
Vertex,
Index,
Uniform,
TextureBuffer,
ShaderStorage,
Indirect,
};
struct VkBufferManagerInitInfo {
VmaAllocator allocator = nullptr;
Uint32 frameCount = 0;
VkDeviceSize minUploadBytes = 4 * 1024 * 1024;
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
Bool transientPersistentMapping = false;
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
// carries the transform feedback usage so capture targets can bind directly.
Bool transformFeedbackUsageEnabled = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
// Owned (refcounted) by the frontend BufferObject; the manager holds only weak
// references (for shutdown) plus strong references on deferred-release lists.
class VkBufferResource : public MG_State::GLState::BackendBufferResource {
public:
~VkBufferResource() override = default;
// Resident storage (may be invalid for streaming-only buffers).
VkBufferObject buffer;
VkDeviceSize storageSize = 0;
VkBufferUsageFlags usageFlags = 0;
// Frame serial of the last GPU reference; drives busy tracking.
Uint64 lastUseSerial = 0;
// Set when an immediate op could not be applied; forces a full re-upload
// on the next AcquireResidentSlice.
Bool pendingFullUpload = false;
// Backs a zero-copy coherent persistent map (PipeResource GPU residency): the
// buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
Bool persistentMapped = false;
// Bumped from a manager-wide counter every time anything that decides which
// BufferSlice an Acquire*Slice call hands back changes: storage created or
// released, a full re-upload becoming due, a promotion/demotion between
// resident and streamed storage, or a new per-frame arena slice. Callers that
// memoise a resolved slice compare this to prove the memo still describes the
// buffer. The counter is manager-wide (never per-resource) so a freshly
// created resource - including one that replaces a destroyed resource at the
// same address - can never reproduce a value some memo already holds. 0 means
// "no slice has ever been handed out", which no memo can match.
Uint64 sliceEpoch = 0;
// Cached transient (streaming) slice for the current frame.
BufferSlice transientSlice{};
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
// Streaming re-copies the whole store into the per-frame arena on every
// frame, which is right for genuinely per-frame data but pure waste for a
// Dynamic-hinted buffer the app stopped touching. After the content
// survives kStreamedPromotionStreak frame boundaries unchanged it is
// promoted to resident storage (one final upload, then zero per-frame
// cost); the first content change demotes it back to streaming, and the
// streaming path's existing downgrade releases the resident store.
Uint32 unchangedStreak = 0;
Bool promotedResident = false;
Uint64 promotedChangeSerial = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
// for staged buffer-range copies. Implemented by VulkanRenderer.
class IBufferCopyCommandProvider {
public:
virtual ~IBufferCopyCommandProvider() = default;
virtual VkCommandBuffer AcquireBufferCopyCommandBuffer() = 0;
};
class VkBufferManager {
public:
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
void Shutdown();
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred buffer/resource releases (and the
// transient arena's parked superseded blocks). Only valid when the
// caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to
// and including `serial` is complete. Raises the completed floor so
// GetCompletedSerial reflects real fence progress instead of only the
// frameSerial-minus-frameCount inference.
void NotifyFrameSerialComplete(Uint64 serial);
void SetCopyCommandProvider(IBufferCopyCommandProvider* provider);
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice);
// Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Draw-time acquire for streamed buffers: uploads the whole shadow into
// the per-frame arena (cached by change serial), releasing any resident
// storage the buffer may still own.
Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Zero-copy persistent map (PipeResource GPU residency): create (once) a
// HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage,
// seed it from the shadow, and return its mapped base for the app to write into
// directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow).
void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject);
// Immediate ops, dispatched from the frontend BufferBackendOps table.
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess);
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
Uint64 GetFrameSerial() const { return m_frameSerial; }
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
// memo was taken means no buffer this manager owns changed which slice it hands
// back, and none was persistently mapped, in between - so a memo of resolved
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
// Highest frame serial whose GPU work is known complete; serials at or
// below it may be considered signaled. Drives IsResourceBusy and the
// backend GL fence objects.
Uint64 GetCompletedSerial() const;
// Busy = potentially referenced by GPU work that has not been fenced yet
// (including commands recorded for the current, unsubmitted frame).
Bool IsResourceBusy(const VkBufferResource& resource) const;
private:
Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
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);
// Swap storage (conditional orphan) and refill it from the shadow copy.
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
// Record a staging-slice copy into the resident storage, ordered against
// in-flight and already-recorded GPU work.
Bool StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
SizeT offset, SizeT size);
void DeferRelease(VkBufferObject&& buffer);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyAllDeferredReleases();
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
void ReleaseAllLiveResources();
// See VkBufferResource::sliceEpoch.
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
SizeT m_liveResourcesLastPruned = 0;
Uint32 m_currentFrameIndex = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
// Never reset (not even by Shutdown): a value handed to a resource must stay
// unique for the process, or a memo taken before a re-initialize could match
// a different resource's state after it.
Uint64 m_sliceEpochCounter = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,179 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.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 "VkBufferObject.h"
namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
other.m_allocation = nullptr;
other.m_mappedData = nullptr;
other.m_size = 0;
}
VkBufferObject& VkBufferObject::operator=(VkBufferObject&& other) noexcept {
if (this == &other) {
return *this;
}
Destroy();
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
other.m_allocation = nullptr;
other.m_mappedData = nullptr;
other.m_size = 0;
return *this;
}
VkBufferObject::~VkBufferObject() {
Destroy();
}
Bool VkBufferObject::Create(const VkBufferObjectDesc& desc) {
return Create(desc.allocator, desc.size, desc.usage, desc.memoryUsage, desc.allocationFlags,
desc.requiredFlags);
}
Bool VkBufferObject::Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage,
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags,
VkMemoryPropertyFlags requiredFlags) {
MOBILEGL_ASSERT(allocator != nullptr, "VkBufferObject::Create requires valid VMA allocator");
MOBILEGL_ASSERT(size > 0, "VkBufferObject::Create requires non-zero size");
Destroy();
m_allocator = allocator;
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = memoryUsage;
allocationInfo.flags = allocationFlags;
allocationInfo.requiredFlags = requiredFlags;
const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
m_allocator = nullptr;
m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr;
m_size = 0;
return false;
}
m_size = size;
return true;
}
void VkBufferObject::Destroy() {
Unmap();
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
}
m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr;
m_allocator = nullptr;
m_size = 0;
}
void* VkBufferObject::Map() {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Map called on invalid buffer");
if (m_mappedData != nullptr) {
return m_mappedData;
}
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr;
return nullptr;
}
return m_mappedData;
}
void VkBufferObject::Unmap() {
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
}
vmaUnmapMemory(m_allocator, m_allocation);
m_mappedData = nullptr;
}
Bool VkBufferObject::Upload(const void* data, VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Upload called on invalid buffer");
MOBILEGL_ASSERT(data != nullptr || size == 0, "VkBufferObject::Upload data pointer is null");
MOBILEGL_ASSERT(offset + size <= m_size, "VkBufferObject::Upload out of range");
if (size == 0) {
return true;
}
const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
return false;
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) {
Unmap();
}
return false;
}
if (!wasMapped) {
Unmap();
}
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;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,76 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.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 "BufferSlice.h"
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
struct VkBufferObjectDesc {
VmaAllocator allocator = nullptr;
VkDeviceSize size = 0;
VkBufferUsageFlags usage = 0;
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags allocationFlags = 0;
// Memory property bits the allocation MUST satisfy (e.g. HOST_VISIBLE|HOST_COHERENT
// for a persistently-mapped buffer the app writes into without explicit flushes).
VkMemoryPropertyFlags requiredFlags = 0;
};
class VkBufferObject {
public:
VkBufferObject() = default;
~VkBufferObject();
VkBufferObject(const VkBufferObject&) = delete;
VkBufferObject& operator=(const VkBufferObject&) = delete;
VkBufferObject(VkBufferObject&& other) noexcept;
VkBufferObject& operator=(VkBufferObject&& other) noexcept;
Bool Create(const VkBufferObjectDesc& desc);
Bool Create(VmaAllocator allocator, VkDeviceSize size, VkBufferUsageFlags usage,
VmaMemoryUsage memoryUsage, VmaAllocationCreateFlags allocationFlags = 0,
VkMemoryPropertyFlags requiredFlags = 0);
void Destroy();
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; }
// Inline: runs on the per-draw acquire path (a resident buffer bind is a
// GetSlice per binding), where an out-of-line call was measurable.
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
BufferSlice slice{};
slice.buffer = m_buffer;
slice.offset = offset;
slice.size = resolvedSize;
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
return slice;
}
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
private:
VmaAllocator m_allocator = nullptr;
VkBuffer m_buffer = VK_NULL_HANDLE;
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,483 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.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 "VkClearManager.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX &&
target <= TextureUploadTarget::CubeMapNegativeZ;
}
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha) {
VkClearColorValue clearValue{};
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
clearValue.int32[0] = payload.colorInt.x();
clearValue.int32[1] = payload.colorInt.y();
clearValue.int32[2] = payload.colorInt.z();
clearValue.int32[3] = formatLacksAlpha ? 1 : payload.colorInt.w();
break;
case ClearColorEncoding::Uint:
clearValue.uint32[0] = payload.colorUint.x();
clearValue.uint32[1] = payload.colorUint.y();
clearValue.uint32[2] = payload.colorUint.z();
clearValue.uint32[3] = formatLacksAlpha ? 1u : payload.colorUint.w();
break;
case ClearColorEncoding::Float:
clearValue.float32[0] = payload.color.x();
clearValue.float32[1] = payload.color.y();
clearValue.float32[2] = payload.color.z();
clearValue.float32[3] = formatLacksAlpha ? 1.0f : payload.color.w();
break;
}
return clearValue;
}
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat) {
if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo.
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
// linearly in an sRGB format and must pass through untouched.
const auto toLinear = [](Float encoded) {
const Float value = std::clamp(encoded, 0.0f, 1.0f);
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
};
payload.color = FloatVec4(toLinear(payload.color.x()), toLinear(payload.color.y()),
toLinear(payload.color.z()), payload.color.w());
}
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
break;
case ClearColorEncoding::Uint:
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
break;
case ClearColorEncoding::Float:
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
break;
}
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) {
if (!IsCubeMapFaceUploadTarget(target)) {
return 0;
}
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
static Uint32 ResolveAttachmentBaseArrayLayer(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return 0;
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
}
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
static Uint32 ResolveAttachmentLayerCount(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
}
return 1u;
}
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
if (attachmentType == FramebufferAttachmentType::None) {
return nullptr;
}
const auto& attachment = drawFbo.GetAttachment(attachmentType);
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
return nullptr;
}
return &attachment;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) {
return PendingClearKey {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
};
}
PendingClearKey VkClearManager::MakePendingClearKey(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
MOBILEGL_ASSERT(attachment.IsTexture() && !attachment.IsRenderbuffer(),
"MakePendingClearKey requires a texture framebuffer attachment");
auto* texture = attachment.GetTexture().get();
MOBILEGL_ASSERT(texture != nullptr, "MakePendingClearKey: texture attachment resolved to null");
const Uint32 mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
const Uint32 layerCount = ResolveAttachmentLayerCount(attachment);
return MakePendingClearKey(texture, mipLevel, baseArrayLayer, layerCount);
}
Bool VkClearManager::Initialize() {
return true;
}
void VkClearManager::Shutdown() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
return TextureIdentity {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
};
}
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask;
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
dst.color = src.color;
}
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth;
}
if ((src.mask & GL_STENCIL_BUFFER_BIT) != 0) {
dst.stencil = src.stencil;
}
}
void VkClearManager::ErasePendingClearsForTextureLocked(const TextureIdentity& identity) {
Vector<PendingClearKey> keysToErase;
keysToErase.reserve(m_pendingClears.size());
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (PendingClearMatchesTextureIdentity(it->first, identity)) {
keysToErase.emplace_back(it->first);
}
}
for (const auto& key : keysToErase) {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
if (identity.texture == nullptr) {
return false;
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt == m_aliveObjects.end()) {
ErasePendingClearsForTextureLocked(identity);
return false;
}
outTexture = aliveIt->second.lock();
if (!outTexture || outTexture.get() != identity.texture || outTexture->GetLifetimeId() != identity.lifetimeId) {
ErasePendingClearsForTextureLocked(identity);
outTexture.reset();
return false;
}
return true;
}
Bool VkClearManager::LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
return LockTextureIdentityLocked(TextureIdentity{
.texture = key.texture,
.lifetimeId = key.textureLifetimeId,
}, outTexture);
}
void VkClearManager::QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) {
if (mask & GL_COLOR_BUFFER_BIT) {
auto& drawbufs = drawFbo.GetDrawBuffers();
// This should automatically work on default & offscreen FBO
for (auto drawbuf: drawbufs) {
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
if (!attachment) {
continue;
}
QueueClear({
.mask = GL_COLOR_BUFFER_BIT,
.color = clearPayload.color
}, *attachment);
MGLOG_D("%s: %s (texture %d) - color = (%.2f, %.2f, %.2f, %.2f)", __func__,
MG_Util::ConvertFramebufferAttachmentTypeToString(drawbuf).c_str(),
attachment->GetTexture()->GetExternalIndex(),
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
}
}
if (mask & GL_DEPTH_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Depth);
if (attachment) {
QueueClear({
.mask = GL_DEPTH_BUFFER_BIT,
.depth = clearPayload.depth,
}, *attachment);
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.depth);
}
}
if (mask & GL_STENCIL_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Stencil);
if (attachment) {
QueueClear({
.mask = GL_STENCIL_BUFFER_BIT,
.stencil = clearPayload.stencil,
}, *attachment);
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.stencil);
}
}
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (clearPayload.mask == 0 || !texture) {
return;
}
const PendingClearKey key = MakePendingClearKey(texture.get());
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (clearPayload.mask == 0 || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
return;
}
const auto texture = attachment.GetTexture();
if (!texture) {
return;
}
const PendingClearKey key = MakePendingClearKey(attachment);
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(it->first, liveTexture);
}
}
return false;
}
Bool VkClearManager::HasPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
return false;
}
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(key, liveTexture);
}
Bool VkClearManager::HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return false;
}
return HasPendingClear(MakePendingClearKey(attachment));
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return GetPendingClear(key, outPayload, liveTexture);
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
return false;
}
auto it = m_pendingClears.find(key);
if (it == m_pendingClears.end()) {
outTexture.reset();
return false;
}
outPayload = it->second;
MGLOG_D("%s: Got pending clear for texture@%p lifetime=%llu, mip=%u layer=%u count=%u mask=0x%x clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
static_cast<void*>(key.texture),
static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount,
static_cast<Uint32>(outPayload.mask),
outPayload.color[0], outPayload.color[1], outPayload.color[2], outPayload.color[3],
outPayload.depth,
outPayload.stencil);
return true;
}
Bool VkClearManager::GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
MGLOG_D("%s: Failed getting pending clear for non-texture framebuffer attachment", __func__);
return false;
}
return GetPendingClear(MakePendingClearKey(attachment), outPayload);
}
Bool VkClearManager::GetPendingClears(MG_State::GLState::ITextureObject* texture,
Vector<PendingClearEntry>& outEntries) {
outEntries.clear();
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (!LockTextureIdentityLocked(MakeTextureIdentity(texture), liveTexture)) {
return false;
}
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
outEntries.emplace_back(PendingClearEntry{.key = it->first, .payload = it->second});
}
}
return !outEntries.empty();
}
void VkClearManager::PopPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
ErasePendingClearsForTextureLocked(identity);
}
void VkClearManager::PopPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return;
}
{
const std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
MGLOG_D("%s: Pop pending clear for texture@%p lifetime=%llu mip=%u layer=%u count=%u", __func__,
static_cast<void*>(key.texture), static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount);
}
void VkClearManager::PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return;
}
PopPendingClear(MakePendingClearKey(attachment));
}
SizeT VkClearManager::CollectGarbage() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
Vector<TextureIdentity> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->second.expired()) {
expiredTextures.emplace_back(it->first);
}
}
if (expiredTextures.empty()) {
return 0;
}
for (const auto& identity : expiredTextures) {
ErasePendingClearsForTextureLocked(identity);
}
return expiredTextures.size();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,168 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.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 "../VkIncludes.h"
#include "../VulkanRendererConfig.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <atomic>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
struct ClearFramebufferPayload {
FloatVec4 color;
Float depth{};
Uint32 stencil{};
};
// A colour clear reaches us from one of glClear/ClearBufferfv, ClearBufferiv or
// ClearBufferuiv, and Vulkan reads VkClearColorValue's union according to the destination
// image's format rather than converting between the members - a float written where an
// integer format is expected is reinterpreted bit for bit, not rounded. Remember which entry
// point supplied the value so the member written when the clear is materialized matches.
enum class ClearColorEncoding : Uint8 { Float, Int, Uint };
struct ClearAttachmentPayload {
GLbitfield mask = 0;
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
ClearColorEncoding colorEncoding = ClearColorEncoding::Float;
IntVec4 colorInt = IntVec4(0, 0, 0, 0);
UintVec4 colorUint = UintVec4(0u, 0u, 0u, 0u);
Float depth = 1.0f;
Uint32 stencil = 0;
};
// Builds the clear value for `payload` in the union member its encoding calls for.
// `formatLacksAlpha` applies GL's rule that a format without an alpha channel reads as one,
// expressed in whichever type matches (GL 4.6 core 15.2.3).
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
// Applies that same rule in place, for the paths that have to bake it into the payload before
// the destination is known.
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
// function to whatever value it is handed. Every other write path in this backend goes through
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
// therefore stores the raw value GL asked for. Rewrites `payload` to the linear colour whose
// encoding is that raw value, so a direct image clear of an sRGB destination agrees with them.
// A no-op for every other format, for integer clear encodings, and when GL is doing the
// encoding itself.
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
struct PendingClearKey {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 textureLifetimeId = 0;
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
Bool operator==(const PendingClearKey& other) const {
return texture == other.texture && textureLifetimeId == other.textureLifetimeId &&
mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount;
}
};
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct PendingClearEntry {
PendingClearKey key{};
ClearAttachmentPayload payload{};
};
struct PendingClearKeyHash {
SizeT operator()(const PendingClearKey& key) const {
const SizeT textureHash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
const SizeT textureLifetimeHash = std::hash<Uint64>{}(key.textureLifetimeId);
const SizeT mipHash = std::hash<Uint32>{}(key.mipLevel);
const SizeT layerHash = std::hash<Uint32>{}(key.baseArrayLayer);
const SizeT layerCountHash = std::hash<Uint32>{}(key.layerCount);
SizeT hash = textureHash;
hash ^= textureLifetimeHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= mipHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerCountHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize();
void Shutdown();
void QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo);
void QueueClear(
const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture);
void QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool HasPendingClear(MG_State::GLState::ITextureObject* texture);
Bool HasPendingClear(const PendingClearKey& key);
Bool HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload);
Bool GetPendingClears(MG_State::GLState::ITextureObject* texture, Vector<PendingClearEntry>& outEntries);
void PopPendingClear(MG_State::GLState::ITextureObject* texture);
void PopPendingClear(const PendingClearKey& key);
void PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
SizeT CollectGarbage();
private:
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
static void MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src);
void ErasePendingClearsForTextureLocked(const TextureIdentity& identity);
Bool LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
// forces the full SetupDraw path (which materializes/consumes it).
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
private:
mutable std::mutex m_mutex;
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
// read it before taking the lock: during draw batches the pending set
// is almost always empty, so this turns several locked map probes per
// draw into one relaxed load.
std::atomic<Uint32> m_pendingCount{0};
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -1,362 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.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 "SwapchainObject.h"
#include "VkClearManager.h"
#include "VkTextureManager.h"
#include "../VkIncludes.h"
#include "../VulkanRendererConfig.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include <Includes.h>
#include <unordered_map>
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class TrackedAttachmentTarget : Uint8 {
Texture,
Renderbuffer,
SwapchainColor,
SwapchainDepthStencil
};
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;
ClearAttachmentPayload inlinePayload{};
};
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> texture;
// Identity-compare shortcut for the per-draw "does the active pass use
// this sampled texture" probe: comparing this against a LIVE texture's
// address needs no weak_ptr::lock (two refcount atomics per probe).
// May dangle once the texture dies - compare only, never dereference.
MG_State::GLState::ITextureObject* textureRaw = nullptr;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
};
struct DepthStencilAttachmentLoadInfo {
VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
};
DepthStencilAttachmentLoadInfo ResolveDepthStencilAttachmentLoadInfo(
VkImageLayout trackedLayout, Bool clearDepth, Bool clearStencil);
IntVec2 ResolveRenderPassFramebufferExtent(Bool isDefaultFbo, const TextureSize& attachmentExtent,
VkExtent2D swapchainExtent);
struct RenderPassEntry {
static inline VkDevice s_device;
static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
Uint64 hash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
Uint64 compatibilityHash = 0;
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
Uint32 attachmentCount = 0;
Uint32 colorAttachmentCount = 0;
Bool hasDepthStencilAttachment = false;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
IntVec2 extent = {0, 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;
RenderPassEntry(RenderPassEntry&& that) noexcept {
std::swap(hash, that.hash);
std::swap(renderPass, that.renderPass);
std::swap(framebuffer, that.framebuffer);
std::swap(compatibilityHash, that.compatibilityHash);
std::swap(pendingClearAttachments, that.pendingClearAttachments);
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
std::swap(attachmentCount, that.attachmentCount);
std::swap(colorAttachmentCount, that.colorAttachmentCount);
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
std::swap(sampleCount, that.sampleCount);
std::swap(extent, that.extent);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
}
RenderPassEntry(
Uint64 hash,
VkRenderPass renderpass,
VkFramebuffer framebuffer,
Uint64 compatibilityHash,
const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
Uint32 attachmentCount,
Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount,
IntVec2 extent, Uint32 layers):
hash(hash),
renderPass(renderpass),
framebuffer(framebuffer),
compatibilityHash(compatibilityHash),
pendingClearAttachments(Move(pendingClearAttachments)),
trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
attachmentCount(attachmentCount),
colorAttachmentCount(colorAttachmentCount),
hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount),
extent(extent),
layers(layers)
{}
~RenderPassEntry() {
if (renderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(s_device, renderPass, nullptr);
}
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(s_device, framebuffer, nullptr);
}
}
Bool CompatibleWith(const RenderPassEntry& that) const {
return this->compatibilityHash == that.compatibilityHash;
}
Bool CompatibleWith(Uint64 compatibilityHash) const {
return this->compatibilityHash == compatibilityHash;
}
};
struct ActiveRenderPassInfo {
Uint64 hash = 0;
Uint64 compatibilityHash = 0;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
IntVec2 extent = {0, 0};
Bool CompatibleWith(const RenderPassEntry& that) const {
return compatibilityHash == that.compatibilityHash;
}
Bool CompatibleWith(Uint64 thatCompatibilityHash) const {
return compatibilityHash == thatCompatibilityHash;
}
};
class VkRenderPassManager {
public:
using HashType = Uint64;
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
// value: pipelines are hashed on the raw handle, and once destroyed the value
// may be recycled for an incompatible pass, so dependent caches must purge
// everything keyed on them before any new pass can be created (the sweep and
// the notification run back-to-back with no creation in between; observers
// compare the values, never dereference them). Batched so a mass-idle cohort
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
// scan, not one per dying pass. The wholesale paths
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
// every pipeline outright.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
};
VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
Bool Initialize();
void Shutdown();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
// Frame boundary hook: ages the render-pass cache and evicts long-unused
// entries (their command buffers retired many frames ago).
void OnPresent();
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
static ActiveRenderPassInfo* GetActiveRenderPass();
private:
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
const VulkanRendererConfig& m_config;
VkClearManager& m_clearManager;
VkTextureManager& m_textureManager;
SwapchainObject& m_swapchainObject;
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
// manager's image epoch this invalidates the render-pass fast path on any attachment
// image recreation.
Uint64 m_renderbufferImageEpoch = 1;
public:
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
// snapshots include it so an attachment respecify forces a re-resolve.
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
// version change, swapchain rotation, any attachment image recreation (the two epochs),
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
// deadSinceFrame sentinel: the owning weak reference has not been observed
// expired. Dead resources age past every in-flight frame before Destroy
// (see CollectRenderbufferGarbage); the GPU may still reference the image
// for frames-in-flight frames after the GL object dies.
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
// UNORM reinterpretation of an sRGB image, used as the attachment view while
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
VkImageView unormTwinView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkExtent2D extent = {0, 0};
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
// m_frameCounter value at which the weak reference was first seen expired.
Uint64 deadSinceFrame = kNeverObservedDead;
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
// until enough frame boundaries have passed that no in-flight command buffer
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
struct DeferredRenderbufferRelease {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
// relocation the cached pointer names freed storage still holding the pre-clear
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
// instead of the clear colour on exactly the iterations that grew the table.
//
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
// ordering rule. m_textureResources is node-based for the same reason. This buys stability
// across rehash and insert only - erase still invalidates the erased element, which is safe
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
// Frame-boundary margin after which a resource last referenced by a retired
// GL object (or superseded backing) is provably past every in-flight frame.
Uint64 RetireAgeFrames() const;
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
void CollectDeferredRenderbufferReleases(Bool destroyAll);
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{};
static inline Bool s_hasActiveRenderPass = false;
static inline VkClearManager* s_clearManager = nullptr;
static inline VkTextureManager* s_textureManager = nullptr;
static inline SwapchainObject* s_swapchainObject = nullptr;
static inline VkRenderPassManager* s_renderPassManager = nullptr;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,319 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.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 "VkSamplerManager.h"
#include "MG_State/GLState/Core.h"
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
}
Bool IsDepthTextureFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::DepthComponent:
case TextureInternalFormat::DepthComponent16:
case TextureInternalFormat::DepthComponent24:
case TextureInternalFormat::DepthComponent32:
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
return true;
default:
return false;
}
}
Bool NearlyEqual(Float lhs, Float rhs) {
return std::fabs(lhs - rhs) <= 1e-6f;
}
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
return 0.0f;
}
return sampler.GetMaxLod();
}
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
} // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
Shutdown();
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) {
vkDestroySampler(m_device, sampler.handle, nullptr);
}
sampler.handle = VK_NULL_HANDLE;
}
m_samplers.clear();
m_device = VK_NULL_HANDLE;
m_config = nullptr;
m_frameBoundaryCounter = 0;
}
void VkSamplerManager::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; destroy samplers whose last use is far past every
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
// double-free the handle; an evicted key that recurs simply re-creates
// its sampler on the next miss.
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
auto& entry = it->second;
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
it = m_samplers.erase(it);
} else {
++it;
}
}
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
const auto mipmapMode = sampler.GetMipmapMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
const auto wrapS = sampler.GetWrapS();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
const auto wrapT = sampler.GetWrapT();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
const auto lodBias = sampler.GetLodBias();
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture);
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
return XXH64_digest(m_hashState);
}
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
// allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
return it->second.handle;
}
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias();
// 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(sampler.GetSamplerCompareFunc());
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE;
VkSampler vkSampler = VK_NULL_HANDLE;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
SamplerCacheEntry entry{};
entry.handle = vkSampler;
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
m_samplers[key] = entry;
return vkSampler;
}
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
}
VkSamplerMipmapMode VkSamplerManager::ToVkMipmapMode(SamplerMipmapMode mode) {
switch (mode) {
case SamplerMipmapMode::Nearest:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
case SamplerMipmapMode::Linear:
return VK_SAMPLER_MIPMAP_MODE_LINEAR;
case SamplerMipmapMode::None:
default:
return VK_SAMPLER_MIPMAP_MODE_NEAREST;
}
}
VkSamplerAddressMode VkSamplerManager::ToVkAddressMode(SamplerWrapMode mode) {
switch (mode) {
case SamplerWrapMode::ClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case SamplerWrapMode::MirroredRepeat:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case SamplerWrapMode::Repeat:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case SamplerWrapMode::ClampToBorder:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
case SamplerWrapMode::MirrorClampToEdge:
return VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE;
default:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
}
VkCompareOp VkSamplerManager::ToVkCompareOp(SamplerCompareFunc func) {
switch (func) {
case SamplerCompareFunc::Never:
return VK_COMPARE_OP_NEVER;
case SamplerCompareFunc::Less:
return VK_COMPARE_OP_LESS;
case SamplerCompareFunc::Equal:
return VK_COMPARE_OP_EQUAL;
case SamplerCompareFunc::LessEqual:
return VK_COMPARE_OP_LESS_OR_EQUAL;
case SamplerCompareFunc::Greater:
return VK_COMPARE_OP_GREATER;
case SamplerCompareFunc::NotEqual:
return VK_COMPARE_OP_NOT_EQUAL;
case SamplerCompareFunc::GreaterEqual:
return VK_COMPARE_OP_GREATER_OR_EQUAL;
case SamplerCompareFunc::Always:
default:
return VK_COMPARE_OP_ALWAYS;
}
}
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) {
if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
if (isDepthTexture) {
if (NearlyEqual(borderColor.x(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
}
if (NearlyEqual(borderColor.x(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
}
}
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
NearlyEqual(borderColor.z(), 0.0f);
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
}
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
}
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,90 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.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 "../VkIncludes.h"
#include "../VulkanRendererConfig.h"
#include <Includes.h>
#include <MG_State/GLState/SamplerState/SamplerObject.h>
namespace MobileGL::MG_State::GLState {
class SamplerObject;
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
class VkSamplerManager {
public:
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
const VulkanRendererConfig* config = nullptr;
// The samplerAnisotropy device feature was requested and granted at vkCreateDevice.
Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
// stream of never-destroyed VkSamplers and eventually exhaust the device's
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
// boundaries cannot be referenced by any in-flight command buffer (frames in
// flight are single digits), and every descriptor set the GPU consumes is
// written that same frame with live handles (the per-binding resolve memo and
// descriptor-set reuse are both frame-reset), so destruction here needs no
// fence wait. Self-gated: one counter bump and compare except on sweep
// boundaries.
void OnFrameBoundary();
private:
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
Uint externalIndex = 0;
Uint16 version = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture);
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
Bool forceNearestFiltering) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -1,580 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.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 "../VkIncludes.h"
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
}
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
class VkTextureManager {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
// Bumped whenever any tracked texture resource is erased; cached
// TextureResource pointers are valid only while this is unchanged.
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VmaAllocator allocator = nullptr;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0;
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
// Union of shader stages sampled-read barriers may name on this device; the renderer
// builds it from the enabled features because geometry/tessellation stage bits are
// invalid in a barrier when their feature is off.
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Family of `graphicsQueue`; the manager creates its own command pool
// on it for the recycled upload-batch command buffers, so their parked
// allocations never sit in (and fragment) the renderer's shared pool
// that frame command buffers churn through every frame.
Uint32 graphicsQueueFamilyIndex = 0;
};
struct TextureResource {
struct AttachmentViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
// May differ from the image format: sRGB images attach through their UNORM
// twin while GL_FRAMEBUFFER_SRGB is disabled.
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
viewFormat == other.viewFormat;
}
};
struct AttachmentViewKeyHash {
SizeT operator()(const AttachmentViewKey& 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.viewFormat)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const StorageImageViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKeyHash {
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKeyHash {
SizeT operator()(const StorageImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView fullView = VK_NULL_HANDLE;
VkImageView sampledView = VK_NULL_HANDLE;
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;
Uint32 arrayLayers = 1;
Uint32 mipLevels = 1;
Uint32 sampledBaseMipLevel = 0;
Uint32 sampledLevelCount = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
// Usage the live image was created with. STORAGE is only requested for textures that
// have actually been bound to a GL image unit, because on Adreno a storage-capable
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
// and recreates the image, so the resolved usage has to be part of the compatibility
// check that decides whether the existing image can be kept.
VkImageUsageFlags usageFlags = 0;
// True once this image was (re)resolved while the texture was already marked as an
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
Bool storageUsageResolved = false;
Uint16 syncedTextureParamsVersion = 0;
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
// command referencing this image that was recorded into the CURRENT frame
// command buffer. An image untouched by the open recording may have its
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
// Snapshot of the defined mip-level count at the last sync. Folded into the early-out key
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
// busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0;
TextureResource() = default;
TextureResource(const TextureResource&) = delete;
TextureResource(TextureResource&& that) noexcept {
std::swap(this->image, that.image);
std::swap(this->allocation, that.allocation);
std::swap(this->fullView, that.fullView);
std::swap(this->sampledView, that.sampledView);
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);
std::swap(this->arrayLayers, that.arrayLayers);
std::swap(this->mipLevels, that.mipLevels);
std::swap(this->sampledBaseMipLevel, that.sampledBaseMipLevel);
std::swap(this->sampledLevelCount, that.sampledLevelCount);
std::swap(this->format, that.format);
std::swap(this->aspect, that.aspect);
std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->usageFlags, that.usageFlags);
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
}
void Reset() {
if (fullView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, fullView, nullptr);
}
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
for (const auto attachmentView : perMipViews) {
if (attachmentView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, attachmentView, nullptr);
}
}
for (const auto sampledView : perMipSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
}
for (const auto& [_, attachmentView] : attachmentViews) {
if (attachmentView != VK_NULL_HANDLE) {
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);
}
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
perMipSampledViews.clear();
attachmentViews.clear();
alternateSampledViews.clear();
storageImageViews.clear();
image = VK_NULL_HANDLE;
allocation = nullptr;
layout = VK_IMAGE_LAYOUT_UNDEFINED;
extent = {0, 0};
depth = 1;
arrayLayers = 1;
mipLevels = 1;
sampledBaseMipLevel = 0;
sampledLevelCount = 1;
format = VK_FORMAT_UNDEFINED;
aspect = VK_IMAGE_ASPECT_NONE;
viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
usageFlags = 0;
storageUsageResolved = false;
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
}
~TextureResource() {
Reset();
}
static inline VkDevice s_device = VK_NULL_HANDLE;
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
};
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// Submits the accumulated texture-upload batch (one command buffer, one
// vkQueueSubmit, one pooled fence) if any uploads are pending. MUST run
// before any other vkQueueSubmit on the shared graphics queue whose
// commands may consume an image the batch writes - the frame command
// buffer submit (mid-frame flush, readback, Present) and the
// preserve-on-recreate copy are the existing callers. No-op when the
// batch is empty.
void FlushPendingUploads();
// Drains every frame slot's deferred image/view releases. Only valid when
// the caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
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,
Uint32 writtenMipLevel,
VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
// any command in the open recording, so its out-of-pass work may safely
// execute ahead of the whole recording (in the pre command buffer).
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
// GL lets an image binding come and go, and re-creating the image every time it does
// would cost far more than the compression it wins back.
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
// True when this texture is marked but its live image predates the mark, i.e. the next sync
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// The same ordering question for the other recreate-and-preserve trigger: true when this
// texture's live image carries a shorter mip chain than a full one, so defining the missing
// levels recreates it and copies the old contents forward.
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
Uint32 layerCount = 1);
SizeT CollectGarbage();
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
// SyncTexture, so after the first successful sync of a texture in a draw the
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
void BeginDrawSyncScope();
void EndDrawSyncScope();
// RAII guard that opens/closes a per-draw sync memo window (see above).
class DrawSyncScope {
public:
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
DrawSyncScope(const DrawSyncScope&) = delete;
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
private:
VkTextureManager& m_manager;
};
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
// See AdvanceRecordingGeneration. Starts above every resource's default
// stamp of 0 so a fresh resource counts as untouched.
Uint64 m_recordingGeneration = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
Bool SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
TextureUploadTarget uploadTarget,
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource);
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer,
Uint32 layerCount,
const VkComponentMapping* components = nullptr,
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget,
TextureResource &outResource);
static Bool CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget& outTarget,
IntVec3& outTexelSize,
SizeT& outByteSize,
Uint32& outMipLevelCount);
static Uint32 GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget target);
static void ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
Uint32& outBaseMipLevel, Uint32& outLevelCount);
static VkImageAspectFlags GetAspectMaskForFormat(VkFormat format);
void DeferResourceRelease(TextureResource&& resource);
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
// Frees the fence/command buffer/staging buffer of every in-flight texture
// upload whose fence has signaled (submission order = completion order on
// the single queue, so the scan stops at the first still-pending entry).
// waitAll blocks on every entry - Shutdown's drain.
void ReclaimCompletedUploads(Bool waitAll = false);
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
// Dedicated pool for the recycled upload-batch command buffers (see
// InitInfo::graphicsQueueFamilyIndex).
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0;
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
Uint32 m_gcFrameCounter = 0;
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
// returns the resource without re-hashing the identity into m_textureResources.
struct DrawSyncedTexture {
TextureIdentity identity;
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
// exactly like the mutable-format verdict above, so it is answered at image creation and
// remembered rather than probed once globally.
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
// --- Batched upload machinery ---
// Uploads within a frame are recorded into ONE shared command buffer and
// submitted with ONE vkQueueSubmit at FlushPendingUploads (the renderer
// flushes before every frame-command-buffer submit). Staging memory comes
// from a pool of persistently-mapped, reusable blocks instead of a
// vmaCreateBuffer per upload.
struct UploadStagingBlock {
VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
Uint8* mapped = nullptr; // persistently mapped for the block's lifetime
VkDeviceSize capacity = 0;
VkDeviceSize cursor = 0; // bump cursor while the block backs the open batch
};
// Opens the batch command buffer lazily (allocates/reuses + begins recording).
VkCommandBuffer EnsureUploadBatchOpen();
// Bump-allocates `size` staging bytes for the open batch, growing onto a
// new/pooled block when the current one cannot fit. Returns the write
// pointer; outBuffer/outBaseOffset locate the space for copy commands.
Uint8* AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer, VkDeviceSize& outBaseOffset);
void RecycleUploadStagingBlock(UploadStagingBlock&& block);
// Drops a recorded-but-unsubmitted batch on the floor. Shutdown only: the
// device is being torn down, so the lost texel data is unobservable.
void DiscardPendingUploadBatch();
void DestroyUploadPools();
Vector<UploadStagingBlock> m_freeUploadStagingBlocks;
VkDeviceSize m_freeUploadStagingBytes = 0;
Vector<VkCommandBuffer> m_freeUploadCommandBuffers;
Vector<VkFence> m_freeUploadFences;
Bool m_uploadBatchOpen = false;
VkCommandBuffer m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
// Blocks whose staging bytes the open batch's copies reference (last =
// the block the bump cursor is currently allocating from).
Vector<UploadStagingBlock> m_uploadBatchBlocks;
// Images the open batch writes; consulted for the rare re-upload-after-
// draw flush and by DeferResourceRelease (an unsubmitted command buffer
// referencing a deferred-released image would escape every fence-based
// destruction proof, so the batch is flushed before the image is parked).
Vector<VkImage> m_uploadBatchImages;
VkDeviceSize m_uploadBatchStagingBytes = 0;
// Texture uploads are submitted out-of-band but NOT waited on (waiting
// behind the queue serialized the CPU against the previous frame's GPU
// work every time an animated atlas re-uploaded). Each flushed batch's
// transients are parked here and RECYCLED (fence reset to the fence pool,
// command buffer reset to the CB pool, staging blocks back to the block
// pool) once the batch fence signals.
struct PendingUploadReclaim {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Vector<UploadStagingBlock> stagingBlocks;
};
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,179 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "VkTimerQueryManager.h"
namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTimerQueryManager::Initialize(const InitInfo& initInfo) {
Shutdown();
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false;
}
m_device = initInfo.device;
m_timestampPeriodNs = initInfo.timestampPeriodNs;
m_validBitsMask = initInfo.timestampValidBits >= 64
? ~0ull
: ((1ull << initInfo.timestampValidBits) - 1ull);
m_slotsPerPool = initInfo.slotsPerPool;
m_pools.resize(initInfo.frameCount);
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
poolInfo.queryCount = m_slotsPerPool;
for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown();
return false;
}
}
return true;
}
void VkTimerQueryManager::Shutdown() {
if (m_device != VK_NULL_HANDLE) {
for (auto& poolState : m_pools) {
if (poolState.pool != VK_NULL_HANDLE) {
vkDestroyQueryPool(m_device, poolState.pool, nullptr);
}
}
}
// Records the frontend still holds simply stay unharvested; their
// results read back as 0.
m_pools.clear();
m_device = VK_NULL_HANDLE;
m_timestampPeriodNs = 0.0f;
m_validBitsMask = 0;
m_slotsPerPool = 0;
}
void VkTimerQueryManager::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.preparedFrameSerial == frameSerial) {
// Recording re-began within the same frame (mid-frame readback
// submit or the Present layout transition); the pool was already
// harvested and reset for this cycle, and resetting again would
// clobber timestamps written earlier in the frame.
return;
}
// Harvest what the pool's previous cycle left behind. The frame slot's
// fence was waited before re-recording, so every executed query is
// already available and the reads return immediately.
DrainPoolPending(poolState);
vkCmdResetQueryPool(commandBuffer, poolState.pool, 0, m_slotsPerPool);
poolState.cursor = 0;
poolState.exhaustionWarned = false;
poolState.preparedFrameSerial = frameSerial;
}
SharedPtr<VkTimerQueryManager::TimestampRecord> VkTimerQueryManager::WriteTimestamp(VkCommandBuffer commandBuffer,
Uint32 frameIndex,
Uint64 frameSerial) {
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path",
frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true;
}
return nullptr;
}
auto record = MakeShared<TimestampRecord>();
record->poolIndex = frameIndex;
record->slot = poolState.cursor++;
record->frameSerial = frameSerial;
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, poolState.pool, record->slot);
poolState.pendingRecords.push_back(record);
return record;
}
Bool VkTimerQueryManager::TryHarvest(TimestampRecord& record) {
if (record.harvested) {
return true;
}
if (m_device == VK_NULL_HANDLE || record.poolIndex >= m_pools.size()) {
return false;
}
Uint64 resultWithAvailability[2] = {0, 0};
const VkResult result = vkGetQueryPoolResults(
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false;
}
if (resultWithAvailability[1] == 0) {
return false;
}
record.rawTicks = resultWithAvailability[0];
record.harvested = true;
return true;
}
void VkTimerQueryManager::InvalidatePendingRecords() {
for (auto& poolState : m_pools) {
DrainPoolPending(poolState);
// Force a harvest-free reset cycle the next time this pool's frame
// begins recording.
poolState.preparedFrameSerial = 0;
}
}
void VkTimerQueryManager::DrainPoolPending(PoolState& poolState) {
for (auto& record : poolState.pendingRecords) {
if (record->harvested) {
continue;
}
if (!TryHarvest(*record)) {
// The commands carrying this timestamp never executed (they
// were dropped, e.g. by a swapchain recreation mid-frame).
// Mark the record resolved-as-invalid so waits on it cannot
// hang; its result reads back as 0.
record->harvested = true;
record->valid = false;
}
}
poolState.pendingRecords.clear();
}
Uint64 VkTimerQueryManager::MaskToValidBits(Uint64 ticks) const {
return ticks & m_validBitsMask;
}
Uint64 VkTimerQueryManager::ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const {
if (!begin.valid || !end.valid) {
return 0;
}
const Uint64 deltaTicks = MaskToValidBits(end.rawTicks - begin.rawTicks);
return static_cast<Uint64>(static_cast<double>(deltaTicks) * static_cast<double>(m_timestampPeriodNs));
}
Uint64 VkTimerQueryManager::TimestampNs(const TimestampRecord& record) const {
if (!record.valid) {
return 0;
}
return static_cast<Uint64>(static_cast<double>(MaskToValidBits(record.rawTicks)) *
static_cast<double>(m_timestampPeriodNs));
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,111 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// GPU timestamp storage backing the GL timer-query frontend (GL_TIME_ELAPSED
// spans and GL_TIMESTAMP one-shots): one VkQueryPool of timestamp slots per
// frame in flight.
//
// Per-frame lifecycle: right after a frame slot's command buffer begins
// recording (and before any render pass, since vkCmdResetQueryPool must be
// recorded outside one), OnFrameCommandRecordingBegan harvests every
// not-yet-read slot of the pool about to be reused (the slot's frame fence
// was waited before re-recording, so the results are already available),
// records a reset of the whole pool, and rewinds the allocation cursor.
class VkTimerQueryManager {
public:
// One vkCmdWriteTimestamp landing spot. Shared (via SharedPtr) between
// the frontend-held query object and the owning pool's pending list, so
// deleting a query while its result is still in flight never leaves the
// pool with a dangling record.
struct TimestampRecord {
Uint32 poolIndex = 0;
Uint32 slot = 0;
// VkBufferManager frame serial current when the timestamp was
// recorded; result availability is bounded by its completion.
Uint64 frameSerial = 0;
Bool harvested = false;
// Cleared when the recorded commands were dropped before they could
// execute (swapchain recreation abandons the in-progress command
// buffer); the result then reads back as 0.
Bool valid = true;
Uint64 rawTicks = 0;
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
Uint32 frameCount = 0;
Uint32 timestampValidBits = 0;
Float timestampPeriodNs = 0.0f; // nanoseconds per timestamp tick
Uint32 slotsPerPool = 128;
};
Bool Initialize(const InitInfo& initInfo);
// The caller guarantees the device is idle (same contract as the other
// DirectVulkan managers' Shutdown paths).
void Shutdown();
// The per-frame hook described in the class comment. Re-begins within
// the same frame serial (mid-frame readback submits, the Present layout
// transition) are skipped so already-written slots survive.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex, Uint64 frameSerial);
// Allocates a slot from the frame's pool and records a bottom-of-pipe
// vkCmdWriteTimestamp (valid both inside and outside a render pass).
// Returns null on pool exhaustion, with one warning per pool cycle; the
// frontend falls back gracefully on a null handle.
SharedPtr<TimestampRecord> WriteTimestamp(VkCommandBuffer commandBuffer, Uint32 frameIndex,
Uint64 frameSerial);
// Non-blocking single-slot read (WITH_AVAILABILITY, no WAIT). Returns
// true once the record holds its raw ticks. Callers gate this on the
// record's frame serial being complete.
Bool TryHarvest(TimestampRecord& record);
// Reads every pending result that is available (the caller guarantees
// the device is idle) and marks the rest invalid. Called when recorded
// but unsubmitted commands are dropped (swapchain recreation), which
// would otherwise leave slots that never become available. Each pool is
// reset lazily on its next OnFrameCommandRecordingBegan.
void InvalidatePendingRecords();
// end - begin using unsigned wrap arithmetic masked to the queue's
// timestampValidBits, converted to nanoseconds. 0 if either record was
// invalidated.
Uint64 ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const;
// Raw GPU timestamp converted to nanoseconds. 0 if invalidated.
Uint64 TimestampNs(const TimestampRecord& record) const;
private:
struct PoolState {
VkQueryPool pool = VK_NULL_HANDLE;
Uint32 cursor = 0;
// Frame serial the pool was last harvested + reset for; guards
// against double resets when recording re-begins mid-frame.
Uint64 preparedFrameSerial = 0;
Bool exhaustionWarned = false;
Vector<SharedPtr<TimestampRecord>> pendingRecords;
};
Uint64 MaskToValidBits(Uint64 ticks) const;
// Harvest (or invalidate, when the result never became available)
// every pending record of a pool and clear its pending list.
void DrainPoolPending(PoolState& pool);
VkDevice m_device = VK_NULL_HANDLE;
Float m_timestampPeriodNs = 0.0f;
Uint64 m_validBitsMask = 0;
Uint32 m_slotsPerPool = 0;
Vector<PoolState> m_pools;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,99 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/VkIncludes.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 "VulkanRendererConfig.h"
#define ENUM_STR_CASE(c) case c: return #c;
namespace MobileGL::MG_Backend::DirectVulkan {
inline const char* VkResultToString(VkResult result) {
switch (result) {
ENUM_STR_CASE(VK_SUCCESS)
ENUM_STR_CASE(VK_NOT_READY)
ENUM_STR_CASE(VK_TIMEOUT)
ENUM_STR_CASE(VK_EVENT_SET)
ENUM_STR_CASE(VK_EVENT_RESET)
ENUM_STR_CASE(VK_INCOMPLETE)
ENUM_STR_CASE(VK_ERROR_OUT_OF_HOST_MEMORY)
ENUM_STR_CASE(VK_ERROR_OUT_OF_DEVICE_MEMORY)
ENUM_STR_CASE(VK_ERROR_INITIALIZATION_FAILED)
ENUM_STR_CASE(VK_ERROR_DEVICE_LOST)
ENUM_STR_CASE(VK_ERROR_MEMORY_MAP_FAILED)
ENUM_STR_CASE(VK_ERROR_LAYER_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_EXTENSION_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_FEATURE_NOT_PRESENT)
ENUM_STR_CASE(VK_ERROR_INCOMPATIBLE_DRIVER)
ENUM_STR_CASE(VK_ERROR_TOO_MANY_OBJECTS)
ENUM_STR_CASE(VK_ERROR_FORMAT_NOT_SUPPORTED)
ENUM_STR_CASE(VK_ERROR_FRAGMENTED_POOL)
ENUM_STR_CASE(VK_ERROR_UNKNOWN)
ENUM_STR_CASE(VK_ERROR_OUT_OF_POOL_MEMORY)
ENUM_STR_CASE(VK_ERROR_INVALID_EXTERNAL_HANDLE)
ENUM_STR_CASE(VK_ERROR_FRAGMENTATION)
ENUM_STR_CASE(VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS)
ENUM_STR_CASE(VK_PIPELINE_COMPILE_REQUIRED)
ENUM_STR_CASE(VK_ERROR_SURFACE_LOST_KHR)
ENUM_STR_CASE(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR)
ENUM_STR_CASE(VK_SUBOPTIMAL_KHR)
ENUM_STR_CASE(VK_ERROR_OUT_OF_DATE_KHR)
ENUM_STR_CASE(VK_ERROR_INCOMPATIBLE_DISPLAY_KHR)
ENUM_STR_CASE(VK_ERROR_VALIDATION_FAILED_EXT)
ENUM_STR_CASE(VK_ERROR_INVALID_SHADER_NV)
default:
return "VK_RESULT_UNKNOWN";
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
namespace MobileGL::MG_Backend::DirectVulkan {
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
// attachment view (and render pass format) therefore drops to the UNORM twin
// whenever the capability is off. Sampled views keep the sRGB format (decode on
// sample is unconditional in GL).
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
if (framebufferSrgbEnabled) return format;
switch (format) {
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_UNORM;
default:
return format;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
if (_vk_verify_result != VK_SUCCESS) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} while (0)
#define XXHASH_VERIFY(expr, ...) \
do { \
XXH_errorcode _xxh_verify_result = (expr); \
if (_xxh_verify_result != XXH_OK) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
} \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
__LINE__); \
} while (0)
@@ -1,32 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererConfig.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 "Config.h"
namespace MobileGL::MG_Backend::DirectVulkan {
struct VulkanRendererConfig {
// Fallback CPU pipeline depth used when the MOBILEGL_MAGMA_FRAMESINFLIGHT env var is
// unset/invalid. A deeper pipeline lets the CPU run further ahead of the GPU, hiding
// per-frame GPU-completion latency. Whatever value is chosen (env or this fallback) is
// only a request: VulkanRenderer::Initialize clamps it down to the surface's maxImageCount
// (and never below 2), since not every driver allows that many swapchain images.
Uint32 MaxFramesInFlight = 3;
String AppName = "MobileGL-VulkanRenderer";
MobileGL::Version Version = MG_Config::CoreVersion;
Uint64 CacheVersion = MG_Config::CacheVersion;
Uint32 SurfaceWidth = 1;
Uint32 SurfaceHeight = 1;
Bool DisablePipelineCache = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
Bool EnableValidationLayers = true;
#else
Bool EnableValidationLayers = false;
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,119 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/BackendObject_DirectVulkanTMP.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 "BackendObject_DirectVulkanTMP.h"
#include "MG_Backend/BackendObject.h"
#include "DirectVulkanTMP.h"
#include "TmpImpl.h"
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkanTMP {
BackendObject_DirectVulkanTMP::~BackendObject_DirectVulkanTMP() = default;
void BackendObject_DirectVulkanTMP::InitWindowSurface() {
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
if (!DirectVulkanTMP::InitWindowSurface(nativeWindow)) {
MGLOG_E("Failed to initialize window surface for DirectVulkanTMP backend");
}
}
void BackendObject_DirectVulkanTMP::Initialize() {
m_initialized = true;
}
void BackendObject_DirectVulkanTMP::InitCapabilities() {
if (!m_initialized) {
MGLOG_E("Cannot initialize capabilities before backend is initialized");
return;
}
MG_Util::BackendLoader::QueryVulkanCapabilities(m_vulkanCaps,
DirectVulkanTMP::GetVulkanState().ctx->GetPhysicalDevice());
UpdateDynamicBackendParameters();
}
const RendererInfo& BackendObject_DirectVulkanTMP::GetRendererInfo() const {
static RendererInfo RendererInfo = {
.RendererName = "Magma-TMP", // Renderer Name
.BackendName = "Direct (Vulkan) TMP", // Backend Name
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
V_OpenGL33},
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
};
return RendererInfo;
}
String BackendObject_DirectVulkanTMP::GetBackendAPIVersionString() const {
if (!m_initialized) {
return "<uninitialized DirectVulkanTMP backend>";
}
// Format:
// <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version>
// TODO
String str = m_vulkanCaps.DeviceName + ", Vulkan " + m_vulkanCaps.VulkanAPIVersion.toString() + ", Driver " +
m_vulkanCaps.DriverVersionString;
return str;
}
BackendType BackendObject_DirectVulkanTMP::GetBackendType() const {
return BackendType::DirectVulkanTMP;
}
const GlobalBackendFunctionsTable& BackendObject_DirectVulkanTMP::GetBackendFunctions() const {
static GlobalBackendFunctionsTable funcsTable;
static Bool funcsTableInitialized = false;
if (!funcsTableInitialized) {
funcsTable.Present = DirectVulkanTMP::Present;
funcsTable.GL.DrawArrays = DrawArrays;
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
funcsTable.GL.DrawRangeElements = DrawRangeElements;
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
funcsTable.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
funcsTable.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
funcsTable.GL.DrawElementsInstanced = DrawElementsInstanced;
funcsTable.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
funcsTable.GL.DrawArraysInstanced = DrawArraysInstanced;
funcsTable.GL.DrawElementsIndirect = DrawElementsIndirect;
funcsTable.GL.DrawArraysIndirect = DrawArraysIndirect;
funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTableInitialized = true;
}
return funcsTable;
}
const DynamicBackendParameters& BackendObject_DirectVulkanTMP::GetDynamicParameters() const {
return m_dynamicParameters;
}
void BackendObject_DirectVulkanTMP::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP
@@ -0,0 +1,36 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/BackendObject_DirectVulkanTMP.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "../BackendObject.h"
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkanTMP {
class BackendObject_DirectVulkanTMP : public BackendObject {
public:
~BackendObject_DirectVulkanTMP() override;
void Initialize() override;
void InitWindowSurface() override;
void InitCapabilities() override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
const DynamicBackendParameters& GetDynamicParameters() const override;
BackendType GetBackendType() const override;
private:
void UpdateDynamicBackendParameters();
Bool m_initialized = false;
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
};
} // namespace MobileGL::MG_Backend::DirectVulkanTMP
@@ -0,0 +1,74 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/DirectVulkanTMP.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 "DirectVulkanTMP.h"
#include "TmpImpl.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP {
void Clear(GLbitfield mask) {
MobileGL::MG_Backend::DirectVulkanTMP::TmpImpl::Clear(mask);
}
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
MobileGL::MG_Backend::DirectVulkanTMP::TmpImpl::DrawElements(mode, count, type, indices);
}
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) {}
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {}
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) {}
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {}
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {}
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {}
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {}
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {}
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {}
void DrawArraysIndirect(GLenum mode, const void* indirect) {}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) {}
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border) {}
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height) {}
void GenerateMipmap(GLenum target) {}
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {}
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {}
Bool InitWindowSurface(NativeWindowType window) {
if (!window) {
MGLOG_E("Cannot initialize Vulkan window surface: invalid window handle");
return false;
}
TmpImpl::InitVulkan(window);
return true;
}
void Present() {
TmpImpl::Present();
}
const TmpImpl::VulkanState& GetVulkanState() {
return TmpImpl::GetVulkanState();
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP
@@ -0,0 +1,58 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/DirectVulkanTMP.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkanTMP {
namespace TmpImpl {
class VulkanState;
} // namespace TmpImpl
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);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance);
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex);
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance);
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect);
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
Bool InitWindowSurface(NativeWindowType window);
void Present();
const TmpImpl::VulkanState& GetVulkanState();
} // namespace MobileGL::MG_Backend::DirectVulkanTMP
@@ -0,0 +1,447 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Managers/ProgramManager.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 "ProgramManager.h"
#include "MG_Util/Debug/Log.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
#include "source/opt/constants.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_builder.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/opt/pass.h"
#include "source/opt/type_manager.h"
#include <bit>
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
namespace {
using ProgramObject = MG_State::GLState::ProgramObject;
using ShaderObject = MG_State::GLState::ShaderObject;
struct PositionTargetInfo {
uint32_t variableId = 0;
uint32_t vectorTypeId = 0;
uint32_t floatTypeId = 0;
uint32_t vectorPtrTypeId = 0;
uint32_t memberIndex = 0;
bool isMember = false;
};
bool IsVec4Float32(spvtools::opt::IRContext* context, uint32_t typeId, uint32_t* outFloatTypeId) {
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
if (!vecInst || vecInst->opcode() != spv::Op::OpTypeVector) return false;
if (vecInst->GetSingleWordInOperand(1) != 4) return false;
const uint32_t floatTypeId = vecInst->GetSingleWordInOperand(0);
auto* floatInst = context->get_def_use_mgr()->GetDef(floatTypeId);
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
if (outFloatTypeId) *outFloatTypeId = floatTypeId;
return true;
}
bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, uint32_t variableId,
PositionTargetInfo* outTarget) {
auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
if (varInst->GetSingleWordInOperand(0) != static_cast<uint32_t>(spv::StorageClass::Output)) return false;
auto* ptrTypeInst = context->get_def_use_mgr()->GetDef(varInst->type_id());
if (!ptrTypeInst || ptrTypeInst->opcode() != spv::Op::OpTypePointer) return false;
if (ptrTypeInst->GetSingleWordInOperand(0) != static_cast<uint32_t>(spv::StorageClass::Output))
return false;
PositionTargetInfo target{};
target.variableId = variableId;
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false;
target.vectorPtrTypeId = varInst->type_id();
target.isMember = false;
*outTarget = target;
return true;
}
uint32_t FindOutputVectorPointerTypeId(spvtools::opt::IRContext* context, uint32_t vectorTypeId) {
auto* vectorType = context->get_type_mgr()->GetType(vectorTypeId);
if (!vectorType) return 0;
spvtools::opt::analysis::Pointer ptrType(vectorType, spv::StorageClass::Output);
return context->get_type_mgr()->GetTypeInstruction(&ptrType);
}
bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, uint32_t structTypeId, uint32_t memberIndex,
PositionTargetInfo* outTarget) {
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
if (memberIndex >= structInst->NumInOperands()) return false;
const uint32_t vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
uint32_t floatTypeId = 0;
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false;
const uint32_t vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
if (vectorPtrTypeId == 0) return false;
for (auto& inst : context->module()->types_values()) {
if (inst.opcode() != spv::Op::OpVariable) continue;
if (inst.GetSingleWordInOperand(0) != static_cast<uint32_t>(spv::StorageClass::Output)) continue;
auto* ptrTypeInst = context->get_def_use_mgr()->GetDef(inst.type_id());
if (!ptrTypeInst || ptrTypeInst->opcode() != spv::Op::OpTypePointer) continue;
if (ptrTypeInst->GetSingleWordInOperand(0) != static_cast<uint32_t>(spv::StorageClass::Output))
continue;
if (ptrTypeInst->GetSingleWordInOperand(1) != structTypeId) continue;
PositionTargetInfo target{};
target.variableId = inst.result_id();
target.vectorTypeId = vectorTypeId;
target.floatTypeId = floatTypeId;
target.vectorPtrTypeId = vectorPtrTypeId;
target.memberIndex = memberIndex;
target.isMember = true;
*outTarget = target;
return true;
}
return false;
}
bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
Vector<Pair<uint32_t, uint32_t>> memberCandidates;
constexpr uint32_t kDecorationBuiltIn = static_cast<uint32_t>(spv::Decoration::BuiltIn);
constexpr uint32_t kBuiltInPosition = static_cast<uint32_t>(spv::BuiltIn::Position);
for (auto& inst : context->module()->annotations()) {
if (inst.opcode() == spv::Op::OpDecorate) {
if (inst.NumInOperands() < 3) continue;
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue;
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true;
} else if (inst.opcode() == spv::Op::OpMemberDecorate) {
if (inst.NumInOperands() < 4) continue;
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue;
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
}
}
for (const auto& [structTypeId, memberIndex] : memberCandidates) {
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true;
}
return false;
}
bool InsertPositionFixup(spvtools::opt::IRContext* context, spvtools::opt::Instruction* insertBefore,
const PositionTargetInfo& target, uint32_t halfConstId, bool doYFlip, bool doZRemap) {
using namespace spvtools::opt;
InstructionBuilder builder(context, insertBefore,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
uint32_t positionPtrId = target.variableId;
if (target.isMember) {
const uint32_t memberIndexId = builder.GetUintConstantId(target.memberIndex);
if (memberIndexId == 0) return false;
auto* access = builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
if (!access) return false;
positionPtrId = access->result_id();
}
auto* position = builder.AddLoad(target.vectorTypeId, positionPtrId);
if (!position) return false;
auto* x = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {0});
auto* y = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {1});
auto* z = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {2});
auto* w = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {3});
if (!x || !y || !z || !w) return false;
if (!doYFlip && !doZRemap) return false;
uint32_t yValueId = y->result_id();
if (doYFlip) {
auto* negY = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, y->result_id());
if (!negY) return false;
yValueId = negY->result_id();
}
uint32_t zValueId = z->result_id();
if (doZRemap) {
auto* zPlusW = builder.AddBinaryOp(target.floatTypeId, spv::Op::OpFAdd, z->result_id(), w->result_id());
if (!zPlusW) return false;
auto* mappedZ =
builder.AddBinaryOp(target.floatTypeId, spv::Op::OpFMul, zPlusW->result_id(), halfConstId);
if (!mappedZ) return false;
zValueId = mappedZ->result_id();
}
auto* fixedPosition = builder.AddCompositeConstruct(target.vectorTypeId,
{x->result_id(), yValueId, zValueId, w->result_id()});
if (!fixedPosition) return false;
return builder.AddStore(positionPtrId, fixedPosition->result_id()) != nullptr;
}
class GlToVulkanPositionFixPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "gl-to-vulkan-position-fix"; }
explicit GlToVulkanPositionFixPass(ShaderTransformFlags transformFlags)
: m_transformFlags(transformFlags) {}
Status Process() override {
if (!m_transformFlags) return Status::SuccessWithoutChange;
PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
if (!floatType) return Status::SuccessWithoutChange;
const uint32_t halfBits = std::bit_cast<uint32_t>(0.5f);
const auto* halfConst = context()->get_constant_mgr()->GetConstant(floatType, {halfBits});
auto* halfInst = context()->get_constant_mgr()->GetDefiningInstruction(halfConst);
if (!halfInst) return Status::SuccessWithoutChange;
const uint32_t halfConstId = halfInst->result_id();
const bool doYFlip = (m_transformFlags & ShaderTransformBit::PositionYFlip);
const bool doZRemap = (m_transformFlags & ShaderTransformBit::PositionZRemap);
bool modified = false;
for (auto& entryPoint : get_module()->entry_points()) {
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
if (entryPoint.NumInOperands() < 2) continue;
const auto model = static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0));
if (model != spv::ExecutionModel::Vertex && model != spv::ExecutionModel::TessellationEvaluation &&
model != spv::ExecutionModel::Geometry) {
continue;
}
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
if (!function) continue;
for (auto& bb : *function) {
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
auto* inst = &*instIter;
const bool needsFixup =
(model == spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpEmitVertex) ||
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
if (!needsFixup) continue;
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap);
}
}
}
if (!modified) return Status::SuccessWithoutChange;
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisDefUse |
spvtools::opt::IRContext::kAnalysisInstrToBlockMapping);
return Status::SuccessWithChange;
}
private:
ShaderTransformFlags m_transformFlags;
};
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(ShaderTransformFlags transformFlags) {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
}
bool TransformSpirvForVulkanPositionFix(const Vector<Uint32>& input, Vector<Uint32>& output,
ShaderTransformFlags transformFlags) {
if (input.empty()) {
output.clear();
return true;
}
if (!transformFlags) {
output = input;
return true;
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
options.set_run_validator(false);
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
const bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: failed to run GL->Vulkan position fix pass");
output = input;
}
return success;
}
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders) {
bool hasGeometry = false;
bool hasTessEval = false;
bool hasVertex = false;
for (const auto& shader : shaders) {
if (!shader) continue;
const auto stage = shader->GetShaderStage();
hasGeometry |= (stage == ShaderStage::Geometry);
hasTessEval |= (stage == ShaderStage::TessEval);
hasVertex |= (stage == ShaderStage::Vertex);
}
if (hasGeometry) return ShaderStage::Geometry;
if (hasTessEval) return ShaderStage::TessEval;
if (hasVertex) return ShaderStage::Vertex;
return ShaderStage::Unknown;
}
} // namespace
ProgramManager::~ProgramManager() {
for (auto& [_, stages] : m_cache) {
DestroyStages(stages);
}
m_cache.clear();
}
ProgramManager::HashType ProgramManager::ComputeSourceSpvHash(MG_State::GLState::ProgramObject* program) const {
if (!program) return 0;
XXH64_state_t* state = XXH64_createState();
XXH64_reset(state, 0xC0FFEEu);
auto& spirvs = program->GetGeneratedSpirv();
for (const auto& spv : spirvs) {
if (spv.empty()) continue;
XXH64_update(state, spv.data(), spv.size() * sizeof(Uint));
}
HashType hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
ProgramManager::HashType ProgramManager::ComputeSpvHash(const Vector<Vector<Uint32>>& spirvs) const {
XXH64_state_t* state = XXH64_createState();
XXH64_reset(state, 0xC0FFEEu);
for (const auto& spv : spirvs) {
if (spv.empty()) continue;
XXH64_update(state, spv.data(), spv.size() * sizeof(Uint32));
}
HashType hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
void ProgramManager::BuildPipelineSpirvModules(MG_State::GLState::ProgramObject* program,
Vector<Vector<Uint32>>& outSpirvs,
ShaderTransformFlags transformFlags) const {
outSpirvs.clear();
if (!program) return;
auto& spirvs = program->GetGeneratedSpirv();
auto& shaders = program->GetAttachedShaders();
const ShaderStage fixupStage = PickClipFixupStage(shaders);
outSpirvs.reserve(spirvs.size());
for (SizeT i = 0; i < spirvs.size(); ++i) {
Vector<Uint32> module = spirvs[i];
ShaderStage stage = ShaderStage::Unknown;
if (i < shaders.size() && shaders[i]) stage = shaders[i]->GetShaderStage();
if (!module.empty() && fixupStage != ShaderStage::Unknown && stage == fixupStage) {
Vector<Uint32> transformed;
TransformSpirvForVulkanPositionFix(module, transformed, transformFlags);
module = Move(transformed);
}
outSpirvs.push_back(Move(module));
}
}
ProgramManager::HashType ProgramManager::ComputeSpvHash(MG_State::GLState::ProgramObject* program,
ShaderTransformFlags transformFlags) const {
Vector<Vector<Uint32>> spirvs;
BuildPipelineSpirvModules(program, spirvs, transformFlags);
return ComputeSpvHash(spirvs);
}
ProgramManager::HashType ProgramManager::ComputeProgramHash(MG_State::GLState::ProgramObject* program,
ShaderTransformFlags transformFlags) const {
if (!program) return 0;
const HashType sourceHash = ComputeSourceSpvHash(program);
auto it = m_cache.find(program);
if (it != m_cache.end() && it->second.sourceHash == sourceHash) return it->second.hash;
return ComputeSpvHash(program, transformFlags);
}
VkShaderStageFlagBits ProgramManager::ToVkStage(ShaderStage stage) const {
switch (stage) {
case ShaderStage::Vertex:
return VK_SHADER_STAGE_VERTEX_BIT;
case ShaderStage::Fragment:
return VK_SHADER_STAGE_FRAGMENT_BIT;
case ShaderStage::Geometry:
return VK_SHADER_STAGE_GEOMETRY_BIT;
case ShaderStage::TessControl:
return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
case ShaderStage::TessEval:
return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;
case ShaderStage::Compute:
return VK_SHADER_STAGE_COMPUTE_BIT;
default:
return VK_SHADER_STAGE_ALL_GRAPHICS;
}
}
void ProgramManager::DestroyStages(ProgramStages& stages) {
for (auto module : stages.modules) {
if (module != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_ctx.GetDevice(), module, nullptr);
}
}
stages.modules.clear();
stages.stages.clear();
stages.sourceHash = 0;
stages.hash = 0;
}
Vector<VkPipelineShaderStageCreateInfo>& ProgramManager::CreatePipelineShaderStages(
MG_State::GLState::ProgramObject* program, ShaderTransformFlags transformFlags) {
auto& entry = m_cache[program];
HashType sourceHash = ComputeSourceSpvHash(program);
if (!entry.stages.empty() && entry.sourceHash == sourceHash) return entry.stages;
DestroyStages(entry);
entry.sourceHash = sourceHash;
if (!program) return entry.stages;
Vector<Vector<Uint32>> spirvs;
BuildPipelineSpirvModules(program, spirvs, transformFlags);
entry.hash = ComputeSpvHash(spirvs);
auto& shaders = program->GetAttachedShaders();
for (SizeT i = 0; i < spirvs.size(); ++i) {
auto& spv = spirvs[i];
if (spv.empty()) continue;
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = spv.size() * sizeof(Uint);
smci.pCode = spv.data();
VkShaderModule module = VK_NULL_HANDLE;
VK_VERIFY(vkCreateShaderModule(m_ctx.GetDevice(), &smci, nullptr, &module), "vkCreateShaderModule");
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
ShaderStage shaderStage = ShaderStage::Unknown;
if (i < shaders.size() && shaders[i]) shaderStage = shaders[i]->GetShaderStage();
stage.stage = ToVkStage(shaderStage);
stage.module = module;
stage.pName = "main";
entry.modules.push_back(module);
entry.stages.push_back(stage);
}
return entry.stages;
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,58 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Managers/ProgramManager.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "../Renderer/VulkanContext.h"
#include "../Renderer/VkCommon.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
enum class ShaderTransformBit : Uint {
None = 0,
PositionYFlip = 1 << 0,
PositionZRemap = 1 << 1,
};
using ShaderTransformFlags = Flags<ShaderTransformBit>;
class ProgramManager {
public:
using HashType = Uint64;
explicit ProgramManager(VulkanContext& ctx) : m_ctx(ctx) {}
~ProgramManager();
ProgramManager(const ProgramManager&) = delete;
ProgramManager& operator=(const ProgramManager&) = delete;
Vector<VkPipelineShaderStageCreateInfo>& CreatePipelineShaderStages(
MG_State::GLState::ProgramObject* program,
ShaderTransformFlags transformFlags = ShaderTransformBit::PositionZRemap);
HashType ComputeProgramHash(MG_State::GLState::ProgramObject* program,
ShaderTransformFlags transformFlags = ShaderTransformBit::PositionZRemap) const;
private:
struct ProgramStages {
HashType sourceHash = 0;
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules;
};
void DestroyStages(ProgramStages& stages);
HashType ComputeSourceSpvHash(MG_State::GLState::ProgramObject* program) const;
HashType ComputeSpvHash(MG_State::GLState::ProgramObject* program, ShaderTransformFlags transformFlags) const;
HashType ComputeSpvHash(const Vector<Vector<Uint32>>& spirvs) const;
void BuildPipelineSpirvModules(MG_State::GLState::ProgramObject* program, Vector<Vector<Uint32>>& outSpirvs,
ShaderTransformFlags transformFlags) const;
VkShaderStageFlagBits ToVkStage(ShaderStage stage) const;
VulkanContext& m_ctx;
UnorderedMap<const MG_State::GLState::ProgramObject*, ProgramStages> m_cache;
};
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/FrameContext.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 "FrameContext.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
void FrameContext::Initialize(VulkanContext& ctx, VkCommandPool pool) {
CommandPool = pool;
VkCommandBufferAllocateInfo abci{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
abci.commandPool = pool;
abci.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
abci.commandBufferCount = 1;
VK_VERIFY(vkAllocateCommandBuffers(ctx.GetDevice(), &abci, &CommandBuffer), "vkAllocateCommandBuffers");
VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_VERIFY(vkCreateSemaphore(ctx.GetDevice(), &sci, nullptr, &ImageAvailable), "vkCreateSemaphore");
VK_VERIFY(vkCreateSemaphore(ctx.GetDevice(), &sci, nullptr, &RenderFinished), "vkCreateSemaphore");
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VK_VERIFY(vkCreateFence(ctx.GetDevice(), &fci, nullptr, &InFlightFence), "vkCreateFence");
}
void FrameContext::Cleanup(VulkanContext& ctx) {
auto device = ctx.GetDevice();
if (InFlightFence != VK_NULL_HANDLE) {
vkDestroyFence(device, InFlightFence, nullptr);
InFlightFence = VK_NULL_HANDLE;
}
if (ImageAvailable != VK_NULL_HANDLE) {
vkDestroySemaphore(device, ImageAvailable, nullptr);
ImageAvailable = VK_NULL_HANDLE;
}
if (RenderFinished != VK_NULL_HANDLE) {
vkDestroySemaphore(device, RenderFinished, nullptr);
RenderFinished = VK_NULL_HANDLE;
}
if (CommandBuffer != VK_NULL_HANDLE) {
vkFreeCommandBuffers(device, CommandPool, 1, &CommandBuffer);
CommandBuffer = VK_NULL_HANDLE;
}
CommandPool = VK_NULL_HANDLE;
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,42 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/FrameContext.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "VulkanContext.h"
#include "VkCommon.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP {
struct TrashBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
Bool mapped = false;
};
struct TrashImage {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
Vector<VkImageView> mipViews;
};
namespace VkManager {
struct FrameContext {
VkCommandBuffer CommandBuffer = VK_NULL_HANDLE;
VkSemaphore ImageAvailable = VK_NULL_HANDLE;
VkSemaphore RenderFinished = VK_NULL_HANDLE;
VkFence InFlightFence = VK_NULL_HANDLE;
Uint32 CurrentImageIndex = 0;
VkCommandPool CommandPool = VK_NULL_HANDLE;
Vector<TrashBuffer> TrashBuffers;
Vector<TrashImage> TrashImages;
// VkCommandPool CommandPool = VK_NULL_HANDLE;
void Initialize(VulkanContext& ctx, VkCommandPool pool);
void Cleanup(VulkanContext& ctx);
};
} // namespace VkManager
} // namespace MobileGL::MG_Backend::DirectVulkanTMP
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.h // MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/PipelineManager.cpp
// Copyright (c) 2025-2026 MobileGL-Dev // Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0: // Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/gpl-3.0.txt
@@ -6,9 +6,6 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header // End of Source File Header
#pragma once
#include <Includes.h> #include <Includes.h>
namespace MobileGL::MG_Impl::NSOpenGLImpl { namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {}
void InstallHooks();
}
@@ -0,0 +1,166 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/SwapchainManager.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 "SwapchainManager.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
namespace {
VkSurfaceFormatKHR ChooseSurfaceFormat(const Vector<VkSurfaceFormatKHR>& formats) {
for (const auto& f : formats) {
if ((f.format == VK_FORMAT_R8G8B8A8_UNORM || f.format == VK_FORMAT_B8G8R8A8_UNORM) &&
f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return f;
}
}
return formats.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}
: formats[0];
}
VkPresentModeKHR ChoosePresentMode(const Vector<VkPresentModeKHR>& modes) {
for (const auto& m : modes) {
if (m == VK_PRESENT_MODE_MAILBOX_KHR) return m;
}
return VK_PRESENT_MODE_FIFO_KHR;
}
VkExtent2D ChooseExtent(const VkSurfaceCapabilitiesKHR& caps, ANativeWindow* window) {
if (caps.currentExtent.width != UINT32_MAX) return caps.currentExtent;
// try to get frontend viewport size
VkExtent2D extent{640, 480};
#ifdef VK_USE_PLATFORM_ANDROID_KHR
if (window) {
extent.width = static_cast<Uint32>(ANativeWindow_getWidth(window));
extent.height = static_cast<Uint32>(ANativeWindow_getHeight(window));
}
#else
(void)window;
#endif
extent.width = std::max(caps.minImageExtent.width, std::min(caps.maxImageExtent.width, extent.width));
extent.height = std::max(caps.minImageExtent.height, std::min(caps.maxImageExtent.height, extent.height));
return extent;
}
} // namespace
SwapchainManager::~SwapchainManager() {
DestroySwapchain();
}
void SwapchainManager::Initialize() {
CreateSwapchain(VK_NULL_HANDLE);
}
void SwapchainManager::Recreate() {
DestroySwapchain();
CreateSwapchain(VK_NULL_HANDLE);
}
void SwapchainManager::SetFramebuffers(Vector<VkFramebuffer>&& framebuffers) {
m_framebuffers = Move(framebuffers);
}
void SwapchainManager::DestroySwapchain() {
auto device = m_ctx.GetDevice();
if (device == VK_NULL_HANDLE) return;
for (auto fb : m_framebuffers) {
if (fb != VK_NULL_HANDLE) vkDestroyFramebuffer(device, fb, nullptr);
}
m_framebuffers.clear();
for (auto view : m_imageViews) {
if (view != VK_NULL_HANDLE) vkDestroyImageView(device, view, nullptr);
}
m_imageViews.clear();
m_images.clear();
m_imagesInFlight.clear();
if (m_swapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device, m_swapchain, nullptr);
m_swapchain = VK_NULL_HANDLE;
}
m_format = VK_FORMAT_UNDEFINED;
m_extent = {0, 0};
}
void SwapchainManager::CreateSwapchain(VkSwapchainKHR oldSwapchain) {
VkSurfaceCapabilitiesKHR caps{};
VK_VERIFY(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_ctx.GetPhysicalDevice(), m_ctx.GetSurface(), &caps),
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
Uint32 fmtCount = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(m_ctx.GetPhysicalDevice(), m_ctx.GetSurface(), &fmtCount, nullptr);
Vector<VkSurfaceFormatKHR> formats(fmtCount);
if (fmtCount > 0) {
vkGetPhysicalDeviceSurfaceFormatsKHR(m_ctx.GetPhysicalDevice(), m_ctx.GetSurface(), &fmtCount,
formats.data());
}
Uint32 modeCount = 0;
vkGetPhysicalDeviceSurfacePresentModesKHR(m_ctx.GetPhysicalDevice(), m_ctx.GetSurface(), &modeCount, nullptr);
Vector<VkPresentModeKHR> modes(modeCount);
if (modeCount > 0) {
vkGetPhysicalDeviceSurfacePresentModesKHR(m_ctx.GetPhysicalDevice(), m_ctx.GetSurface(), &modeCount,
modes.data());
}
VkSurfaceFormatKHR surfaceFormat = ChooseSurfaceFormat(formats);
VkPresentModeKHR presentMode = ChoosePresentMode(modes);
VkExtent2D extent;
if (m_viewportSize.x() == 0 || m_viewportSize.y() == 0) {
extent = ChooseExtent(caps, m_ctx.GetWindow());
} else {
extent = {m_viewportSize.x(), m_viewportSize.y()};
}
Uint32 imageCount = caps.minImageCount + 1;
if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount) imageCount = caps.maxImageCount;
VkSwapchainCreateInfoKHR sci{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
sci.surface = m_ctx.GetSurface();
sci.minImageCount = imageCount;
sci.imageFormat = surfaceFormat.format;
sci.imageColorSpace = surfaceFormat.colorSpace;
sci.imageExtent = extent;
sci.imageArrayLayers = 1;
sci.imageUsage =
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
sci.preTransform = caps.currentTransform;
sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
sci.presentMode = presentMode;
sci.clipped = VK_TRUE;
sci.oldSwapchain = oldSwapchain;
VK_VERIFY(vkCreateSwapchainKHR(m_ctx.GetDevice(), &sci, nullptr, &m_swapchain), "vkCreateSwapchainKHR");
Uint32 actualCount = 0;
vkGetSwapchainImagesKHR(m_ctx.GetDevice(), m_swapchain, &actualCount, nullptr);
m_images.resize(actualCount);
vkGetSwapchainImagesKHR(m_ctx.GetDevice(), m_swapchain, &actualCount, m_images.data());
m_imageViews.clear();
m_imageViews.reserve(actualCount);
for (auto image : m_images) {
VkImageViewCreateInfo ivci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
ivci.image = image;
ivci.viewType = VK_IMAGE_VIEW_TYPE_2D;
ivci.format = surfaceFormat.format;
ivci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
ivci.subresourceRange.baseMipLevel = 0;
ivci.subresourceRange.levelCount = 1;
ivci.subresourceRange.baseArrayLayer = 0;
ivci.subresourceRange.layerCount = 1;
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_ctx.GetDevice(), &ivci, nullptr, &view), "vkCreateImageView swapchain");
m_imageViews.push_back(view);
}
m_imagesInFlight.assign(actualCount, VK_NULL_HANDLE);
m_format = surfaceFormat.format;
m_extent = extent;
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,52 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/SwapchainManager.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MG_Util/Math/VectorTypes.h"
#include "VulkanContext.h"
#include "VkCommon.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
class SwapchainManager {
public:
explicit SwapchainManager(VulkanContext& ctx) : m_ctx(ctx) {}
~SwapchainManager();
SwapchainManager(const SwapchainManager&) = delete;
SwapchainManager& operator=(const SwapchainManager&) = delete;
void Initialize();
void Recreate();
void SetViewportSize(const UintVec2& size) { m_viewportSize = size; }
VkSwapchainKHR GetSwapchain() const { return m_swapchain; }
VkFormat GetFormat() const { return m_format; }
VkExtent2D GetExtent() const { return m_extent; }
const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
const Vector<VkFramebuffer>& GetFramebuffers() const { return m_framebuffers; }
void SetFramebuffers(Vector<VkFramebuffer>&& framebuffers);
Vector<VkFence>& GetImagesInFlight() { return m_imagesInFlight; }
const UintVec2& GetViewportSize() const { return m_viewportSize; }
private:
void CreateSwapchain(VkSwapchainKHR oldSwapchain);
void DestroySwapchain();
VulkanContext& m_ctx;
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkFormat m_format = VK_FORMAT_UNDEFINED;
VkExtent2D m_extent{0, 0};
Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews;
Vector<VkFramebuffer> m_framebuffers;
Vector<VkFence> m_imagesInFlight;
UintVec2 m_viewportSize{0, 0};
};
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,22 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/VkCommon.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
inline void VkCheck(VkResult result, const char* msg) {
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) return;
MGLOG_E("Vulkan error %d at %s", static_cast<Int>(result), msg ? msg : "(unknown)");
throw RuntimeError(msg ? msg : "Vulkan error");
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
#ifndef VK_VERIFY
#define VK_VERIFY(res, msg) ::MobileGL::MG_Backend::DirectVulkanTMP::VkManager::VkCheck((res), (msg))
#endif
@@ -0,0 +1,191 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/VulkanContext.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 "VulkanContext.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
namespace {
Bool CheckDeviceExtensionSupport(VkPhysicalDevice device, const Vector<const char*>& required) {
Uint32 count = 0;
vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr);
Vector<VkExtensionProperties> props(count);
if (count > 0) vkEnumerateDeviceExtensionProperties(device, nullptr, &count, props.data());
for (auto* ext : required) {
Bool found = false;
for (const auto& p : props) {
if (std::strcmp(p.extensionName, ext) == 0) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
Bool FindGraphicsQueueFamily(VkPhysicalDevice device, VkSurfaceKHR surface, Uint32& outFamily) {
Uint32 count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr);
if (count == 0) return false;
Vector<VkQueueFamilyProperties> props(count);
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, props.data());
for (Uint32 i = 0; i < count; ++i) {
if (!(props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) continue;
if (surface != VK_NULL_HANDLE) {
VkBool32 presentSupport = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
if (!presentSupport) continue;
}
outFamily = i;
return true;
}
return false;
}
Bool SupportsDynamicRendering(VkPhysicalDevice device) {
VkPhysicalDeviceVulkan13Features vk13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
VkPhysicalDeviceFeatures2 features2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
features2.pNext = &vk13;
vkGetPhysicalDeviceFeatures2(device, &features2);
return vk13.dynamicRendering == VK_TRUE;
}
Bool SupportsTimelineSemaphore(VkPhysicalDevice device) {
VkPhysicalDeviceVulkan12Features vk12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
VkPhysicalDeviceFeatures2 features2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
features2.pNext = &vk12;
vkGetPhysicalDeviceFeatures2(device, &features2);
return vk12.timelineSemaphore == VK_TRUE;
}
} // namespace
VulkanContext::~VulkanContext() {
Cleanup();
}
void VulkanContext::Initialize(ANativeWindow* window, const char* appName) {
if (m_instance != VK_NULL_HANDLE) return;
m_window = window;
CreateInstance(appName ? appName : "MobileGL-Vulkan");
CreateSurface(window);
PickPhysicalDevice();
CreateDevice();
}
void VulkanContext::Cleanup() {
if (m_device != VK_NULL_HANDLE) {
vkDeviceWaitIdle(m_device);
vkDestroyDevice(m_device, nullptr);
m_device = VK_NULL_HANDLE;
}
if (m_surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
m_surface = VK_NULL_HANDLE;
}
if (m_instance != VK_NULL_HANDLE) {
vkDestroyInstance(m_instance, nullptr);
m_instance = VK_NULL_HANDLE;
}
m_physicalDevice = VK_NULL_HANDLE;
m_graphicsQueue = VK_NULL_HANDLE;
m_graphicsQueueFamily = ~0u;
m_window = nullptr;
}
void VulkanContext::CreateInstance(const char* appName) {
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = appName;
app.applicationVersion = VK_MAKE_VERSION(1, 3, 0);
app.pEngineName = "MobileGL";
app.engineVersion = VK_MAKE_VERSION(1, 3, 0);
app.apiVersion = VK_API_VERSION_1_3;
Vector<const char*> extensions;
extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
#ifdef VK_USE_PLATFORM_ANDROID_KHR
extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#endif
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
ici.pApplicationInfo = &app;
ici.enabledExtensionCount = static_cast<Uint32>(extensions.size());
ici.ppEnabledExtensionNames = extensions.data();
VK_VERIFY(vkCreateInstance(&ici, nullptr, &m_instance), "vkCreateInstance");
}
void VulkanContext::CreateSurface(ANativeWindow* window) {
if (!window) return;
#ifdef VK_USE_PLATFORM_ANDROID_KHR
VkAndroidSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR};
sci.window = window;
VK_VERIFY(vkCreateAndroidSurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateAndroidSurfaceKHR");
#else
(void)window;
#endif
}
void VulkanContext::PickPhysicalDevice() {
Uint32 count = 0;
VK_VERIFY(vkEnumeratePhysicalDevices(m_instance, &count, nullptr), "vkEnumeratePhysicalDevices");
if (count == 0) throw RuntimeError("No Vulkan physical devices found");
Vector<VkPhysicalDevice> devices(count);
VK_VERIFY(vkEnumeratePhysicalDevices(m_instance, &count, devices.data()), "vkEnumeratePhysicalDevices list");
Vector<const char*> requiredExts = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
for (auto device : devices) {
Uint32 family = ~0u;
if (!FindGraphicsQueueFamily(device, m_surface, family)) continue;
if (!CheckDeviceExtensionSupport(device, requiredExts)) continue;
if (!SupportsDynamicRendering(device)) continue;
m_physicalDevice = device;
m_graphicsQueueFamily = family;
break;
}
if (m_physicalDevice == VK_NULL_HANDLE) {
throw RuntimeError("No suitable Vulkan physical device found");
}
}
void VulkanContext::CreateDevice() {
float priority = 1.0f;
VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
qci.queueFamilyIndex = m_graphicsQueueFamily;
qci.queueCount = 1;
qci.pQueuePriorities = &priority;
Vector<const char*> deviceExtensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
VkPhysicalDeviceFeatures features{};
VkPhysicalDeviceVulkan13Features vk13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
vk13.dynamicRendering = VK_TRUE;
VkPhysicalDeviceVulkan12Features vk12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
vk12.timelineSemaphore = SupportsTimelineSemaphore(m_physicalDevice) ? VK_TRUE : VK_FALSE;
VkPhysicalDeviceFeatures2 features2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
features2.features = features;
vk13.pNext = &vk12;
features2.pNext = &vk13;
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
dci.pNext = &features2;
dci.queueCreateInfoCount = 1;
dci.pQueueCreateInfos = &qci;
dci.enabledExtensionCount = static_cast<Uint32>(deviceExtensions.size());
dci.ppEnabledExtensionNames = deviceExtensions.data();
dci.pEnabledFeatures = nullptr;
VK_VERIFY(vkCreateDevice(m_physicalDevice, &dci, nullptr, &m_device), "vkCreateDevice");
vkGetDeviceQueue(m_device, m_graphicsQueueFamily, 0, &m_graphicsQueue);
}
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
@@ -0,0 +1,47 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/Renderer/VulkanContext.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "VkCommon.h"
namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager {
class VulkanContext {
public:
VulkanContext() = default;
~VulkanContext();
VulkanContext(const VulkanContext&) = delete;
VulkanContext& operator=(const VulkanContext&) = delete;
void Initialize(ANativeWindow* window, const char* appName);
void Cleanup();
VkInstance GetInstance() const { return m_instance; }
VkPhysicalDevice GetPhysicalDevice() const { return m_physicalDevice; }
VkDevice GetDevice() const { return m_device; }
VkQueue GetGraphicsQueue() const { return m_graphicsQueue; }
Uint32 GetGraphicsQueueFamily() const { return m_graphicsQueueFamily; }
VkSurfaceKHR GetSurface() const { return m_surface; }
ANativeWindow* GetWindow() const { return m_window; }
private:
void CreateInstance(const char* appName);
void CreateSurface(ANativeWindow* window);
void PickPhysicalDevice();
void CreateDevice();
VkInstance m_instance = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkDevice m_device = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
VkSurfaceKHR m_surface = VK_NULL_HANDLE;
Uint32 m_graphicsQueueFamily = ~0u;
ANativeWindow* m_window = nullptr;
};
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::VkManager
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkanTMP/TmpImpl.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "Renderer/VkCommon.h"
#include "Renderer/VulkanContext.h"
#include "Renderer/SwapchainManager.h"
#include "Renderer/FrameContext.h"
#include "Managers/ProgramManager.h"
#include <MG_State/GLState/Core.h>
namespace MobileGL::MG_Backend::DirectVulkanTMP::TmpImpl {
namespace DV = MobileGL::MG_Backend::DirectVulkanTMP::VkManager;
using BufferObject = MobileGL::MG_State::GLState::BufferObject;
using ProgramObject = MobileGL::MG_State::GLState::ProgramObject;
using VertexArrayObject = MobileGL::MG_State::GLState::VertexArrayObject;
using FramebufferObject = MobileGL::MG_State::GLState::FramebufferObject;
using FramebufferAttachmentObject = MobileGL::MG_State::GLState::FramebufferAttachmentObject;
using ITextureObject = MobileGL::MG_State::GLState::ITextureObject;
using RenderbufferObject = MobileGL::MG_State::GLState::RenderbufferObject;
using SamplerObject = MobileGL::MG_State::GLState::SamplerObject;
using TextureObjectMipmap = MobileGL::MG_State::GLState::TextureObjectMipmap;
using TrashImage = MobileGL::MG_Backend::DirectVulkanTMP::TrashImage;
struct BufferResource {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize size = 0;
VkDeviceSize offset = 0;
VkBufferUsageFlags usage = 0;
VkMemoryPropertyFlags props = 0;
void* mapped = nullptr;
Bool fromRing = false;
Uint32 lastUsedFrame = ~0u;
};
struct BufferResourceSet {
Vector<BufferResource> perFrame;
};
struct PendingBufferCopyBatch {
VkBuffer src = VK_NULL_HANDLE;
VkBuffer dst = VK_NULL_HANDLE;
Vector<VkBufferCopy> regions;
};
struct StagingRing {
BufferResource buffer;
VkDeviceSize capacity = 0;
VkDeviceSize head = 0;
};
struct TextureResource {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
UnorderedMap<Uint32, VkImageView> mipViews;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent3D extent{0, 0, 1};
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
Uint32 mipLevels = 1;
Uint16 paramsVersion = 0;
bool valid = false;
};
struct RenderbufferResource {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{0, 0};
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
bool valid = false;
};
struct SamplerResource {
VkSampler sampler = VK_NULL_HANDLE;
Uint16 version = 0;
SamplerParameters params{};
};
struct PendingClearInfo {
GLbitfield mask = 0;
FloatVec4 clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
Float clearDepth = 1.0f;
};
struct UniformBindingInfo {
String name;
Uint32 binding = 0;
Uint32 set = 0;
Uint32 blockIndex = 0xFFFFFFFFu;
VkShaderStageFlags stages = 0;
};
struct SamplerBindingInfo {
String name;
Uint32 binding = 0;
Uint32 set = 0;
VkShaderStageFlags stages = 0;
};
struct ProgramResource {
ProgramObject* program = nullptr;
Uint64 spvHash = 0;
DV::ProgramManager::HashType programHash = 0;
Vector<VkPipelineShaderStageCreateInfo>* shaderStages = nullptr;
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
struct UboPool {
Vector<BufferResource> buffers;
Uint32 cursor = 0;
};
Vector<UboPool> uboPools;
SizeT uboSize = 0;
Int32 uboBinding = -1;
Uint32 uniformDescriptorCount = 0;
Uint32 samplerDescriptorCount = 0;
Vector<UniformBindingInfo> uniformBindings;
Vector<SamplerBindingInfo> samplerBindings;
UnorderedMap<String, Uint32> samplerBindingByName;
UnorderedMap<Uint64, VkPipeline> pipelines;
};
struct DescriptorPoolConfig {
Uint32 maxSets = 0;
Uint32 uniformCount = 0;
Uint32 samplerCount = 0;
};
struct FrameDescriptorPools {
Vector<VkDescriptorPool> pools;
DescriptorPoolConfig config;
Uint32 activePool = 0;
};
struct VertexAttribKey {
Uint8 enabled = 0;
Uint8 size = 0;
Uint8 type = 0;
Uint8 normalized = 0;
Uint8 isInteger = 0;
Uint8 divisor = 0;
Uint16 pad = 0;
Uint32 stride = 0;
Uint64 offset = 0;
};
struct AttachmentInfo {
FramebufferAttachmentType type = FramebufferAttachmentType::None;
VkImageView view = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = 0;
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
ITextureObject* texture = nullptr;
RenderbufferObject* renderbuffer = nullptr;
};
struct BackendFramebufferObject {
VkExtent2D extent{0, 0};
Uint32 colorAttachmentCount = 0;
Bool hasDepth = false;
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
VkFormat depthAttachmentFormat = VK_FORMAT_UNDEFINED;
VkFormat stencilAttachmentFormat = VK_FORMAT_UNDEFINED;
Uint64 renderingCompatHash = 0;
Vector<AttachmentInfo> attachments;
Vector<VkFormat> colorAttachmentFormats;
Array<Int32, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)> attachmentIndex =
{};
FramebufferObject::FramebufferAttachmentVersionArray syncedAttachmentVersions = {0};
FramebufferObject::FramebufferAttachmentArray frontendDrawBuffers = {FramebufferAttachmentType::None};
Array<Int32, FramebufferObject::MAX_DRAW_BUFFERS> drawBufferAttachmentIndices = {};
FramebufferAttachmentType frontendReadBuffer = FramebufferAttachmentType::Color0;
Uint64 configHash = 0;
Uint16 objectVersion = 0;
Bool IsValid() const { return !attachments.empty() && extent.width > 0 && extent.height > 0; }
};
struct VulkanState {
UniquePtr<DV::VulkanContext> ctx;
UniquePtr<DV::SwapchainManager> swapchain;
UniquePtr<DV::ProgramManager> programMgr;
VkCommandPool commandPool = VK_NULL_HANDLE;
Vector<UniquePtr<DV::FrameContext>> frames;
Vector<FrameDescriptorPools> frameDescriptorPools;
Vector<BufferResource> stagingPool;
Vector<Vector<BufferResource>> stagingPending;
Uint32 currentFrame = 0;
Uint32 maxFramesInFlight = 2;
Bool initialized = false;
UintVec2 viewportSize{0, 0};
VkImage depthImage = VK_NULL_HANDLE;
VkDeviceMemory depthMemory = VK_NULL_HANDLE;
VkImageView depthView = VK_NULL_HANDLE;
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
Uint64 defaultRenderingCompatHash = 0;
BufferResource nullUbo;
TextureResource defaultTexture;
SamplerResource defaultSampler;
UnorderedMap<const BufferObject*, BufferResourceSet> buffers;
UnorderedMap<const ITextureObject*, TextureResource> textures;
UnorderedMap<const RenderbufferObject*, RenderbufferResource> renderbuffers;
UnorderedMap<const SamplerObject*, SamplerResource> samplers;
UnorderedMap<const ProgramObject*, ProgramResource> programs;
UnorderedMap<SharedPtr<MG_State::GLState::FramebufferObject>, SharedPtr<BackendFramebufferObject>> framebuffers;
Vector<PendingBufferCopyBatch> pendingBufferCopies;
Vector<StagingRing> stagingRings;
VkExtent2D activeExtent{0, 0};
Vector<VkImageView> activeColorViews;
Vector<VkImageLayout> activeColorLayouts;
Vector<VkFormat> activeColorFormats;
Vector<Int32> activeColorAttachmentIndices;
VkImageView activeDepthView = VK_NULL_HANDLE;
VkImageLayout activeDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageView activeStencilView = VK_NULL_HANDLE;
VkImageLayout activeStencilLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Uint32 activeSwapchainImageIndex = ~0u;
Uint32 activeColorAttachmentCount = 1;
Bool activeHasDepth = false;
VkFormat activeDepthFormat = VK_FORMAT_UNDEFINED;
VkFormat activeDepthAttachmentFormat = VK_FORMAT_UNDEFINED;
VkFormat activeStencilAttachmentFormat = VK_FORMAT_UNDEFINED;
Uint64 activeRenderingCompatHash = 0;
Uint64 activeRenderingConfigHash = 0;
SharedPtr<MG_State::GLState::FramebufferObject> activeStateFBO = nullptr;
SharedPtr<BackendFramebufferObject> activeBackendFBO = nullptr;
Bool activeIsDefault = true;
Bool activeDefaultColorWrites = true;
Uint64 recordingRenderingConfigHash = 0;
Bool cmdBufferBegun = false;
Bool recording = false;
Bool frameSubmitted = false;
FloatVec4 clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
Float clearDepth = 1.0f;
UnorderedMap<SharedPtr<FramebufferObject>, PendingClearInfo> pendingClears;
Vector<VkImageLayout> swapchainImageLayouts;
PFN_vkCmdBeginRendering pfnCmdBeginRendering = nullptr;
PFN_vkCmdEndRendering pfnCmdEndRendering = nullptr;
PFN_vkWaitSemaphores pfnWaitSemaphores = nullptr;
VkSemaphore timelineSemaphore = VK_NULL_HANDLE;
Uint64 timelineValue = 0;
};
void Present();
void FrameBegin();
void InitVulkan(ANativeWindow* window);
const VulkanState& GetVulkanState();
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);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance);
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex);
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance);
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect);
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void GenerateMipmap(GLenum target);
} // namespace MobileGL::MG_Backend::DirectVulkanTMP::TmpImpl
+2 -2
View File
@@ -52,8 +52,8 @@ namespace MobileGL::MG_Backend {
case BackendType::DirectGLES: case BackendType::DirectGLES:
pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>(); pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>();
break; break;
case BackendType::DirectVulkan: case BackendType::DirectVulkanTMP:
pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>(); pActiveBackendObject = MakeUnique<DirectVulkanTMP::BackendObject_DirectVulkanTMP>();
break; break;
case BackendType::Unknown: case BackendType::Unknown:
default: default:
+2 -2
View File
@@ -61,7 +61,7 @@ static void BM_CreateBufferObjectsAndBindBuffer(benchmark::State& state) {
BENCHMARK(BM_CreateBufferObjectsAndBindBuffer)->Unit(benchmark::kMillisecond)->UseRealTime(); BENCHMARK(BM_CreateBufferObjectsAndBindBuffer)->Unit(benchmark::kMillisecond)->UseRealTime();
static void BM_DeleteBufferObjects(benchmark::State& state) { static void BM_DeleteBufferObjects(benchmark::State& state) {
Initialize(); MG_Initialize();
std::vector<GLuint> buffers(BUFFER_COUNT); std::vector<GLuint> buffers(BUFFER_COUNT);
for (auto _ : state) { for (auto _ : state) {
@@ -179,7 +179,7 @@ static void BM_UpdateDataPartially(benchmark::State& state) {
BENCHMARK(BM_UpdateDataPartially)->Unit(benchmark::kMillisecond)->UseRealTime(); BENCHMARK(BM_UpdateDataPartially)->Unit(benchmark::kMillisecond)->UseRealTime();
int main(int argc, char** argv) { int main(int argc, char** argv) {
Initialize(); MG_Initialize();
benchmark ::MaybeReenterWithoutASLR(argc, argv); benchmark ::MaybeReenterWithoutASLR(argc, argv);
char arg0_default[] = "benchmark"; char arg0_default[] = "benchmark";
char* args_default = reinterpret_cast<char*>(arg0_default); char* args_default = reinterpret_cast<char*>(arg0_default);
+1 -2
View File
@@ -16,5 +16,4 @@ target_link_libraries(
${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)
+1 -3
View File
@@ -38,8 +38,6 @@ target_link_libraries(
) )
add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true) add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program) add_subdirectory(Program)
add_subdirectory(Buffer) add_subdirectory(Buffer)
add_subdirectory(Driver)
@@ -1,15 +0,0 @@
cmake_minimum_required(VERSION 3.24)
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
# MobileGL backend), so the same binary measures all three stacks.
if (NOT UNIX OR APPLE OR ANDROID)
return()
endif()
add_executable(DriverBench DriverBench.c)
target_link_libraries(DriverBench PRIVATE dl)
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)

Some files were not shown because too many files have changed in this diff Show More