Compare commits

..
241 changed files with 2008 additions and 35763 deletions
+13 -185
View File
@@ -9,45 +9,17 @@ fi
case_name="$1" case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}" fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}" python_bin="${PYTHON:-python3}"
# Fixture mirrors, tried in order before falling back to Git LFS. Override the mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
# 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 if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python python_bin=python
fi 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 \ fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \ --format fixture-files \
--case "${case_name}" \ --case "${case_name}" \
--fixture-root "${fixture_dir}")" --fixture-root "${fixture_dir}")"
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows). mapfile -t files <<< "${fixture_list}"
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
include="$(IFS=,; echo "${files[*]}")" include="$(IFS=,; echo "${files[*]}")"
if [ "${case_name}" = "OpenRA" ]; then if [ "${case_name}" = "OpenRA" ]; then
@@ -62,178 +34,34 @@ if [ "${case_name}" = "OpenRA" ]; then
exit 0 exit 0
fi fi
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
fetch_file_from_mirror() {
local file="$1"
local url="$2"
local metadata
local expected_oid
local expected_size
local tmp_file="${file}.tmp"
local attempt
local partial_size
local curl_status
local curl_auth
metadata="$(get_lfs_metadata "${file}")" || return 1
read -r expected_oid expected_size <<< "${metadata}"
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
if [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
elif [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
fi
fi
for ((attempt = 1; attempt <= download_attempts; attempt++)); do
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" -gt 0 ]; then
echo "Resuming mirror download for ${file} at byte ${partial_size} (attempt ${attempt}/${download_attempts})"
else
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
fi
curl_auth=()
if [ -n "${mirror_token}" ]; then
curl_auth=(--header "Authorization: token ${mirror_token}")
fi
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
else
curl_status=$?
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
partial_size=0
elif [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
partial_size=0
elif [ "${curl_status}" -eq 33 ]; then
echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
partial_size=0
fi
echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2
fi
if [ "${attempt}" -lt "${download_attempts}" ]; then
sleep "${retry_delay}"
fi
done
rm -f "${tmp_file}"
return 1
}
fetch_from_mirror() { fetch_from_mirror() {
mkdir -p "${fixture_dir}" mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do for file in "${files[@]}"; do
local name local name
local url local url
local base
local fetched=0
name="$(basename "${file}")" name="$(basename "${file}")"
for base in "${mirror_bases[@]}"; do url="${mirror_base%/}/${name}"
url="${base%/}/${name}" echo "Fetching trace fixture from mirror: ${url}"
echo "Fetching trace fixture from mirror: ${url}" if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
if fetch_file_from_mirror "${file}" "${url}"; then rm -f "${file}.tmp"
fetched=1
break
fi
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
return 1 return 1
fi fi
mv "${file}.tmp" "${file}"
done done
} }
if fetch_from_mirror; then if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}" echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else else
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}" echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
git lfs install --local git lfs install --local
git lfs pull --include="${include}" --exclude="" git lfs pull --include="${include}" --exclude=""
fi fi
for file in "${files[@]}"; do for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")" test -s "${file}"
read -r expected_oid expected_size <<< "${metadata}" if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}" echo "failed to hydrate LFS fixture: ${file}" >&2
exit 1
fi
done done
+32 -60
View File
@@ -227,8 +227,6 @@ jobs:
env: env:
AVD_NAME: mobilegl-ci AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -247,11 +245,11 @@ jobs:
with: with:
path: | path: |
${{ env.ANDROID_AVD_HOME }} ${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator /usr/local/lib/android/sdk/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools /usr/local/lib/android/sdk/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35 /usr/local/lib/android/sdk/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64 /usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }} key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Create AVD - name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true' if: steps.android-avd-cache.outputs.cache-hit != 'true'
@@ -276,8 +274,6 @@ jobs:
env: env:
AVD_NAME: mobilegl-ci AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 4 max-parallel: 4
@@ -292,7 +288,7 @@ jobs:
- name: Set Swap Space - name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0 uses: pierotofy/set-swap-space@v1.0
with: with:
swap-size-gb: 8 swap-size-gb: 16
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -328,11 +324,11 @@ jobs:
with: with:
path: | path: |
${{ env.ANDROID_AVD_HOME }} ${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator /usr/local/lib/android/sdk/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools /usr/local/lib/android/sdk/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35 /usr/local/lib/android/sdk/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64 /usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }} key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Download retrace APK - name: Download retrace APK
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@@ -382,51 +378,27 @@ jobs:
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush) extra_retrace_args+=(--coherent-as-flush)
fi fi
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
run_retrace() { --apk-file "${apk_file}" \
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \ --package top.mobilegl.plugin.trace \
--apk-file "${apk_file}" \ --backend "${{ matrix.backend.name }}" \
--package top.mobilegl.plugin.trace \ --result-root android-retrace-result \
--backend "${{ matrix.backend.name }}" \ --fixture-root android-retrace-fixture \
--result-root android-retrace-result \ --case "${{ matrix.case.name }}" \
--fixture-root android-retrace-fixture \ --trace-archive "${{ matrix.case.trace_archive }}" \
--case "${{ matrix.case.name }}" \ --trace-file "${{ matrix.case.trace_file }}" \
--trace-archive "${{ matrix.case.trace_archive }}" \ --golden "${{ matrix.case.golden }}" \
--trace-file "${{ matrix.case.trace_file }}" \ --alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
--golden "${{ matrix.case.golden }}" \ --target-call "${{ matrix.case.target_call }}" \
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \ --width "${{ matrix.case.width }}" \
--target-call "${{ matrix.case.target_call }}" \ --height "${{ matrix.case.height }}" \
--width "${{ matrix.case.width }}" \ --ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
--height "${{ matrix.case.height }}" \ --crop-x "${{ matrix.case.crop_x }}" \
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \ --crop-y "${{ matrix.case.crop_y }}" \
--crop-x "${{ matrix.case.crop_x }}" \ --crop-width "${{ matrix.case.crop_width }}" \
--crop-y "${{ matrix.case.crop_y }}" \ --crop-height "${{ matrix.case.crop_height }}" \
--crop-width "${{ matrix.case.crop_width }}" \ --timeout-seconds "${{ matrix.case.timeout_seconds }}" \
--crop-height "${{ matrix.case.crop_height }}" \ "${extra_retrace_args[@]}"
--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 - name: Collect retrace summary inputs
if: always() if: always()
+5 -15
View File
@@ -107,7 +107,6 @@ jobs:
--exclude='build.ninja' \ --exclude='build.ninja' \
--exclude='cmake_install.cmake' \ --exclude='cmake_install.cmake' \
-czf ci-artifacts/mobilegl-linux-runtime.tgz \ -czf ci-artifacts/mobilegl-linux-runtime.tgz \
"${BUILD_DIR}/CTestTestfile.cmake" \
"${BUILD_DIR}/MobileGL/MG_Test" \ "${BUILD_DIR}/MobileGL/MG_Test" \
"${BUILD_DIR}/MobileGL/MG_Benchmark" \ "${BUILD_DIR}/MobileGL/MG_Benchmark" \
"${SHARED_LIBS[@]}" "${SHARED_LIBS[@]}"
@@ -157,12 +156,12 @@ jobs:
PY PY
- name: Test - name: Test
working-directory: build-linux working-directory: build-linux/MobileGL/MG_Test
run: | run: |
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L unit --no-tests=error ctest -V
else else
ctest --output-on-failure -L unit --no-tests=error ctest --output-on-failure
fi fi
benchmark: benchmark:
@@ -203,8 +202,8 @@ jobs:
PY PY
- name: Benchmark - name: Benchmark
working-directory: build-linux working-directory: build-linux/MobileGL/MG_Benchmark
run: ctest -V -C Release -L benchmark --no-tests=error run: ctest -V -C Release
build-retrace: build-retrace:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -455,15 +454,6 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1 export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi 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 }}$' ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
- name: Upload actual image - name: Upload actual image
-2
View File
@@ -25,5 +25,3 @@ MobileGL/MG*/cmake-build*
/android-plugin/app/src/trace/jniLibs /android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties /android-plugin/local.properties
tools/trace_replay/work/ tools/trace_replay/work/
__pycache__/
*.py[cod]
+1 -54
View File
@@ -188,12 +188,8 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.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/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -303,13 +299,6 @@ if (ANDROID)
) )
endif() endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
set(MOBILEGL_LINK_LIBRARIES set(MOBILEGL_LINK_LIBRARIES
glslang::glslang glslang::glslang
spirv-cross-c spirv-cross-c
@@ -342,14 +331,6 @@ 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
@@ -392,18 +373,6 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach() endforeach()
endif() 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}
@@ -455,21 +424,8 @@ if (ANDROID)
endif() endif()
if (APPLE AND NOT MOBILEGL_IOS) if (APPLE AND NOT MOBILEGL_IOS)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa" "-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore" "-framework QuartzCore"
"-framework Foundation" "-framework Foundation"
"-framework OpenGL" "-framework OpenGL"
@@ -477,7 +433,6 @@ if (APPLE AND NOT MOBILEGL_IOS)
if(TARGET ${CMAKE_PROJECT_NAME}_s) if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa" "-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore" "-framework QuartzCore"
"-framework Foundation" "-framework Foundation"
"-framework OpenGL" "-framework OpenGL"
@@ -520,15 +475,6 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
endif () endif ()
if (NOT ANDROID) 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()
@@ -538,6 +484,7 @@ if (NOT ANDROID)
endif() endif()
if (MOBILEGL_BUILD_TRACE_REPLAY) if (MOBILEGL_BUILD_TRACE_REPLAY)
enable_testing()
add_subdirectory(tools/trace_replay) add_subdirectory(tools/trace_replay)
endif() endif()
endif() endif()
+1 -31
View File
@@ -14,21 +14,12 @@ 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, 7, 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; 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,
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init() // Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share // (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
// one truthy rule: the variable is set, non-empty, not "0", and not "false" // one truthy rule: the variable is set, non-empty, not "0", and not "false"
@@ -70,27 +61,6 @@ namespace MobileGL::MG_Config {
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator // per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch). // (negative control / driver-bug escape hatch).
Bool DisableUboRing = false; Bool DisableUboRing = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
}; };
extern FeaturesTable Features; extern FeaturesTable Features;
} // namespace MobileGL::MG_Config } // namespace MobileGL::MG_Config
-16
View File
@@ -86,17 +86,6 @@ namespace MobileGL::MG_ConfigLoader {
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second); return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
} }
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) { inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key); auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) { if (it == acceptedEnvVariablesMap->end()) {
@@ -133,11 +122,6 @@ namespace MobileGL::MG_ConfigLoader {
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
} }
inline void InitBackendType() { inline void InitBackendType() {
-1
View File
@@ -34,7 +34,6 @@
#define MOBILEGL_EGL_API MOBILEGL_API #define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API #define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API #define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= // // ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL #ifndef MOBILEGL_LOG_ACTIVE_LEVEL
+1 -7
View File
@@ -14,13 +14,7 @@ namespace MobileGL {
} // 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
+33 -47
View File
@@ -9,25 +9,14 @@
#include "Init.h" #include "Init.h"
#include "Config.h" #include "Config.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_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 <atomic>
#include <mutex>
namespace MobileGL { namespace MobileGL {
namespace { namespace {
std::atomic<Bool> g_isInitialized = false; Bool g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) { void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) { if (!g_isInitialized) {
@@ -38,12 +27,6 @@ namespace MobileGL {
MGLOG_I("MobileGL closing..."); MGLOG_I("MobileGL closing...");
} }
glslang::FinalizeProcess(); glslang::FinalizeProcess();
// GL syncs die with their contexts, and every context is gone by the
// time full teardown runs: drain the live-sync registry while the
// backend function table can still release the backend handles (and
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
MG_Backend::pActiveBackendObject.reset(); MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
MG_State::pEGLContext.reset(); MG_State::pEGLContext.reset();
@@ -81,37 +64,40 @@ namespace MobileGL {
MGLOG_I("MobileGL initialized"); MGLOG_I("MobileGL initialized");
} }
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() { void Destroy() {
DestroyImpl(true); DestroyImpl(true);
} }
// MobileGL's lifecycle is owned entirely by the host-API layers #if defined(__linux__) || defined(__APPLE__)
// (EGL/WGL/CGL): initialization happens lazily on the first entry point __attribute__((constructor)) static void AutoInit() {
// via EnsureInitialized(), and full teardown happens deterministically Initialize();
// when the last EGL display is terminated with nothing current (EGLImpl }
// calls Destroy()). There is intentionally no backend-initializing static
// constructor, no static destructor, and no DllMain: the global singletons __attribute__((destructor)) static void AutoDestroy() {
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits if (MG_Config::Features.TraceSkipAutodestroy) {
// without eglTerminate simply leaks them to the OS instead of running return;
// backend destructors during static teardown. macOS has a lightweight }
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend #if defined(__APPLE__)
// initialization still enters here from the first hooked CGL context. // macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
} // namespace MobileGL } // namespace MobileGL
-7
View File
@@ -11,13 +11,6 @@
namespace MobileGL { namespace MobileGL {
void Initialize(); void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// full backend initialization never depends on ELF/DLL static constructors,
// and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized();
void Destroy(); void Destroy();
namespace MG_Util::Debug { namespace MG_Util::Debug {
+1 -40
View File
@@ -220,15 +220,6 @@ namespace MobileGL {
// and leave the query readable later. // and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds); Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query); void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
}; };
struct GlobalBackendFunctionsTable { struct GlobalBackendFunctionsTable {
@@ -239,26 +230,8 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval); 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 AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f; Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f; Float SmoothLineWidthRangeMin = 1.0f;
@@ -296,9 +269,6 @@ namespace MobileGL {
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8; Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
@@ -309,28 +279,19 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f; Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0; 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; Bool SupportsWideLines = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0; Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0; Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false; Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
}; };
enum class WindowBackend { enum class WindowBackend {
Android, Android,
X11, X11,
MetalLayer, MetalLayer,
Win32, // Handle is an HWND // TODO: Wayland, Windows, etc.
// TODO: Wayland, etc.
WindowBackendCount, WindowBackendCount,
Unknown = -1 Unknown = -1
}; };
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h" #include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend { namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject>& pActiveBackendObject; extern UniquePtr<BackendObject> pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable; extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend } // namespace MobileGL::MG_Backend
@@ -20,7 +20,6 @@
#include <MG_Util/Texture/TextureFormatProcessor.h> #include <MG_Util/Texture/TextureFormatProcessor.h>
#include <Config.h> #include <Config.h>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <format> #include <format>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
@@ -604,11 +603,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor .ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo = .RendererGLInfo =
{ {
.TargetGLVersion = {3, 3, 0}, // GL target version .TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled // Baseline advertisement (no timer queries yet); reconciled once
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. // the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
.Extensions = BuildAdvertisedExtensions(false, false), .Extensions = BuildAdvertisedExtensions(false),
.IsCompatibilityProfile = false // Is Compatibility Profile .IsCompatibilityProfile = false // Is Compatibility Profile
}, },
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -628,9 +627,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the // thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole // advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent. // list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) { void UpdateAdvertisedTimerQueryExtension() {
MutableRendererInfo().RendererGLInfo.Extensions = MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
} }
} // namespace } // namespace
@@ -674,11 +672,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false; return false;
} }
DirectGLES::SetGLESCapabilities(m_GLESCapabilities); DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and // Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on // reconcile the E_GL_ARB_timer_query advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension // UpdateAdvertisedTimerQueryExtension for why it cannot happen when
// list is first built). // the extension list is first built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy); UpdateAdvertisedTimerQueryExtension();
UpdateDynamicBackendParameters(); UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities()); PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities());
@@ -702,10 +700,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if ((handle.Backend != WindowBackend::Android && if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::MetalLayer) ||
handle.Backend != WindowBackend::Win32) ||
!handle.Handle) { !handle.Handle) {
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows"); MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
return false; return false;
} }
@@ -821,14 +818,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo(); return MutableRendererInfo();
} }
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) { Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader, V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_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_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_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_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
@@ -839,14 +836,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query); extensions.push_back(E_GL_ARB_timer_query);
} }
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
// get plain trilinear.
if (anisotropicFilteringSupported) {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
}
return extensions; return extensions;
} }
@@ -935,16 +924,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery; funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery; funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp; funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs; funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
} }
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
// and share the handle-based result/delete entries, which must exist even
// when the timer-query group above is disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true; funcsTableInitialized = true;
} }
return funcsTable; return funcsTable;
@@ -954,15 +938,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_dynamicParameters; return m_dynamicParameters;
} }
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
const MG_External::GLESCapabilities& capabilities) {
m_GLESCapabilities = capabilities;
UpdateDynamicBackendParameters();
}
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax; m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin; m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
@@ -1016,21 +993,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
const Int maxSupportedTextureUnits = const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxImageUnits = m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits);
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms;
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0); m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms;
const auto clampStageImageUniforms = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
m_dynamicParameters.MaxCombinedImageUniforms});
};
m_dynamicParameters.MaxVertexImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxGeometryImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
m_dynamicParameters.MaxFragmentImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
m_dynamicParameters.MaxComputeImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
@@ -1040,52 +1005,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax; m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits; m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
? m_GLESCapabilities.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset =
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset =
m_GLESCapabilities.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits =
m_GLESCapabilities.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_dynamicParameters.SupportsWideLines =
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f; m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
return haystack.find(needle) != String::npos;
});
};
const String vendorAndRenderer =
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) {
// Check software rasterizers first: ANGLE-on-llvmpipe reports both.
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
} else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
} else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
} else if (containsAny(vendorAndRenderer, {"NVIDIA"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
} else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
} else if (containsAny(vendorAndRenderer, {"Intel"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
} else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
} else {
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
}
} }
const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const { const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
@@ -41,7 +41,6 @@ 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();
@@ -67,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity(); const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS)) // The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering are (or are not) usable. // for a device whose timer queries are (or are not) usable. The
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside. // MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported); Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an // Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up // initialized backend returns from GetBackendAPIVersionString (and that ends up
File diff suppressed because it is too large Load Diff
@@ -19,10 +19,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);
@@ -130,11 +126,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery(); BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query); void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp(); BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query); Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for // Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it // null or stale-generation handles IS final: the frontend may cache it
File diff suppressed because it is too large Load Diff
+4 -171
View File
@@ -15,7 +15,6 @@
#include "MG_State/GLState/TextureState/TextureEnum.h" #include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
@@ -178,21 +177,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER. // glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id); void BindBufferId(GLenum target, Uint id);
void InvalidateArrayBufferBindingCache(); void InvalidateArrayBufferBindingCache();
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
// upload site routes its binding through these so the shadow always matches
// the driver; the resting state between operations is 0, which keeps any
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
// and a recycled name matching the shadow would false-skip the rebind) and
// invalidated on MakeCurrent (context may reset).
void BindPixelPackBufferId(Uint id);
void BindPixelUnpackBufferId(Uint id);
void InvalidatePixelBufferBindingCaches();
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
// client-attribute staging buffers): scrub every buffer-binding shadow that
// could false-skip when the name is recycled.
void NoteBufferIdDeleted(Uint id);
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on // Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the // GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/ // (id, range) already at that index matches, like the array-buffer/texture/
@@ -270,48 +254,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl { namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) { inline Bool IsSupportedTextureTarget(TextureTarget target) {
// Rectangle textures need non-normalized sampling ES cannot express; everything else is if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray)
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and return false;
// coordinate padding for 1D/1D-array shaders. return true;
return target != TextureTarget::TextureRectangle;
}
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
default:
return target;
}
}
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
}
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
return GL_TEXTURE_2D;
case TextureUploadTarget::Texture1DArray:
return GL_TEXTURE_2D_ARRAY;
default:
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
}
}
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
// moves the layer count into depth.
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {texelSize.x(), 1, texelSize.y()};
}
return texelSize;
} }
inline Bool IsMultisampleTextureTarget(TextureTarget target) { inline Bool IsMultisampleTextureTarget(TextureTarget target) {
@@ -347,12 +293,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendTextureObject { class BackendTextureObject {
public: public:
BackendTextureObject(); BackendTextureObject();
// Deletes the GL texture (frontend glDeleteTextures used to leak every
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
// shadows so a recycled name or heap address cannot false-skip a rebind.
~BackendTextureObject();
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
@@ -364,9 +304,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RecreateBackendTexture(); void RecreateBackendTexture();
Uint m_backendTextureId = 0; Uint m_backendTextureId = 0;
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
Bool m_isInitialized = false; Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false; Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false; Bool m_backendStorageImmutable = false;
@@ -391,9 +328,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache; g_boundTexturesCache;
extern Uint g_activeTextureUnit; extern Uint g_activeTextureUnit;
// Bumped when the backend ES context is destroyed; texture ids stamped with
// an older generation belong to a dead context and must not be deleted.
extern Uint g_textureContextGeneration;
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl { namespace FramebufferImpl {
@@ -402,10 +336,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendFramebufferObject(); BackendFramebufferObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget); FramebufferTarget asTarget);
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
void InvalidateSyncedState(); void InvalidateSyncedState();
Uint GetBackendFramebufferId() const { return m_backendFBOId; } Uint GetBackendFramebufferId() const { return m_backendFBOId; }
void Bind(FramebufferTarget target) const; void Bind(FramebufferTarget target) const;
@@ -439,105 +369,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject> extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects; g_backendFramebufferObjects;
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions; extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
// must re-sync it even when the binding-slot version has not moved.
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects;
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
// save/restore the current binding without a glGetIntegerv round-trip (that
// query forces a driver pipeline sync) and so redundant rebinds no-op.
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
// the driver once. Invalidated on MakeCurrent (context may reset).
// GL_FRAMEBUFFER binds both targets.
void BindFramebufferId(GLenum fbTarget, Uint id);
Uint CurrentFramebufferBinding(FramebufferTarget target);
void InvalidateFramebufferBindingCache();
} // namespace FramebufferImpl } // namespace FramebufferImpl
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
// texture) is detached exactly when a later use of another aspect would
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
// incomplete and silently degraded later readbacks).
namespace ScratchFBOImpl {
struct ScratchFramebuffer {
Uint id = 0;
// false => attachment state unknown; scrub every point on next use.
// A fresh FBO starts with nothing attached, so creation sets it true.
Bool attachmentsKnown = false;
Uint colorTex = 0;
GLenum colorTarget = 0;
GLint colorLevel = 0;
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
Uint depthTex = 0;
GLenum depthTarget = 0;
GLint depthLevel = 0;
Bool depthHasStencil = false;
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
GLenum readBuffer = 0;
GLenum drawBuffer = 0;
};
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
// Returns the GL id, generating it if needed (requires a current ES context).
Uint EnsureId(ScratchFramebuffer& fb);
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
// target the READ/DRAW binding respectively). Each Ensure* performs the
// minimal detach/attach set and keeps the shadow in sync; a failed attach
// records the point as detached so the completeness check fails instead of
// silently reading a stale attachment.
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
Bool withStencil);
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
// binding while respecifying texture storage). Attachment is set once at
// creation and never changes.
Uint EnsureCompleteTinyFramebufferId();
// A backend texture id is being deleted or respecified: a scratch FBO still
// referencing it would hold a dangling attachment (ES only auto-detaches
// from the *bound* framebuffer), and a recycled name could false-skip a
// re-attach; force a full scrub on next use.
void NoteTextureIdDeleted(Uint textureId);
// The ES context (and the scratch FBO ids with it) is going away.
void OnBackendContextDestroyed();
} // namespace ScratchFBOImpl
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
// pins the driver to the shadow by writing all fields once. Invalidated on
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
// frontend context state instead.
namespace PixelStoreImpl {
struct PackState {
GLint Alignment = 4;
GLint RowLength = 0;
GLint SkipRows = 0;
GLint SkipPixels = 0;
Bool operator==(const PackState& o) const {
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
SkipPixels == o.SkipPixels;
}
};
void ApplyPackState(const PackState& desired);
PackState CurrentPackState();
void InvalidatePackStateCache();
} // namespace PixelStoreImpl
// Image uniforms take their unit from the layout(binding=N) qualifier baked into // Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i. // assigned through glUniform1i.
-407
View File
@@ -18,10 +18,6 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Math/SmallFloat.h>
#include <cmath>
namespace MobileGL::MG_Backend::DirectGLES { namespace MobileGL::MG_Backend::DirectGLES {
namespace { namespace {
@@ -452,407 +448,4 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
} }
} // namespace Utils } // namespace Utils
// ---- Client-format readback conversion helpers -------------------------------------------------
// ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack
// it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so
// unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror
// the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers).
namespace ReadbackImpl {
using MG_Util::DecodeHalfBitsToFloat;
using MG_Util::EncodeFloatToHalfBits;
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) {
switch (format) {
case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true;
case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true;
// Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue):
// the destination holds one component sourced from the named channel of the wide RGBA read.
// GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the
// legacy alpha-texture upload hack.
case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true;
case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true;
case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true;
case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true;
case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true;
case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true;
case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true;
case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true;
case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true;
case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true;
case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true;
case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true;
case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true;
case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true;
case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true;
case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true;
default:
return false;
}
}
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) {
switch (type) {
// Non-REV types pack the first format component starting at the most significant bit,
// *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5:
// R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15).
case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true;
case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true;
case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true;
case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true;
case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true;
case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true;
case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true;
case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true;
case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true;
// Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit
// exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field.
case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true;
case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true;
default:
return false;
}
}
SizeT GetReadbackComponentSize(GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
return packedLayout.byteSize;
}
switch (type) {
case GL_UNSIGNED_BYTE:
case GL_BYTE:
return 1;
case GL_UNSIGNED_SHORT:
case GL_SHORT:
case GL_HALF_FLOAT:
return 2;
case GL_UNSIGNED_INT:
case GL_INT:
case GL_FLOAT:
return 4;
default:
return 0;
}
}
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
if (packedLayout.fieldCount != mapping.channelCount) {
return 0; // 3-field packed types pair with 3-component formats only, 4 with 4
}
if (mapping.isInteger && packedLayout.isFloatPacked) {
return 0; // packed-float RGB types never pair with integer formats
}
return packedLayout.byteSize;
}
if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) {
return 0;
}
const SizeT componentSize = GetReadbackComponentSize(type);
return componentSize == 0 ? 0 : static_cast<SizeT>(mapping.channelCount) * componentSize;
}
namespace {
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
switch (byteSize) {
case 1: {
const auto out = static_cast<Uint8>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
case 2: {
const auto out = static_cast<Uint16>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
default:
Memcpy(dst, &word, sizeof(word));
break;
}
}
} // namespace
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); }
Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); }
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); }
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
const Bool isPacked = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
for (SizeT col = 0; col < width; ++col) {
const Uint8* srcPixel = src + col * srcPixelBytes;
Uint8* dstPixel = dst + col * dstPixelBytes;
if (mapping.isInteger) {
Int64 srcValues[4];
for (Int c = 0; c < 4; ++c) {
srcValues[c] = wideType == GL_INT
? static_cast<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
: static_cast<Int64>(reinterpret_cast<const Uint32*>(srcPixel)[c]);
}
if (isPacked) {
// Integer sources clamp each component to the unsigned range of its field
// (GL 3.3 section 4.3.1 final conversion).
Uint32 word = 0;
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1;
const auto v = static_cast<Uint32>(
std::clamp<Int64>(srcValues[mapping.sourceChannel[ch]], 0, fieldMax));
word |= v << packedLayout.shift[ch];
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Int64 v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
break;
case GL_BYTE: {
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out =
static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
} else {
Float srcValues[4];
switch (wideType) {
case GL_UNSIGNED_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = static_cast<Float>(srcPixel[c]) / 255.0f;
}
break;
case GL_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
}
break;
case GL_UNSIGNED_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] =
static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
}
break;
case GL_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
}
break;
case GL_HALF_FLOAT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
}
break;
default: // GL_FLOAT
for (Int c = 0; c < 4; ++c) {
srcValues[c] = reinterpret_cast<const Float*>(srcPixel)[c];
}
break;
}
if (isPacked) {
Uint32 word = 0;
if (packedLayout.isFloatPacked) {
const Float fields[3] = {srcValues[mapping.sourceChannel[0]],
srcValues[mapping.sourceChannel[1]],
srcValues[mapping.sourceChannel[2]]};
word = type == GL_UNSIGNED_INT_5_9_9_9_REV
? EncodeSharedExponentRGB9E5(fields)
: (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) |
(EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) |
(EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]);
} else {
// Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field.
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const auto fieldMax = static_cast<Float>((1u << packedLayout.width[ch]) - 1u);
const auto v = static_cast<Uint32>(std::llround(
std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax));
word |= v << packedLayout.shift[ch];
}
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Float v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent =
static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
break;
case GL_BYTE: {
const auto out =
static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out =
static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out =
static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out = static_cast<Int32>(
std::llround(static_cast<Double>(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_FLOAT:
Memcpy(dstComponent, &v, sizeof(v));
break;
case GL_HALF_FLOAT: {
const Uint16 out = EncodeFloatToHalfBits(v);
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
}
}
}
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
return true;
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES } // namespace MobileGL::MG_Backend::DirectGLES
-53
View File
@@ -45,59 +45,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
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);
@@ -18,7 +18,6 @@
#include "MG_Util/Texture/TextureFormatProcessor.h" #include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h> #include <Config.h>
#include <cmath>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -141,20 +140,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureInternalFormat::RGB: case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8: case TextureInternalFormat::RGB8:
return TextureInternalFormat::RGBA8; 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: case TextureInternalFormat::SRGB8:
return TextureInternalFormat::SRGB8Alpha8; return TextureInternalFormat::SRGB8Alpha8;
case TextureInternalFormat::RGB8Snorm: case TextureInternalFormat::RGB8Snorm:
@@ -412,9 +397,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (!handle.Handle || (handle.Backend != WindowBackend::Android && if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::MetalLayer)) {
handle.Backend != WindowBackend::Win32)) { MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false; return false;
} }
@@ -470,9 +454,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on. // treat them as signaled/available with zero results from here on.
BumpRendererGeneration(); BumpRendererGeneration();
pVulkanRenderer.reset(); 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(); BackendObject::ReleaseEGLResources();
} }
@@ -482,9 +463,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on. // treat them as signaled/available with zero results from here on.
BumpRendererGeneration(); BumpRendererGeneration();
pVulkanRenderer.reset(); 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 { const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
@@ -510,15 +488,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.TargetGLSLVersion = {4, 6, 0}, .TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a // Baseline advertisement (no shader subgroup, no timer queries); a
// live backend reconciles its copy in UpdateAdvertisedExtensions. // live backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false), .Extensions = BuildAdvertisedExtensions(false, false),
.IsCompatibilityProfile = false .IsCompatibilityProfile = false
}, },
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}}; .StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo; return rendererInfo;
} }
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
Bool anisotropicFilteringSupported) {
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader, V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -526,11 +503,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, 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_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_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_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, 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_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
E_GL_ARB_explicit_attrib_location};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) { if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup); extensions.push_back(E_GL_KHR_shader_subgroup);
} }
@@ -540,13 +516,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query); 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; return extensions;
} }
@@ -636,15 +605,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs; 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; funcsTableInitialized = true;
} }
return funcsTable; return funcsTable;
@@ -670,8 +630,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// run without a renderer; no timer query is advertised then. Rebuilding // run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent. // the whole list keeps re-runs idempotent.
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions( m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(), m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
} }
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -721,11 +680,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax; 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.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax; m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity; m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
@@ -778,24 +732,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits = m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0); m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
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 = const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
@@ -807,23 +746,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax; m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits; m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
? m_vulkanCaps.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset =
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits =
m_vulkanCaps.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines; m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
m_dynamicParameters.MaxShaderStorageBlockSize = m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -843,32 +765,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps.MaxShaderStorageBlockSize, m_vulkanCaps.MaxShaderStorageBlockSize,
m_dynamicParameters.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 } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -73,8 +73,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and // a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass // MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless). // the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
Bool anisotropicFilteringSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact // Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that // string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -20,8 +20,7 @@
#include <spirv_reflect.h> #include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
// Leak-at-exit storage; see GlobalObjects.cpp. UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
namespace { namespace {
// Generation of the live VulkanRenderer instance, mirroring // Generation of the live VulkanRenderer instance, mirroring
@@ -61,12 +60,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
struct ProgramResourceCache { struct ProgramResourceCache {
// Lifetime id of the program the cached reflection belongs to. GL names are
// recycled (IndexGenerator hands freed indices straight back), and a
// recreated program's backendStateVersion restarts at the same small values,
// so the version alone can collide; the never-reused lifetime id makes the
// slot's ownership unambiguous.
Uint64 programLifetimeId = 0;
Uint32 backendStateVersion = 0; Uint32 backendStateVersion = 0;
Vector<StorageBlockResource> storageBlocks; Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables; Vector<BufferVariableResource> bufferVariables;
@@ -88,11 +81,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 baseInstance = 0; Uint32 baseInstance = 0;
}; };
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
// checked against the program's lifetime id before it is served (see
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
// ClearProgramResourceCaches.
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches; UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
@@ -153,19 +141,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) { ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
auto& cache = g_programResourceCaches[program.GetExternalIndex()]; auto& cache = g_programResourceCaches[program.GetExternalIndex()];
const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 backendStateVersion = program.GetBackendStateVersion(); const Uint32 backendStateVersion = program.GetBackendStateVersion();
// The lifetime id must match too: a new program that reuses a deleted if (cache.backendStateVersion == backendStateVersion &&
// program's name and happens to land on the same backendStateVersion (both
// count from zero) would otherwise be served the dead program's reflection.
if (cache.programLifetimeId == programLifetimeId &&
cache.backendStateVersion == backendStateVersion &&
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) { (!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
return cache; return cache;
} }
cache = {}; cache = {};
cache.programLifetimeId = programLifetimeId;
cache.backendStateVersion = backendStateVersion; cache.backendStateVersion = backendStateVersion;
Vector<SpvReflectShaderModule> modules; Vector<SpvReflectShaderModule> modules;
@@ -383,15 +365,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} // namespace } // namespace
void ClearProgramResourceCaches() {
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
// calls are serialized in this codebase (contexts migrate threads but never
// run concurrently), so no other thread can be inside the unsynchronized map.
// Live programs in another context self-heal: their entry rebuilds from the
// retained generated SPIR-V on the next resource query.
g_programResourceCaches.clear();
}
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) { GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
auto& cache = GetProgramResourceCache(program); auto& cache = GetProgramResourceCache(program);
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(), const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
@@ -1250,76 +1223,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->Clear(mask); pVulkanRenderer->Clear(mask);
} }
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
// whose synthesized index list revisits the first vertex at the end.
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
DrawIndexedCmd payload{};
payload.mode = GL_LINE_STRIP;
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
payload.indexBufferView.forceClientMemory = true;
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
payload.params.instanceCount = 1;
payload.params.vertexOffset = basevertex;
pVulkanRenderer->DrawElements(payload);
}
// Resolve a DrawElements index list (bound element-array buffer or client
// memory) into uint32 values with the loop-closing first index appended.
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
Vector<Uint32>& outIndices) {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0 || count < 2) {
return false;
}
const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices);
const SizeT bufferSize = indexBufferShared->GetSize();
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
return false;
}
indexBufferShared->SyncPersistentMappedRange();
indexBytes = indexBufferShared->MappedData() + offset;
} else {
indexBytes = static_cast<const Uint8*>(indices);
if (indexBytes == nullptr) {
return false;
}
}
outIndices.resize(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
switch (indexSize) {
case 1: outIndices[i] = indexBytes[i]; break;
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
}
}
outIndices[count] = outIndices[0];
return true;
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) { void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) {
if (count < 2) {
return;
}
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
closedIndices[i] = static_cast<Uint32>(first + i);
}
closedIndices[count] = static_cast<Uint32>(first);
DrawLineLoopAsIndexedStrip(closedIndices, 0);
return;
}
DrawCmd payload{}; DrawCmd payload{};
payload.mode = mode; payload.mode = mode;
payload.params.firstVertex = first; payload.params.firstVertex = first;
@@ -1332,14 +1239,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, 0);
}
return;
}
DrawIndexedCmd payload{}; DrawIndexedCmd payload{};
payload.mode = mode; payload.mode = mode;
payload.indexBufferView.indexType = type; payload.indexBufferView.indexType = type;
@@ -1408,13 +1307,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
}
return;
}
DrawIndexedCmd payload{}; DrawIndexedCmd payload{};
payload.mode = mode; payload.mode = mode;
payload.indexBufferView.indexType = type; payload.indexBufferView.indexType = type;
@@ -1564,12 +1456,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// records are shared (SharedPtr) with the owning pool's pending list, // records are shared (SharedPtr) with the owning pool's pending list,
// so deleting the query while results are still in flight is safe. // so deleting the query while results are still in flight is safe.
struct VulkanTimerQuery { struct VulkanTimerQuery {
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
Kind kind = Kind::Timer;
SharedPtr<VkTimerQueryManager::TimestampRecord> begin; SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
SharedPtr<VkTimerQueryManager::TimestampRecord> end; SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Renderer generation the records were written under (see // Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available // g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame // with a final zero result: the records' pool indices and frame
@@ -1659,26 +1547,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ever be produced, so resolve with a final 0. // ever be produced, so resolve with a final 0.
return true; return true;
} }
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
Uint64 samples = 0;
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
return false;
}
query->occlusionSlots.clear(); // slots are recycled by the resolve
*outNanoseconds = samples;
return true;
}
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) {
return false;
}
*outNanoseconds = primitives;
return true;
}
// With wait, mirrors ClientWaitSync: a query ended this frame cannot // With wait, mirrors ClientWaitSync: a query ended this frame cannot
// complete until Present submits the commands, so the wait refuses to // complete until Present submits the commands, so the wait refuses to
// block on the current unsubmitted serial. Returning false keeps the // block on the current unsubmitted serial. Returning false keeps the
@@ -1711,47 +1579,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
delete static_cast<VulkanTimerQuery*>(handle); delete static_cast<VulkanTimerQuery*>(handle);
} }
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
}
BackendQueryHandle BeginOcclusionQuery() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = VulkanTimerQuery::Kind::Occlusion;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndOcclusionQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
}
Int64 GetGpuTimestampNs() { Int64 GetGpuTimestampNs() {
// Vulkan cannot synchronously sample the GPU clock: timestamps only // Vulkan cannot synchronously sample the GPU clock: timestamps only
// exist as vkCmdWriteTimestamp results read back later, and // exist as vkCmdWriteTimestamp results read back later, and
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h" #include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer>& pVulkanRenderer; extern UniquePtr<VulkanRenderer> pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's // Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever // g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -23,12 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetRendererGeneration(); Uint64 GetRendererGeneration();
void BumpRendererGeneration(); 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 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);
@@ -123,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only while a live renderer exists whose device can actually time. // only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported(); Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery(); BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query); void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp(); BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query); Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -16,19 +16,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = device; m_device = device;
m_commandPool = commandPool; m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE); Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{}; VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool; allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount * 2; allocInfo.commandBufferCount = frameCount;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()); VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
return result; return result;
} }
for (Uint32 i = 0; i < frameCount; ++i) { for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i]; m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
} }
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -48,10 +47,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) { void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size()); const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE); Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) { for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer; commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
} }
for (Uint32 i = 0; i < frameCount; ++i) { for (Uint32 i = 0; i < frameCount; ++i) {
@@ -62,7 +60,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& frame : m_frames) { for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame); FreeRetiredCommandBuffers(frame);
} }
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data()); vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
} }
m_frames.clear(); m_frames.clear();
currentFrameIndex = 0; currentFrameIndex = 0;
@@ -89,8 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size()); currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false; GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false; GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
} }
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags, VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
@@ -122,41 +118,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = true; 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) { VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device); DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) { if (swapchainImageCount == 0) {
@@ -189,30 +150,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) { Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent(); auto& frame = GetCurrent();
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) { if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false; return false;
} }
// The barrier belongs in the frame's own recording. Bailing out because auto& commandBuffer = BeginCommandRecording();
// 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{}; VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
@@ -231,9 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier); nullptr, 0, nullptr, 1, &presentBarrier);
if (openedRecording) { EndCommandRecording();
EndCommandRecording();
}
return true; return true;
} }
@@ -241,27 +182,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex) const { Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent(); const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active"); 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); AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{}; SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore; packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex]; packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U; packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore; packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask; packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = commandBufferCount; packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr; packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.signalSemaphoreCount = 1; packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore; packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet; return packet;
@@ -296,21 +227,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence, result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex); &outImageIndex);
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and if (result != VK_SUCCESS) {
// 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; return result;
} }
frame.imageAvailableSemaphoreConsumed = false; frame.imageAvailableSemaphoreConsumed = false;
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence); return 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 { Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -325,14 +247,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer; m_recordingObserver = observer;
} }
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) { VkResult FrameContext::RetireCurrentCommandBuffer() {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE, MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext"); "RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent(); auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording"); "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{}; VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
@@ -340,23 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1; allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE; VkCommandBuffer replacement = VK_NULL_HANDLE;
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement); const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
return result; return result;
} }
if (retirePreCommandBuffer) { frame.retiredCommandBuffers.push_back(frame.commandBuffer);
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; frame.commandBuffer = replacement;
return VK_SUCCESS; return VK_SUCCESS;
} }
@@ -366,40 +274,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return; return;
} }
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) { if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
for (const auto& retired : frame.retiredCommandBuffers) { vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer); frame.retiredCommandBuffers.data());
}
} }
frame.retiredCommandBuffers.clear(); 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 { void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range"); MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
} }
@@ -29,9 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE; VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE; VkSemaphore signalSemaphore = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO}; VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
}; };
@@ -42,35 +40,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; 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 { struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE; 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; VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE; VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false; Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false; Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false; Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands), // Command buffers submitted mid-frame (FlushPendingCommands) whose
// appended in submit order; freed once their submission is known // execution is only known complete once this slot's fence has been
// complete (fence wait or completion poll). // waited again; freed at that point.
Vector<RetiredCommandBuffer> retiredCommandBuffers; Vector<VkCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission // Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time). // (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0; Uint64 lastSubmitIndex = 0;
@@ -87,14 +67,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0, VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr); const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording(); 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); VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device); void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout, Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
@@ -107,18 +79,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Parks the current (already ended and submitted) command buffer on the // Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording // slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired // can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited, or as soon // buffers are freed after the slot's fence is next waited.
// as their submission is observed complete. VkResult RetireCurrentCommandBuffer();
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 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const; Uint32 GetFrameCount() const;
@@ -8,8 +8,6 @@
#include "PipelineFactory.h" #include "PipelineFactory.h"
#include <algorithm>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) { static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) { switch (topology) {
@@ -110,81 +108,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache"); "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() { PipelineFactory::~PipelineFactory() {
DestroyAll(); DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) { if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -229,8 +152,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp))); XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY( XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp))); XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) { if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update( XXHASH_VERIFY(XXH64_update(
m_hashState, m_hashState,
@@ -244,108 +165,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const HashType hash = ComputeHash(payload); const HashType hash = ComputeHash(payload);
auto it = m_cache.find(hash); auto it = m_cache.find(hash);
if (it != m_cache.end()) { if (it != m_cache.end()) {
it->second.lastUsedFrame = m_frameCounter; return it->second;
return it->second.pipeline;
} }
VkPipeline pipeline = CreatePipeline(payload); VkPipeline pipeline = CreatePipeline(payload);
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass, m_cache.emplace(hash, pipeline);
m_frameCounter});
return pipeline; return pipeline;
} }
void PipelineFactory::DestroyAll() { void PipelineFactory::DestroyAll() {
for (auto& pair : m_cache) { for (auto& pair : m_cache) {
if (pair.second.pipeline != VK_NULL_HANDLE) { if (pair.second != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr); vkDestroyPipeline(m_device, pair.second, nullptr);
} }
} }
m_cache.clear(); 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 { VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty"); MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null"); MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
@@ -422,17 +258,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[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}; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE; blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp; blend.logicOp = payload.logicOp;
@@ -49,9 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP; VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP; VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS; 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{}; Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr; const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr; const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
@@ -65,69 +62,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload); VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll(); 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: 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; VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config; const VulkanRendererConfig& m_config;
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, PipelineCacheEntry> m_cache; UnorderedMap<HashType, VkPipeline> 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 XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -14,16 +14,8 @@
#include "MG_State/GLState/TextureState/TextureEnum.h" #include "MG_State/GLState/TextureState/TextureEnum.h"
#include <Includes.h> #include <Includes.h>
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8 {
Unknown = 0,
Float,
SignedInteger,
UnsignedInteger,
};
class ProgramFactory { class ProgramFactory {
public: public:
enum class DescriptorBindingKind : Uint8 { enum class DescriptorBindingKind : Uint8 {
@@ -42,16 +34,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SurfaceRotate90 = 1 << 2, SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3, SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4, 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 CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64; using HashType = Uint64;
@@ -69,23 +51,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<DescriptorBindingKind> bindingKinds; Vector<DescriptorBindingKind> bindingKinds;
Vector<Uint32> dynamicBindings; Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding; Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
// 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<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding; Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding; Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding; Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding; 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; Int globalUboBinding = -1;
Uint32 activeVertexInputLocationMask = 0; Uint32 activeVertexInputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{}; Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
@@ -94,13 +64,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ShaderStage rasterizationProducerStage = ShaderStage::Unknown; ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0; Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0; Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
static inline VkDevice s_device = VK_NULL_HANDLE; static inline VkDevice s_device = VK_NULL_HANDLE;
@@ -116,18 +79,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds); bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings); dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); 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); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask; activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes; vertexInputTypes = other.vertexInputTypes;
@@ -136,20 +92,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage; rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount; producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1; other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0; other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0; other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown; other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0; other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0; other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
} }
VkProgramObject& operator=(VkProgramObject&& other) noexcept { VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) { if (this == &other) {
@@ -164,18 +115,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds); bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings); dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); 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); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask; activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes; vertexInputTypes = other.vertexInputTypes;
@@ -184,20 +128,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage; rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount; producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1; other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0; other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0; other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown; other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0; other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0; other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
return *this; return *this;
} }
@@ -227,24 +166,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
}; };
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
// layout handle value may be recycled for an unrelated layout, and the program
// hash may be re-inserted by a later rebuild of the same content.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false, Bool shaderDrawParametersEnabled = false)
Bool unformattedFloatStorageImagesEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config), : m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled), m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device; VkProgramObject::s_device = device;
} }
~ProgramFactory() = default; ~ProgramFactory() = default;
@@ -254,24 +179,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkProgramObject& GetOrCreateProgram( const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
// Frame boundary hook: ages the program cache and evicts long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage); static 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: private:
struct ProgramLookupCache { struct ProgramLookupCache {
@@ -298,13 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass // True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
// (which needs the DrawParameters capability / gl_BaseInstance builtin). // (which needs the DrawParameters capability / gl_BaseInstance builtin).
Bool m_shaderDrawParametersEnabled = false; Bool m_shaderDrawParametersEnabled = false;
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup; mutable ProgramLookupCache m_lastLookup;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -247,11 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace}; m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
m_extent = createInfo.imageExtent; 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; m_preTransform = createInfo.preTransform;
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain)); VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
@@ -262,9 +257,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.resize(imageCount, VK_NULL_HANDLE); m_images.resize(imageCount, VK_NULL_HANDLE);
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data())); VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED); 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); CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice); CreateDepthStencilResources(device, physicalDevice);
@@ -436,39 +428,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.clear(); m_images.clear();
m_imageLayouts.clear(); m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; 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 { VkImage SwapchainObject::GetImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range"); MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
return m_images[index]; return m_images[index];
@@ -35,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR GetHandle() const { return m_swapchain; } VkSwapchainKHR GetHandle() const { return m_swapchain; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; } const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; } 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; } VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; } const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; } const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
@@ -52,21 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SetImageLayout(Uint32 index, VkImageLayout layout); void SetImageLayout(Uint32 index, VkImageLayout layout);
SizeT GetImageCount() const { return m_images.size(); } 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: private:
void CreateImageViews(VkDevice device); void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice); void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
@@ -81,7 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkSurfaceFormatKHR m_surfaceFormat{}; VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{}; VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
Vector<VkImage> m_images; Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews; Vector<VkImageView> m_imageViews;
@@ -92,7 +73,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkDeviceMemory> m_depthStencilImageMemories; Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews; Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts; Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -13,12 +13,10 @@
#include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObject2D.h"
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h" #include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h> #include <Config.h>
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -80,16 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return uniformUnit >= 0 ? uniformUnit : 0; return uniformUnit >= 0 ? uniformUnit : 0;
} }
VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat) {
if (useBindingFormat) {
const TextureInternalFormat bindingInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat);
return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat);
}
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
}
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager, Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
ProgramFactory* programFactory, ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
@@ -205,7 +193,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The frame's descriptor sets are recycled above, so last frame's reuse target // The frame's descriptor sets are recycled above, so last frame's reuse target
// is gone: start the per-draw descriptor-reuse cache fresh this frame. // is gone: start the per-draw descriptor-reuse cache fresh this frame.
m_hasLastDescriptor = false; m_hasLastDescriptor = false;
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address // Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo). // reuse cannot outlive a single frame (see SamplerResolveMemo).
for (auto& memo : m_samplerResolveMemo) { for (auto& memo : m_samplerResolveMemo) {
@@ -213,40 +200,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
SizeT purgedSets = 0;
for (auto& frame : m_frames) {
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
if (it == frame.descriptorSetCacheByLayout.end()) {
continue;
}
// Free the sets back to their pools and credit the bucket accounting, so
// program churn recycles pool capacity instead of abandoning the slots.
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
// in-flight command buffer references these sets.
for (const auto& cached : it->second.sets) {
if (cached.set == VK_NULL_HANDLE) {
continue;
}
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
const auto bucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
--bucket->allocatedSets;
}
}
purgedSets += it->second.sets.size();
frame.descriptorSetCacheByLayout.erase(it);
}
if (purgedSets > 0) {
// The per-draw reuse memo folds the layout handle into its signature; drop
// it so a recycled handle value cannot revive a purged set mid-frame.
m_hasLastDescriptor = false;
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
@@ -323,55 +276,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding); static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
} }
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
// descriptor valid.
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
numericDomain == SamplerNumericDomain::UnsignedInteger;
SamplerResolveMemo* viewFormatMemo =
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
VkFormat sampledViewFormat;
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
viewFormatMemo->viewFormatSource == resource->format &&
viewFormatMemo->viewFormatDomain == numericDomain) {
sampledViewFormat = viewFormatMemo->viewFormat;
} else {
sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
if (viewFormatMemo != nullptr) {
viewFormatMemo->viewFormatSource = resource->format;
viewFormatMemo->viewFormatDomain = numericDomain;
viewFormatMemo->viewFormat = sampledViewFormat;
viewFormatMemo->viewFormatValid = true;
}
}
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
return false;
}
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
const VkImageView sampledImageView =
sampledViewFormat == resource->format
? resource->sampledView
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
static_cast<Int>(numericDomain));
return false;
}
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
// sampler object and texture (both by lifetime id + version) are unchanged from the // sampler object and texture (both by lifetime id + version) are unchanged from the
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler // last draw that resolved it: the resulting sampler key, and therefore the VkSampler
@@ -386,32 +290,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint16 samplerVersion = samplerToUse->GetVersion(); const Uint16 samplerVersion = samplerToUse->GetVersion();
const Uint64 textureLifetimeId = texture->GetLifetimeId(); const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
// follows uploads as well as GL parameters - so it belongs in the memo key too.
const Uint32 viewLevelCount = resource->sampledLevelCount;
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion && memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) {
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
resolvedSampler = memo.sampler; resolvedSampler = memo.sampler;
} else { } else {
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
forceNearestFiltering, viewLevelCount);
memo.samplerLifetimeId = samplerLifetimeId; memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion; memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId; memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion; memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering;
memo.viewLevelCount = viewLevelCount;
memo.sampler = resolvedSampler; memo.sampler = resolvedSampler;
memo.valid = true; memo.valid = true;
} }
} else { } else {
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering, resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
resource->sampledLevelCount);
} }
outImageInfo = { outImageInfo = {
.sampler = resolvedSampler, .sampler = resolvedSampler,
.imageView = sampledImageView, .imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
.imageLayout = resource->layout, .imageLayout = resource->layout,
}; };
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
@@ -448,48 +344,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
} }
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
Bool sawSampler = false;
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
if (texture == nullptr) return false;
const auto& levelRange = texture->GetLevelRange();
if (levelRange.x() != levelRange.y()) return false;
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
// filtering - which a single-level view can still have. Resolve the sampler exactly
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
const auto* effectiveSampler =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (effectiveSampler == nullptr) return false;
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
return false;
}
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
// min/mag decision. That only matches the implicit form when lambda could not have been
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
// filters are the same and the choice cannot be observed.
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
? 0.0f
: effectiveSampler->GetMaxLod();
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
return false;
}
sawSampler = true;
}
return sawSampler;
}
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) { SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
@@ -506,12 +360,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject(); outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so callers keep
// taking their fallback paths instead of trying to sync a storage-less texture.
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
outTexture.reset();
}
return true; return true;
} }
@@ -532,15 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without // GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
// touching the refcount (no atomic inc/dec per binding per draw). // touching the refcount (no atomic inc/dec per binding per draw).
MG_State::GLState::ITextureObject* texture = return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so the caller
// substitutes its fallback texture exactly like it did for the old null slot.
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
return nullptr;
}
return texture;
} }
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
@@ -718,32 +558,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level)); const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel);
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
binding);
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
const VkFormat viewFormat = ResolveStorageImageViewFormat(
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
if (viewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
useBindingFormat ? "true" : "false");
return false;
}
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
if (view == VK_NULL_HANDLE) { if (view == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u " view = resource->fullView;
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
static_cast<Int>(viewFormat),
useBindingFormat ? "true" : "false");
return false;
} }
outImageInfo.sampler = VK_NULL_HANDLE; outImageInfo.sampler = VK_NULL_HANDLE;
outImageInfo.imageView = view; outImageInfo.imageView = view;
@@ -782,15 +599,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding); MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
if (!texture) { if (!texture) {
// ResolveSamplerDescriptor will substitute the fallback texture for this binding; continue;
// include it in the sampled set so the pre-render-pass sync/transition pass covers
// its first use instead of leaving that work to happen inside an active pass.
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
if (preferredTarget != TextureTarget::Texture2D &&
preferredTarget != TextureTarget::TextureRectangle) {
continue;
}
texture = GetFallbackTexture(preferredTarget).get();
} }
auto found = std::find(outTextures.begin(), outTextures.end(), texture); auto found = std::find(outTextures.begin(), outTextures.end(), texture);
@@ -801,53 +610,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool UniformManager::CollectStorageImageTextures(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
outTextures.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
"CollectStorageImageTextures: GL context is null");
const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
continue;
}
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
return false;
}
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u",
imageUnit, binding);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u",
imageUnit, binding);
return false;
}
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
outTextures.push_back(texture);
}
}
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const { UboBindResult& out) const {
const void* outData = nullptr; const void* outData = nullptr;
VkDeviceSize outSize = 0; VkDeviceSize outSize = 0;
@@ -873,19 +638,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; const Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
if (arrayElement > 0) {
const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding);
const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() &&
arrayElement < arrayIt->second.size();
MOBILEGL_ASSERT(elementValid,
"ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding,
arrayElement);
if (!elementValid) {
return false;
}
blockIndex = arrayIt->second[arrayElement];
}
MOBILEGL_ASSERT(blockIndex >= 0, MOBILEGL_ASSERT(blockIndex >= 0,
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding); "ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
@@ -993,11 +746,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPoolCreateInfo poolInfo{}; VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
// The cost is on set allocation only, which happens when a layout's per-frame
// cache grows - never on the per-draw reuse path.
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
poolInfo.maxSets = maxSets; poolInfo.maxSets = maxSets;
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes)); poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
poolInfo.pPoolSizes = poolSizes; poolInfo.pPoolSizes = poolSizes;
@@ -1077,7 +825,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& frame = m_frames[frameIndex]; auto& frame = m_frames[frameIndex];
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout]; auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
if (cache.cursor < cache.sets.size()) { if (cache.cursor < cache.sets.size()) {
outDescriptorSet = cache.sets[cache.cursor++].set; outDescriptorSet = cache.sets[cache.cursor++];
} else { } else {
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet); VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) { if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
@@ -1091,9 +839,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return allocResult; return allocResult;
} }
// The successful allocation came from the bucket the alloc helper left cache.sets.push_back(outDescriptorSet);
// active; record it so a layout-destroyed purge can free the set back.
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
++cache.cursor; ++cache.cursor;
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex, MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
cache.sets.size()); cache.sets.size());
@@ -1139,17 +885,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfos.clear(); imageInfos.clear();
texelBufferViews.clear(); texelBufferViews.clear();
dynamicOffsets.clear(); dynamicOffsets.clear();
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
Uint32 uboArrayExtra = 0;
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
}
writes.reserve(m_maxBindings); writes.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings + uboArrayExtra); bufferInfos.reserve(m_maxBindings);
imageInfos.reserve(m_maxBindings); imageInfos.reserve(m_maxBindings);
texelBufferViews.reserve(m_maxBindings); texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra); dynamicOffsets.reserve(programObj.dynamicBindings.size());
const Uint32 bindingCount = const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size())); std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
@@ -1167,82 +907,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorCount = 1; write.descriptorCount = 1;
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
const Uint32 descriptorCount = UboBindResult ubo{};
binding < programObj.bindingDescriptorCounts.size() const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo);
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding]) MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
: 1u; "UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u",
const SizeT firstBufferInfoIndex = bufferInfos.size(); binding);
for (Uint32 element = 0; element < descriptorCount; ++element) {
UboBindResult ubo{};
const Bool hasPayload =
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
binding, element);
VkDescriptorBufferInfo bufferInfo{}; VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo // Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting. // is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0; bufferInfo.offset = 0;
Uint32 dynOffset; Uint32 dynOffset;
if (ubo.directBindable) { if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy. // Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer; bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range; bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset); dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else { } else {
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged BufferSlice slice{};
// uniform bytes re-use the slice already uploaded this frame. if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
const Bool isGlobalUbo = ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0; MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial(); binding);
const Uint64 uboProgramLifetimeId = program.GetLifetimeId(); return false;
const Uint32 uboContentVersion = program.GetUBOContentVersion();
Bool reusedSlice = false;
if (isGlobalUbo) {
for (const auto& memo : m_globalUboMemo) {
if (memo.buffer != VK_NULL_HANDLE &&
memo.programLifetimeId == uboProgramLifetimeId &&
memo.frameSerial == uboFrameSerial &&
memo.uboContentVersion == uboContentVersion &&
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
bufferInfo.buffer = memo.buffer;
bufferInfo.range = memo.range;
dynOffset = static_cast<Uint32>(memo.offset);
reusedSlice = true;
break;
}
}
}
if (!reusedSlice) {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
}
}
} }
bufferInfos.push_back(bufferInfo); bufferInfo.buffer = slice.buffer;
// Dynamic offsets are consumed in binding order, then array element order, bufferInfo.range = ubo.payloadSize;
// matching Vulkan's dynamic-offset consumption rules. dynOffset = static_cast<Uint32>(slice.offset);
dynamicOffsets.push_back(dynOffset);
} }
bufferInfos.push_back(bufferInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
write.descriptorCount = descriptorCount; write.pBufferInfo = &bufferInfos.back();
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
writes.push_back(write); writes.push_back(write);
dynamicOffsets.push_back(dynOffset);
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) { } else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
VkBufferView bufferView = VK_NULL_HANDLE; VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) || if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
@@ -1370,34 +1068,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_hasLastDescriptor = cacheable; m_hasLastDescriptor = cacheable;
} }
// Skip the driver call when this exact binding is already live on the vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
// command buffer (see the bind-dedup shadow in the header). &descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
if (identicalBind) {
for (Uint32 i = 0; i < offsetCount; ++i) {
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
identicalBind = false;
break;
}
}
}
if (!identicalBind) {
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, offsetCount, dynamicOffsets.data());
if (offsetCount <= kMaxShadowedDynamicOffsets) {
m_lastBindValid = true;
m_lastBindSet = descriptorSet;
m_lastBindLayout = programObj.pipelineLayout;
m_lastBindPoint = bindPoint;
m_lastBindOffsetCount = offsetCount;
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
} else {
m_lastBindValid = false;
}
}
return true; return true;
} }
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -39,26 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown(); void Shutdown();
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
// vkFreeDescriptorSets'd back to their pools (created with
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures); Vector<MG_State::GLState::ITextureObject*>& outTextures);
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
@@ -66,22 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr); const SamplerBindingOverride* samplerBindingOverride = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
// format and never silently fall back to the backing image format.
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private: private:
struct DescriptorPoolBucket { struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE; VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -89,16 +56,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 allocatedSets = 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 { struct DescriptorSetCacheEntry {
Vector<CachedDescriptorSet> sets; Vector<VkDescriptorSet> sets;
Uint32 cursor = 0; Uint32 cursor = 0;
}; };
@@ -148,7 +107,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const; UboBindResult& out) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const; Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex); Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool( VkResult AllocateDescriptorSetsFromActivePool(
@@ -188,35 +147,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_lastDescriptorSignature = 0; Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false; Bool m_hasLastDescriptor = false;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
// layout+bind point, so a pipeline-layout switch always rebinds.
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
Bool m_lastBindValid = false;
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint32 m_lastBindOffsetCount = 0;
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
// Global-UBO transient-slice reuse: MC leaves the default uniform block
// untouched across long GUI/terrain runs, so the per-draw re-upload of
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
// serial guards arena recycling; the content version guards writes).
struct GlobalUboSliceMemo {
Uint64 programLifetimeId = 0;
Uint64 frameSerial = 0;
Uint32 uboContentVersion = 0;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize range = 0;
};
static constexpr Uint32 kGlobalUboMemoSize = 4;
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
Uint32 m_globalUboMemoNext = 0;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which // 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 // 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 // sampler objects with identical state still resolve to one VkSampler. This memo only
@@ -233,19 +163,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 samplerLifetimeId = 0; Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0; Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0; Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0; Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false; Bool valid = false;
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
// so a reinterpreted sampler pays that scan once, not once per draw.
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool viewFormatValid = false;
}; };
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo; mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -32,13 +32,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor))); XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
// The buffer's heap address is an identity component of the key: a freed
// buffer's reused address can alias an old cache entry, but only under a
// byte-identical attribute layout - and the entry payload is a pure function
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
// against the live VAO attribute pointers, so an aliased hit returns exactly
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
// aging sweep bounds that.
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get()); const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey))); XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
} }
@@ -58,27 +51,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) { const MG_State::GLState::VertexArrayObject& vao) {
// Per-draw fast path: the VAO carries a pointer to its resolved entry, return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
return entry;
} }
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) { const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash); auto it = m_cache.find(hash);
if (it != m_cache.end()) { if (it != m_cache.end()) {
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter; return it->second;
return *it->second;
} }
VertexInputStateBuilder builder; VertexInputStateBuilder builder;
@@ -86,7 +66,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Uint32 unsupportedAttribMask = 0; Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -95,9 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const VkFormat sourceVkFormat = const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); if (vkFormat == VK_FORMAT_UNDEFINED) {
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat", "enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -105,33 +83,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; 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); const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) { if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is " MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
@@ -141,33 +92,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const Uint32 sourceStride = const Uint32 stride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize); 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 = const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -177,49 +103,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0); bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
bindingAttributeLocations.push_back(location); bindingAttributeLocations.push_back(location);
bindingUsesClientMemory.push_back(attr.Buffer == nullptr); bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate); builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0); builder.AddAttribute(location, binding, vkFormat, 0);
} }
const auto& state = builder.Build(); const auto& state = builder.Build();
auto& slot = m_cache[hash]; auto& entry = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash; entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindings = builder.GetBindings(); entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes(); entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys); entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets); entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations); entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory); entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask; entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state; entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
@@ -227,33 +124,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return entry; 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, VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra) { Bool isBgra) {
if (isBgra) { if (isBgra) {
@@ -412,47 +282,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT componentSize = GetComponentSize(type); const SizeT componentSize = GetComponentSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size); 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 } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,47 +19,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public: public:
using HashType = Uint64; using HashType = Uint64;
enum class VertexStreamConversion : Uint8 {
None = 0,
Repack,
ScaledIntegerToFloat32,
};
struct BackendVertexInputState { struct BackendVertexInputState {
HashType hash = 0; HashType hash = 0;
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
// pipelines on that minted one VkPipeline per chunk section for an
// identical layout, defeating pipeline reuse and the per-draw memo.
// Pipelines depend only on the layout, so they key on this instead.
HashType layoutHash = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age are evicted (CPU heap only).
// Mutable: the VAO's state-pointer memo fast path stamps it through
// a const entry reference.
mutable Uint64 lastUsedFrameBoundary = 0;
Vector<VkVertexInputBindingDescription> bindings; Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes; Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys; Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are // 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 // 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. // a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0; Uint32 unsupportedAttribMask = 0;
// Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time.
Uint32 attributeLocationMask = 0;
VkPipelineVertexInputStateCreateInfo state{ VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
}; };
}; };
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice): explicit VertexInputStateFactory(const VulkanRendererConfig& config):
m_config(config), m_physicalDevice(physicalDevice) {} m_config(config) {}
~VertexInputStateFactory() = default; ~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete; VertexInputStateFactory(const VertexInputStateFactory&) = delete;
@@ -70,14 +48,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const BackendVertexInputState& GetOrCreateVertexInputState( const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash); const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao); const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
// Frame boundary hook: ages the cache and evicts entries not hit for many
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
// minting fresh keys; without eviction the map grows for the whole session.
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
// and the draw path's entry reference never spans a frame boundary, so
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
// compare except on sweep boundaries.
void OnFrameBoundary();
static SizeT GetComponentSize(DataType type); static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for // 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 // normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
@@ -86,25 +56,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private: private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false); static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
const VulkanRendererConfig& m_config; const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; UnorderedMap<HashType, BackendVertexInputState> m_cache;
// 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(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -22,10 +22,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT; 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 // 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). // be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags = constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
@@ -145,15 +141,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_transientUploadArena.BeginFrame(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() { void VkBufferManager::NotifyDeviceIdle() {
// Everything submitted so far has completed. Work recorded for the // Everything submitted so far has completed. Work recorded for the
// current frame has not been submitted yet, so the current serial // current frame has not been submitted yet, so the current serial
@@ -210,20 +197,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get()); return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
} }
VkBufferResource* VkBufferManager::GetOrCreateResource( SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) { const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
// Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
// 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) { if (existing) {
return static_cast<VkBufferResource*>(existing.get()); return existing;
} }
auto resource = MakeShared<VkBufferResource>(); auto resource = MakeShared<VkBufferResource>();
VkBufferResource* raw = resource.get();
bufferObject->SetBackendResource(resource); bufferObject->SetBackendResource(resource);
TrackLiveResource(resource); TrackLiveResource(resource);
return raw; return resource;
} }
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) { void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
@@ -469,10 +452,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// it from the current shadow - MappedData() is still the shadow here because the // it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns. // frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer)); DeferRelease(std::move(resource->buffer));
const VkBufferUsageFlags persistentUsage = if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
kPersistentBackedUsage |
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false; resource->persistentMapped = false;
resource->storageSize = 0; resource->storageSize = 0;
resource->usageFlags = 0; resource->usageFlags = 0;
@@ -31,9 +31,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO; VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
Bool transientPersistentMapping = false; 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). // The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
@@ -80,11 +77,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Recreate all per-frame transient arenas // Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount); Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred buffer/resource releases (and the
// 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). // All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle(); void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to // A frame slot's submission fence has been waited: every serial up to
@@ -132,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private: private:
Bool InitializeTransientArenas(); Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject); SharedPtr<VkBufferResource> GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage, Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags requiredFlags = 0); VkMemoryPropertyFlags requiredFlags = 0);
@@ -157,25 +157,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
if (resolvedSize == 0) {
return true;
}
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
}
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const { BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range"); MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size; const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
@@ -44,7 +44,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* Map(); void* Map();
void Unmap(); void Unmap();
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0); 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; } VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; } VkDeviceSize GetSize() const { return m_size; }
@@ -93,7 +93,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear(); m_pendingClears.clear();
m_aliveObjects.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) { TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
@@ -128,7 +127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_pendingClears.erase(key); m_pendingClears.erase(key);
} }
m_aliveObjects.erase(identity); m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
} }
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity, Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
@@ -223,7 +221,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture; m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key]; auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload); MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
} }
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload, void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
@@ -241,7 +238,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture; m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key]; auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload); MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
} }
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) { Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
@@ -249,10 +245,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; 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 Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) { for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -268,9 +260,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) { if (key.texture == nullptr) {
return false; 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); const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) { if (m_pendingClears.find(key) == m_pendingClears.end()) {
@@ -298,9 +287,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) { if (key.texture == nullptr) {
return false; 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); const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) { if (!LockTextureLocked(key, outTexture)) {
@@ -339,9 +325,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (texture == nullptr) { if (texture == nullptr) {
return false; 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 Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
@@ -362,9 +345,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return; return;
} }
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture); const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex()); MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
@@ -381,7 +361,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto it = m_pendingClears.find(key); auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) { if (it != m_pendingClears.end()) {
m_pendingClears.erase(it); m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
} }
} }
@@ -14,7 +14,6 @@
#include "MG_Util/Math/VectorTypes.h" #include "MG_Util/Math/VectorTypes.h"
#include <Includes.h> #include <Includes.h>
#include <atomic>
#include <unordered_map> #include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
@@ -121,19 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject>& outTexture); SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0; 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; 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<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects; std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
}; };
@@ -16,21 +16,31 @@
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) { static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
// GL promises "at least the requested samples", so a non-power-of-two switch (requestedSamples <= 0 ? 1 : requestedSamples) {
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit. case 1:
if (requestedSamples <= 1) {
outSampleCount = VK_SAMPLE_COUNT_1_BIT; outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true; return true;
} case 2:
if (requestedSamples > 64) { outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
return false; return false;
} }
Uint32 bit = 1;
while (bit < static_cast<Uint32>(requestedSamples)) {
bit <<= 1;
}
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
return true;
} }
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) { static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
@@ -156,9 +166,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (view != VK_NULL_HANDLE) { if (view != VK_NULL_HANDLE) {
vkDestroyImageView(device, view, nullptr); vkDestroyImageView(device, view, nullptr);
} }
if (unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(device, unormTwinView, nullptr);
}
if (image != VK_NULL_HANDLE && allocation != nullptr) { if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(allocator, image, allocation); vmaDestroyImage(allocator, image, allocation);
} }
@@ -166,7 +173,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
image = VK_NULL_HANDLE; image = VK_NULL_HANDLE;
allocation = nullptr; allocation = nullptr;
view = VK_NULL_HANDLE; view = VK_NULL_HANDLE;
unormTwinView = VK_NULL_HANDLE;
layout = VK_IMAGE_LAYOUT_UNDEFINED; layout = VK_IMAGE_LAYOUT_UNDEFINED;
format = VK_FORMAT_UNDEFINED; format = VK_FORMAT_UNDEFINED;
aspect = VK_IMAGE_ASPECT_NONE; aspect = VK_IMAGE_ASPECT_NONE;
@@ -174,7 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampleCount = VK_SAMPLE_COUNT_1_BIT; sampleCount = VK_SAMPLE_COUNT_1_BIT;
internalFormat = TextureInternalFormat::Unknown; internalFormat = TextureInternalFormat::Unknown;
samples = 0; samples = 0;
deadSinceFrame = kNeverObservedDead;
} }
VkRenderPassManager::VkRenderPassManager(VkDevice device, VkRenderPassManager::VkRenderPassManager(VkDevice device,
@@ -201,7 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.Destroy(m_device, m_allocator); resource.Destroy(m_device, m_allocator);
} }
m_renderbufferResources.clear(); m_renderbufferResources.clear();
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
m_pendingRenderbufferClears.clear(); m_pendingRenderbufferClears.clear();
RenderPassEntry::s_textureResourcesScratch.clear(); RenderPassEntry::s_textureResourcesScratch.clear();
s_activeRenderPass = {}; s_activeRenderPass = {};
@@ -209,80 +213,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastValid = false; m_rpFastValid = false;
} }
Uint64 VkRenderPassManager::RetireAgeFrames() const {
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
// recording-to-submit gap and one because OnPresent runs ahead of Present's
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
// still releasing multi-MB attachment memory promptly (the render-pass cache's
// 1024-frame retirement would pin it for no additional safety).
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
}
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
// The superseded backing may still be referenced by in-flight command buffers
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
return;
}
m_deferredRenderbufferReleases.push_back(
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
resource.view = VK_NULL_HANDLE;
resource.unormTwinView = VK_NULL_HANDLE;
}
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
if (m_deferredRenderbufferReleases.empty()) {
return;
}
const Uint64 retireAgeFrames = RetireAgeFrames();
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
return false;
}
if (release.view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.view, nullptr);
}
if (release.unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
}
if (release.image != VK_NULL_HANDLE) {
vmaDestroyImage(m_allocator, release.image, release.allocation);
}
return true;
});
}
void VkRenderPassManager::CollectRenderbufferGarbage() { void VkRenderPassManager::CollectRenderbufferGarbage() {
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
// command buffers submitted up to frames-in-flight frames ago (it was legally deadRenderbuffers.reserve(m_renderbufferResources.size());
// attached and drawn right up to its deletion), so the first observation of an for (auto& [renderbuffer, resource] : m_renderbufferResources) {
// expired weak reference only stamps the current frame counter; Destroy runs once
// enough frame boundaries have passed that the stamping frame's submission fence
// has provably been waited (see RetireAgeFrames).
const Uint64 retireAgeFrames = RetireAgeFrames();
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
auto& resource = it->second;
const auto liveRenderbuffer = resource.renderbuffer.lock(); const auto liveRenderbuffer = resource.renderbuffer.lock();
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) { if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead; deadRenderbuffers.emplace_back(renderbuffer);
++it;
continue;
} }
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) { }
resource.deadSinceFrame = m_frameCounter; for (auto* renderbuffer : deadRenderbuffers) {
++it; auto resourceIt = m_renderbufferResources.find(renderbuffer);
continue; if (resourceIt != m_renderbufferResources.end()) {
resourceIt->second.Destroy(m_device, m_allocator);
m_renderbufferResources.erase(resourceIt);
} }
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) { m_pendingRenderbufferClears.erase(renderbuffer);
++it;
continue;
}
m_pendingRenderbufferClears.erase(it->first);
resource.Destroy(m_device, m_allocator);
it = m_renderbufferResources.erase(it);
} }
} }
@@ -303,94 +249,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const auto internalFormat = renderbuffer->GetInternalFormat(); const auto internalFormat = renderbuffer->GetInternalFormat();
// Three-channel color formats widen to their RGBA twin exactly like textures do const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
// renderbuffer and a texture of the same GL format then see one VkFormat.
const VkFormat format = [&]() -> VkFormat {
switch (internalFormat) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8:
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format); const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer), MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet",
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization. renderbuffer->GetExternalIndex());
const VkImageUsageFlags imageUsage = return nullptr;
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
// GL allows the implementation to allocate more samples than requested
// (glRenderbufferStorageMultisample only promises "at least"), and devices
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
// nearest supported count for this format.
if (renderbuffer->GetSamples() > 0) {
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
VkImageFormatProperties formatProperties{};
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
&formatProperties) == VK_SUCCESS) {
supported = formatProperties.sampleCounts;
}
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
}
const VkSampleCountFlags supported = supportedIt->second;
if ((supported & sampleCount) == 0) {
// Smallest supported count above the request, else the largest below it.
Uint32 rounded = 0;
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
if (rounded == 0) {
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
}
if (rounded != 0) {
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
}
} }
auto& resource = m_renderbufferResources[renderbuffer.get()]; auto& resource = m_renderbufferResources[renderbuffer.get()];
@@ -404,15 +268,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.samples != renderbuffer->GetSamples(); resource.samples != renderbuffer->GetSamples();
if (!needsCreate) { if (!needsCreate) {
resource.renderbuffer = renderbuffer; resource.renderbuffer = renderbuffer;
// A new renderbuffer at a recycled address may adopt a compatible entry that
// was already stamped dead; it is alive again, so cancel the aging.
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
return &resource; return &resource;
} }
// Respecify: park the old backing for aged destruction instead of destroying
// inline - it may still be referenced by in-flight command buffers.
DeferRenderbufferBackingRelease(resource);
resource.Destroy(m_device, m_allocator); resource.Destroy(m_device, m_allocator);
resource.renderbuffer = renderbuffer; resource.renderbuffer = renderbuffer;
@@ -427,15 +285,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = format; imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = imageUsage; imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
imageInfo.samples = sampleCount; imageInfo.samples = sampleCount;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
// is disabled, which needs a format-reinterpreting second view.
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
if (hasUnormTwin) {
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
VkImageFormatProperties imageFormatProperties{}; VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
@@ -470,11 +322,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.subresourceRange.layerCount = 1; viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view), VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
"vkCreateImageView(renderbuffer)"); "vkCreateImageView(renderbuffer)");
if (hasUnormTwin) {
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
"vkCreateImageView(renderbuffer unorm twin)");
}
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED; resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.format = format; resource.format = format;
@@ -539,18 +386,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkRenderPassManager::QueueRenderbufferClear( void VkRenderPassManager::QueueRenderbufferClear(
GLbitfield mask, const ClearFramebufferPayload& clearPayload, GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) { const MG_State::GLState::FramebufferObject& drawFbo) {
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
drawFbo.GetAttachment(attachmentType));
}
}
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) { if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
QueueRenderbufferClear( QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth}, ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
@@ -571,18 +406,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash( VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear, const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
Bool includeDefaultFboDepthStencil) {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
if (isDefaultFbo) { if (isDefaultFbo) {
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex))); XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
} }
// sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
auto& drawBuffers = fbo.GetDrawBuffers(); auto& drawBuffers = fbo.GetDrawBuffers();
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0]))); XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
auto readBuffer = fbo.GetReadBuffer(); auto readBuffer = fbo.GetReadBuffer();
@@ -656,17 +485,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachment <= FramebufferAttachmentType::BackRight); attachment <= FramebufferAttachmentType::BackRight);
if (isDefaultColorAttachment) { if (isDefaultColorAttachment) {
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex); currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// Content validity feeds the attachment's loadOp (see the
// creation path), so it must key the cache as well.
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
} else if (attachment == FramebufferAttachmentType::Depth || } else if (attachment == FramebufferAttachmentType::Depth ||
attachment == FramebufferAttachmentType::Stencil) { attachment == FramebufferAttachmentType::Stencil) {
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex); currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
} }
} else { } else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture); auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
@@ -721,49 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
combineFramebufferAttachmentObjHash(drawbuf); combineFramebufferAttachmentObjHash(drawbuf);
} }
// The depth-less default-FBO flavor omits the depth/stencil attachment combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
// entirely, so it must hash differently from the depth-full flavor. combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
if (depthStencilIncluded) {
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
}
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex, Uint32 swapchainImageIndex) {
Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the
// depth attachment when the caller needs it, when a depth/stencil clear is
// pending, or when the active pass already carries it (escalate-only, so
// alternating depth-less draws never split an established depth pass).
Bool includeDefaultFboDepthStencil = true;
if (fbo.IsDefaultFramebuffer()) {
Bool activeDefaultHasDepthStencil = false;
if (const auto* active = GetActiveRenderPass()) {
Bool activeIsSwapchainPass = false;
Bool activeHasSwapchainDepthStencil = false;
for (const auto& tracked : active->trackedAttachmentLayouts) {
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
activeHasSwapchainDepthStencil |=
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
}
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
}
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
const Bool pendingDepthStencilClear =
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
HasPendingRenderbufferClear(defaultDepthAtt) ||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
HasPendingRenderbufferClear(defaultStencilAtt);
includeDefaultFboDepthStencil =
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
}
auto hasPendingClearOnFramebuffer = [&]() -> Bool { auto hasPendingClearOnFramebuffer = [&]() -> Bool {
const auto& drawBuffers = fbo.GetDrawBuffers(); const auto& drawBuffers = fbo.GetDrawBuffers();
for (auto attachment : drawBuffers) { for (auto attachment : drawBuffers) {
@@ -813,16 +599,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex && m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() && m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch && m_rpFastRbEpoch == m_renderbufferImageEpoch &&
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) { m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
auto activeIt = m_renderPasses.find(activeRenderPass->hash); auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) { if (activeIt != m_renderPasses.end()) {
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return activeIt->second;
} }
} }
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil); auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
if (activeRenderPass != nullptr && if (activeRenderPass != nullptr &&
activeRenderPass->CompatibleWith(compatibilityHash) && activeRenderPass->CompatibleWith(compatibilityHash) &&
!hasPendingClearOnFramebuffer()) { !hasPendingClearOnFramebuffer()) {
@@ -839,16 +623,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch(); m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
m_rpFastRbEpoch = m_renderbufferImageEpoch; m_rpFastRbEpoch = m_renderbufferImageEpoch;
m_rpFastRenderPassHash = activeRenderPass->hash; m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return activeIt->second;
} }
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil); auto hash = ComputeHash(fbo, swapchainImageIndex, true);
auto it = m_renderPasses.find(hash); auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) { if (it != m_renderPasses.end())
it->second.lastUsedFrame = m_frameCounter;
return it->second; return it->second;
}
Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
// Color attachment // Color attachment
@@ -898,88 +678,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// assuming default FBO has the right param // assuming default FBO has the right param
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) { for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
auto drawbuf = drawbufs[i]; auto drawbuf = drawbufs[i];
// Renderbuffer color attachments mirror the texture path below, with the
// resource (image/view/format/layout) coming from the render-pass manager's
// renderbuffer store instead of the texture manager.
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
const auto& rbAtt = fbo.GetAttachment(drawbuf);
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue;
}
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back();
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
ClearAttachmentPayload rbClearPayload{};
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
if (rbHasClear &&
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
rbClearPayload.color =
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
rbClearPayload.color.z(), 1.0f);
}
const VkImageLayout trackedRbLayout = rbResource->layout;
const Bool rbFramebufferSrgb =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat rbAttachmentFormat =
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
rbDesc.flags = 0;
rbDesc.format = rbAttachmentFormat;
rbDesc.samples = rbResource->sampleCount;
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
: VK_ATTACHMENT_LOAD_OP_LOAD);
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
static_cast<Int>(renderbuffer->GetExternalIndex()));
if (rbHasClear) {
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
.attachmentIndex = rbAttachmentIndex,
.colorAttachmentSlot = i,
.renderbuffer = renderbuffer.get(),
.hasInlinePayload = true,
.inlinePayload = rbClearPayload,
});
}
if (width == 0)
width = static_cast<Int>(rbResource->extent.width);
if (height == 0)
height = static_cast<Int>(rbResource->extent.height);
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Renderbuffer,
.renderbuffer = renderbuffer,
.finalLayout = rbDesc.finalLayout,
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue;
}
}
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i); auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
if (texture == nullptr) if (texture == nullptr)
continue; continue;
@@ -998,10 +696,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureTarget::Texture2D: case TextureTarget::Texture2D:
case TextureTarget::Texture2DArray: case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisample: case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
case TextureTarget::Texture3D:
case TextureTarget::TextureCubeMap:
case TextureTarget::TextureCubeMapArray:
case TextureTarget::TextureRectangle: { case TextureTarget::TextureRectangle: {
desc.flags = 0; desc.flags = 0;
desc.format = isDefaultFbo ? desc.format = isDefaultFbo ?
@@ -1024,7 +718,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (hasClear) { if (hasClear) {
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo { pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
.attachmentIndex = attachmentIndex, .attachmentIndex = attachmentIndex,
.colorAttachmentSlot = i,
.key = VkClearManager::MakePendingClearKey(att) .key = VkClearManager::MakePendingClearKey(att)
}); });
} }
@@ -1040,13 +733,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(), MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
"GetOrCreateRenderPass: swapchain image index out of range"); "GetOrCreateRenderPass: swapchain image index out of range");
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex); trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// EGL: a presented color buffer's content is undefined when its
// image comes back around (EGL_BUFFER_DESTROYED, the default
// swap behaviour) - skip the tile load instead of reloading
// stale pixels nobody may rely on.
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::SwapchainColor, .target = TrackedAttachmentTarget::SwapchainColor,
.swapchainImageIndex = swapchainImageIndex, .swapchainImageIndex = swapchainImageIndex,
@@ -1060,15 +746,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(textureResource, MOBILEGL_ASSERT(textureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i); "GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
textureResources.emplace_back(textureResource); textureResources.emplace_back(textureResource);
desc.format = ResolveSrgbAttachmentWriteFormat( desc.format = textureResource->format;
textureResource->format,
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
attachmentSampleCount = textureResource->sampleCount; attachmentSampleCount = textureResource->sampleCount;
trackedColorLayout = textureResource->layout; trackedColorLayout = textureResource->layout;
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture, .target = TrackedAttachmentTarget::Texture,
.texture = att.GetTexture(), .texture = att.GetTexture(),
.textureRaw = att.GetTexture().get(),
.textureMipLevel = attachmentMipLevel, .textureMipLevel = attachmentMipLevel,
.finalLayout = desc.finalLayout, .finalLayout = desc.finalLayout,
}); });
@@ -1132,12 +815,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt : const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr); (isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
// and their content is undefined anyway (EGL swap), so drop the attachment
// and its whole tile load + store.
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
selectedDepthStencilAttachment = nullptr;
}
const Bool hasDistinctDepthAndStencilAttachments = const Bool hasDistinctDepthAndStencilAttachments =
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) && isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt); !sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
@@ -1156,12 +833,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout trackedDepthLayout = isDefaultFbo ? VkImageLayout trackedDepthLayout = isDefaultFbo ?
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) : m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
// undefined after a swap, so the first default-FBO pass of a frame can
// skip the depth/stencil tile load outright.
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
depthAttachmentDescription.flags = 0; depthAttachmentDescription.flags = 0;
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
Int depthAttachmentId = 0; Int depthAttachmentId = 0;
@@ -1245,7 +916,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture, .target = TrackedAttachmentTarget::Texture,
.texture = selectedDepthStencilAttachment->GetTexture(), .texture = selectedDepthStencilAttachment->GetTexture(),
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
.textureMipLevel = attachmentMipLevel, .textureMipLevel = attachmentMipLevel,
.finalLayout = depthAttachmentDescription.finalLayout, .finalLayout = depthAttachmentDescription.finalLayout,
}); });
@@ -1291,22 +961,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED; const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
// Declare only the used colour-reference span. The GL draw-buffer array
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
// per-pixel render-backend/export path from the DECLARED count, so every
// fragment of every pass paid the 8-target export cost (measured on
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
// Interior GL_NONE holes keep their slots so fragment-output locations
// still line up; a fragment output at a location past the trimmed count
// is discarded, which is exactly GL's semantic for writing to a draw
// buffer set to GL_NONE.
while (!colorAttachmentRefs.empty() &&
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
colorAttachmentRefs.pop_back();
}
// Subpass // Subpass
VkSubpassDescription subpassDesc; VkSubpassDescription subpassDesc;
subpassDesc.flags = 0; subpassDesc.flags = 0;
@@ -1320,52 +974,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
subpassDesc.preserveAttachmentCount = 0; subpassDesc.preserveAttachmentCount = 0;
subpassDesc.pPreserveAttachments = nullptr; subpassDesc.pPreserveAttachments = nullptr;
// External subpass dependencies. Without them there is NO execution/memory
// dependency between consecutive render passes (or a pass and a transfer)
// touching the same attachments when the image layout does not change — the
// layout-transition helper no-ops on identical layouts and no other barrier
// exists. Tile-based GPUs then race tile loads against the previous pass's
// stores (multi-pass FBO chains like MC 26.3's OIT flicker on Adreno).
// Conservative both-ways dependencies: prior writes (attachment output,
// depth/stencil, transfer, shader) are made visible to this pass's loads,
// and this pass's attachment writes to subsequent sampling/transfer/loads.
VkSubpassDependency subpassDependencies[2];
subpassDependencies[0] = {};
subpassDependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
subpassDependencies[0].dstSubpass = 0;
subpassDependencies[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
subpassDependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
subpassDependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
subpassDependencies[0].dstAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_READ_BIT;
subpassDependencies[0].dependencyFlags = 0;
subpassDependencies[1] = {};
subpassDependencies[1].srcSubpass = 0;
subpassDependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
subpassDependencies[1].srcStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
subpassDependencies[1].srcAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
subpassDependencies[1].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT;
subpassDependencies[1].dstAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
subpassDependencies[1].dependencyFlags = 0;
// Render Pass // Render Pass
VkRenderPassCreateInfo renderPassCreateInfo; VkRenderPassCreateInfo renderPassCreateInfo;
renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
@@ -1375,8 +983,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.pAttachments = attachmentDescriptions.data(); renderPassCreateInfo.pAttachments = attachmentDescriptions.data();
renderPassCreateInfo.subpassCount = 1; renderPassCreateInfo.subpassCount = 1;
renderPassCreateInfo.pSubpasses = &subpassDesc; renderPassCreateInfo.pSubpasses = &subpassDesc;
renderPassCreateInfo.dependencyCount = 2; renderPassCreateInfo.dependencyCount = 0;
renderPassCreateInfo.pDependencies = subpassDependencies; renderPassCreateInfo.pDependencies = nullptr;
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass)); VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
@@ -1407,7 +1015,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hasDepthStencilAttachment, hasDepthStencilAttachment,
renderPassSampleCount, renderPassSampleCount,
extent, extent,
framebufferLayers }; static_cast<Int>(framebufferLayers) };
MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d", MGLOG_D("VkRenderPassManager::GetOrCreateRenderPass: hash=0x%llx compatibilityHash=0x%llx attachmentCount=%u colorAttachmentCount=%u samples=%d extent=%dx%d",
static_cast<unsigned long long>(hash), static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(compatibilityHash), static_cast<unsigned long long>(compatibilityHash),
@@ -1417,54 +1025,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.x(), extent.x(),
extent.y()); extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry)); auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second; return insertedIt->second;
} }
void VkRenderPassManager::OnPresent() {
++m_frameCounter;
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
// is O(#renderbuffer resources) — single digits in practice — and per-frame
// invocation keeps dead-resource reclaim latency at the aging bound instead of
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
// site never runs again once an app stops using renderbuffers).
CollectRenderbufferGarbage();
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
// Sweep occasionally; evict entries whose last use is far past every
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
// safely (RenderPassEntry's destructor releases the handles).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return;
}
// Collect the dying handles and notify once after the loop: pipelines hashed
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
// by draws that hit those entries), so the observer may destroy them
// immediately - and a single batched notification costs one pipeline-cache
// scan instead of one per evicted pass.
Vector<VkRenderPass> destroyedRenderPasses;
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
m_rpFastValid = false;
}
destroyedRenderPasses.push_back(it->second.renderPass);
it = m_renderPasses.erase(it);
} else {
++it;
}
}
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
}
}
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) { Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
// TODO: Transition all the attachments into proper layout before starting the render pass // TODO: Transition all the attachments into proper layout before starting the render pass
VkRenderPassBeginInfo renderPassBeginInfo; VkRenderPassBeginInfo renderPassBeginInfo;
@@ -1515,17 +1078,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassBeginInfo.pClearValues = clearValues.data(); renderPassBeginInfo.pClearValues = clearValues.data();
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// Pre-pass stream bookkeeping: this pass's attachment images are now
// referenced by the open frame recording.
if (s_textureManager != nullptr) {
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
if (tracked.target == TrackedAttachmentTarget::Texture) {
if (const auto texture = tracked.texture.lock()) {
s_textureManager->StampTextureRecordingUse(texture.get());
}
}
}
}
for (const auto& pending: renderPassEntry.pendingClearAttachments) { for (const auto& pending: renderPassEntry.pendingClearAttachments) {
if (pending.hasInlinePayload) { if (pending.hasInlinePayload) {
if (s_renderPassManager != nullptr) { if (s_renderPassManager != nullptr) {
@@ -1578,15 +1130,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TrackedAttachmentTarget::SwapchainColor: case TrackedAttachmentTarget::SwapchainColor:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null"); MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout); s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
// The pass stored into the attachment: its content is defined
// until the image is next presented.
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
break; break;
case TrackedAttachmentTarget::SwapchainDepthStencil: case TrackedAttachmentTarget::SwapchainDepthStencil:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null"); MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex, s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
trackedAttachment.finalLayout); trackedAttachment.finalLayout);
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
break; break;
default: default:
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d", MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
@@ -27,12 +27,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
struct PendingClearAttachmentInfo { struct PendingClearAttachmentInfo {
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
Uint32 attachmentIndex = 0; 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{}; PendingClearKey key{};
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr; MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
Bool hasInlinePayload = false; Bool hasInlinePayload = false;
@@ -42,11 +37,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct TrackedAttachmentLayoutInfo { struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture; TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> 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; WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0; Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0; Uint32 swapchainImageIndex = 0;
@@ -78,10 +68,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool hasDepthStencilAttachment = false; Bool hasDepthStencilAttachment = false;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
IntVec2 extent = {0, 0}; IntVec2 extent = {0, 0};
// VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments). Uint32 subpass = 0;
Uint32 layers = 1;
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
Uint64 lastUsedFrame = 0;
RenderPassEntry() = default; RenderPassEntry() = default;
RenderPassEntry(const RenderPassEntry&) = delete; RenderPassEntry(const RenderPassEntry&) = delete;
@@ -97,8 +84,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment); std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
std::swap(sampleCount, that.sampleCount); std::swap(sampleCount, that.sampleCount);
std::swap(extent, that.extent); std::swap(extent, that.extent);
std::swap(layers, that.layers); std::swap(subpass, that.subpass);
std::swap(lastUsedFrame, that.lastUsedFrame);
} }
RenderPassEntry( RenderPassEntry(
Uint64 hash, Uint64 hash,
@@ -111,7 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 colorAttachmentCount, Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment, Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount, VkSampleCountFlagBits sampleCount,
IntVec2 extent, Uint32 layers): IntVec2 extent, int subpass):
hash(hash), hash(hash),
renderPass(renderpass), renderPass(renderpass),
framebuffer(framebuffer), framebuffer(framebuffer),
@@ -123,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hasDepthStencilAttachment(hasDepthStencilAttachment), hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount), sampleCount(sampleCount),
extent(extent), extent(extent),
layers(layers) subpass(subpass)
{} {}
~RenderPassEntry() { ~RenderPassEntry() {
@@ -162,61 +148,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkRenderPassManager { class VkRenderPassManager {
public: public:
using HashType = Uint64; 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, VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject); VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager(); ~VkRenderPassManager();
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
Bool Initialize(); Bool Initialize();
void Shutdown(); void Shutdown();
HashType ComputeHash( HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo, const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex, Uint32 swapchainImageIndex,
Bool includePendingClear = true, Bool includePendingClear = true);
Bool includeDefaultFboDepthStencil = true); RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
// 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, void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo); const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload, void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment); const MG_State::GLState::FramebufferAttachmentObject& attachment);
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer); 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 BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
static Bool EndRenderPass(VkCommandBuffer commandBuffer); static Bool EndRenderPass(VkCommandBuffer commandBuffer);
static ActiveRenderPassInfo* GetActiveRenderPass(); static ActiveRenderPassInfo* GetActiveRenderPass();
@@ -229,22 +178,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager& m_textureManager; VkTextureManager& m_textureManager;
SwapchainObject& m_swapchainObject; SwapchainObject& m_swapchainObject;
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses; 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 // 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 // manager's image epoch this invalidates the render-pass fast path on any attachment
// image recreation. // image recreation.
Uint64 m_renderbufferImageEpoch = 1; 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 // 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 // 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 / // is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
@@ -257,26 +196,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_rpFastTexEpoch = 0; Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0; Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 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 { 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; WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr; VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE; 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; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED; VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
@@ -284,51 +209,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown; TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0; Int samples = 0;
// m_frameCounter value at which the weak reference was first seen expired.
Uint64 deadSinceFrame = kNeverObservedDead;
void Destroy(VkDevice device, VmaAllocator allocator); 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 { struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer; WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{}; ClearAttachmentPayload payload{};
}; };
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
// until enough frame boundaries have passed that no in-flight command buffer
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
struct DeferredRenderbufferRelease {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources; UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears; UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
Bool HasPendingRenderbufferClear( Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const; const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage(); 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 XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{}; static inline ActiveRenderPassInfo s_activeRenderPass{};
@@ -51,18 +51,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) { Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), 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 } // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) { Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
@@ -70,26 +58,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = initInfo.device; m_device = initInfo.device;
m_config = initInfo.config; 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, MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info"); "VkSamplerManager::Initialize failed: invalid initialization info");
return true; 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() { void VkSamplerManager::Shutdown() {
for (auto& [_, sampler] : m_samplers) { for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) { if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
@@ -101,44 +74,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE; m_device = VK_NULL_HANDLE;
m_config = nullptr; 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, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) const {
Bool forceNearestFiltering, Bool singleLevelView) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); 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(); const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter(); const auto magFilter = sampler.GetMagFilter();
@@ -151,17 +93,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT))); XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR(); const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR))); XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView); const auto maxLod = ResolveEffectiveMaxLod(sampler);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod); const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod))); XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod))); XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
const auto lodBias = sampler.GetLodBias(); const auto lodBias = sampler.GetLodBias();
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias))); XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan // Anisotropy is currently an accepted frontend-only state on DirectVulkan.
// will not apply (NEAREST filtering, or requests past the device limit) must still share one // Keep it out of the key so changing this no-op does not manufacture duplicate
// VkSampler, while two samplers that really do differ must not collide onto the first one's. // VkSamplers while sampler versioning still exposes the new frontend value.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode(); const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = ResolveCompareFunc(sampler, texture); const auto compareFunc = ResolveCompareFunc(sampler, texture);
@@ -172,44 +112,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) {
Bool forceNearestFiltering, Uint32 viewLevelCount) { const Uint64 key = BuildSamplerKey(sampler, texture);
// 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); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
return it->second.handle; return it->second.handle;
} }
VkSamplerCreateInfo samplerInfo{}; VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter()); samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter()); samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView) samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS()); samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias(); samplerInfo.mipLodBias = sampler.GetLodBias();
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two // DirectVulkan does not yet plumb samplerAnisotropy feature/limit discovery;
// different samplers or silently create duplicates. // preserve the accepted frontend state without requesting an unsupported feature.
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; samplerInfo.maxAnisotropy = 1.0f;
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture)); samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
// Must match BuildSamplerKey's resolution exactly. samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod); samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture); samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.unnormalizedCoordinates = VK_FALSE;
@@ -221,7 +146,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.handle = vkSampler; entry.handle = vkSampler;
entry.externalIndex = sampler.GetExternalIndex(); entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion(); entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
m_samplers[key] = entry; m_samplers[key] = entry;
return vkSampler; return vkSampler;
} }
@@ -24,47 +24,23 @@ public:
struct InitInfo { struct InitInfo {
VkDevice device = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE;
const VulkanRendererConfig* config = nullptr; 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); Bool Initialize(const InitInfo& initInfo);
void Shutdown(); 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, VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture);
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
// stream of never-destroyed VkSamplers and eventually exhaust the device's
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
// boundaries cannot be referenced by any in-flight command buffer (frames in
// flight are single digits), and every descriptor set the GPU consumes is
// written that same frame with live handles (the per-binding resolve memo and
// descriptor-set reuse are both frame-reset), so destruction here needs no
// fence wait. Self-gated: one counter bump and compare except on sweep
// boundaries.
void OnFrameBoundary();
private: private:
struct SamplerCacheEntry { struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE; VkSampler handle = VK_NULL_HANDLE;
Uint externalIndex = 0; Uint externalIndex = 0;
Uint16 version = 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, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) const;
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -73,20 +49,10 @@ private:
const MG_State::GLState::ITextureObject& texture); const MG_State::GLState::ITextureObject& texture);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture); 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; VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr; const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers; 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(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -13,24 +13,18 @@
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_State::GLState { namespace MobileGL::MG_State::GLState {
class ITextureObject; class ITextureObject;
} }
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
class VkTextureManager { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // 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 // 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). // invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; } 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 { struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr; MG_State::GLState::ITextureObject* texture = nullptr;
@@ -56,9 +50,6 @@ public:
VkCommandPool commandPool = VK_NULL_HANDLE; VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE; VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0; Uint32 frameCount = 0;
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
}; };
struct TextureResource { struct TextureResource {
@@ -67,16 +58,12 @@ public:
Uint32 baseArrayLayer = 0; Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1; Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; 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 { Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel && return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer && baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount && layerCount == other.layerCount &&
viewType == other.viewType && viewType == other.viewType;
viewFormat == other.viewFormat;
} }
}; };
@@ -87,63 +74,6 @@ public:
hash ^= std::hash<Uint32>{}(key.layerCount) + 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)) + hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2); 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; return hash;
} }
}; };
@@ -155,8 +85,6 @@ public:
Vector<VkImageView> perMipViews; Vector<VkImageView> perMipViews;
Vector<VkImageView> perMipSampledViews; Vector<VkImageView> perMipSampledViews;
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews; UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0}; VkExtent2D extent = {0, 0};
Uint32 depth = 1; Uint32 depth = 1;
@@ -168,26 +96,7 @@ public:
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; 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; 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; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged. // lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0; Uint64 syncedContentVersion = 0;
@@ -206,8 +115,6 @@ public:
std::swap(this->perMipViews, that.perMipViews); std::swap(this->perMipViews, that.perMipViews);
std::swap(this->perMipSampledViews, that.perMipSampledViews); std::swap(this->perMipSampledViews, that.perMipSampledViews);
std::swap(this->attachmentViews, that.attachmentViews); 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->layout, that.layout);
std::swap(this->extent, that.extent); std::swap(this->extent, that.extent);
std::swap(this->depth, that.depth); std::swap(this->depth, that.depth);
@@ -219,11 +126,7 @@ public:
std::swap(this->aspect, that.aspect); std::swap(this->aspect, that.aspect);
std::swap(this->viewType, that.viewType); std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount); 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->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
} }
@@ -250,16 +153,6 @@ public:
vkDestroyImageView(s_device, attachmentView, nullptr); 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) { if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator, image, allocation); vmaDestroyImage(s_allocator, image, allocation);
} }
@@ -268,8 +161,6 @@ public:
perMipViews.clear(); perMipViews.clear();
perMipSampledViews.clear(); perMipSampledViews.clear();
attachmentViews.clear(); attachmentViews.clear();
alternateSampledViews.clear();
storageImageViews.clear();
image = VK_NULL_HANDLE; image = VK_NULL_HANDLE;
allocation = nullptr; allocation = nullptr;
layout = VK_IMAGE_LAYOUT_UNDEFINED; layout = VK_IMAGE_LAYOUT_UNDEFINED;
@@ -283,9 +174,6 @@ public:
aspect = VK_IMAGE_ASPECT_NONE; aspect = VK_IMAGE_ASPECT_NONE;
viewType = VK_IMAGE_VIEW_TYPE_2D; viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT; sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
usageFlags = 0;
storageUsageResolved = false;
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
@@ -302,10 +190,6 @@ public:
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
void Shutdown(); void Shutdown();
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred image/view releases. Only valid when
// the caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
TextureResource* SyncTextureAndGetDescriptor( TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
@@ -314,9 +198,6 @@ public:
Uint32 baseArrayLayer, Uint32 layerCount, Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType); VkImageViewType viewType);
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); 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 UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture, MG_State::GLState::ITextureObject* texture,
@@ -325,41 +206,7 @@ public:
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
// any command in the open recording, so its out-of-pass work may safely
// execute ahead of the whole recording (in the pre command buffer).
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
// GL lets an image binding come and go, and re-creating the image every time it does
// would cost far more than the compression it wins back.
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
// True when this texture is marked but its live image predates the mark, i.e. the next sync
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); static 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, static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
@@ -396,9 +243,6 @@ public:
private: private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch(). // Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1; 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, Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource); TextureResource &outResource);
@@ -411,8 +255,7 @@ private:
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer, Uint32 baseArrayLayer,
Uint32 layerCount, Uint32 layerCount,
const VkComponentMapping* components = nullptr, const VkComponentMapping* components = nullptr) const;
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture, Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget, TextureUploadTarget uploadTarget,
TextureResource &outResource); TextureResource &outResource);
@@ -429,28 +272,18 @@ private:
void DeferViewRelease(VkImageView view); void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex); void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases(); 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); static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity); void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture); void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr; VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE; VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE; VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
Uint32 m_currentFrameIndex = 0; Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 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 // Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan). // textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false; Bool m_drawSyncScopeActive = false;
@@ -463,45 +296,9 @@ private:
TextureResource* resource = nullptr; TextureResource* resource = nullptr;
}; };
Vector<DrawSyncedTexture> m_drawSyncedThisDraw; Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects; std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources; std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
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<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases; Vector<Vector<VkImageView>> m_deferredViewReleases;
// Texture uploads are submitted out-of-band but NOT waited on (waiting
// behind the queue serialized the CPU against the previous frame's GPU
// work every time an animated atlas re-uploaded). Their transient objects
// are parked here and reclaimed once the upload fence signals.
struct PendingUploadReclaim {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VmaAllocation stagingAllocation = nullptr;
};
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -51,12 +51,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 instanceCount = 1; Uint32 instanceCount = 1;
Uint32 firstVertex = 0; Uint32 firstVertex = 0;
Uint32 firstInstance = 0; Uint32 firstInstance = 0;
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
Int32 baseVertex = 0;
Bool indexRangeIsExactView = false;
}; };
struct DrawIndexedCmdParam { struct DrawIndexedCmdParam {
@@ -76,10 +70,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum indexType = GL_UNSIGNED_SHORT; GLenum indexType = GL_UNSIGNED_SHORT;
SizeT indexByteOffset = 0; SizeT indexByteOffset = 0;
SizeT indexByteSize = 0; SizeT indexByteSize = 0;
// Interpret indexByteOffset as a raw client pointer even when an element
// array buffer is bound (backend-synthesized index lists, e.g. the
// GL_LINE_LOOP -> LINE_STRIP rewrite).
Bool forceClientMemory = false;
}; };
struct DrawIndexedCmd { struct DrawIndexedCmd {
@@ -118,10 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
}; };
class VulkanRenderer : public IBufferCopyCommandProvider, class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
public FrameContext::IRecordingObserver,
public VkRenderPassManager::IEvictionObserver,
public ProgramFactory::IEvictionObserver {
public: public:
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {}); VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
~VulkanRenderer(); ~VulkanRenderer();
@@ -138,42 +125,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recording, before any render pass. // recording, before any render pass.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override; void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
// dying handle (they share its >1024-boundary idleness, so immediate
// destruction is safe) and drop the last-pipeline memo if any went.
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
// ProgramFactory::IEvictionObserver: an aged-out program entry was
// destroyed; evict its compute pipeline and graphics pipelines (same
// idleness guarantee - they are only bound through draws/dispatches that
// stamp the program entry) and purge the descriptor-set cache entries
// keyed by its now-recyclable VkDescriptorSetLayout handle.
void OnProgramEvicted(ProgramFactory::HashType programHash,
VkDescriptorSetLayout descriptorSetLayout) override;
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects, Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr); const IndexBufferView* pIndexBufferView = nullptr);
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
// resolved draw configuration; the next draw whose cheap version/identity
// checks all match skips the resolution half (LOD probe, sampled-set
// walk, render-pass and pipeline resolution) and jumps straight to the
// per-draw tail. Returns false (leaving no side effects that the full
// path cannot redo idempotently) whenever anything might have changed.
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
const RenderPassEntry& compatibleRenderPassEntry); const RenderPassEntry& compatibleRenderPassEntry);
enum class ScissoredClearPrep {
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
NoOp, // nothing to clear (degenerate target or empty scissor rect)
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
};
ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
VkClearRect& outClearRect);
void Clear(GLbitfield mask); void Clear(GLbitfield mask);
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);
@@ -200,30 +157,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); 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* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
// CPU repacking into the requested client layout).
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels);
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
// expects command recording to be active and any render pass already ended.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels);
// Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type, TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
@@ -286,9 +219,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// frontend. Timestamp support (queue timestampValidBits > 0 and a // frontend. Timestamp support (queue timestampValidBits > 0 and a
// non-zero timestampPeriod) is cached at device creation. // non-zero timestampPeriod) is cached at device creation.
Bool IsTimerQuerySupported() const; Bool IsTimerQuerySupported() const;
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// Ensures the frame command buffer is recording (same lazy pattern as // Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current // SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted. // frame's pool. Null when unsupported or the pool is exhausted.
@@ -304,23 +234,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkTimerQueryManager::TimestampRecord& end) const; const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const; Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
// unsupported) when the device lacks it.
Bool StartOcclusionQueryCapture();
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
// Flushes pending commands, waits, sums the slots, and recycles them.
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height); void RequestSwapchainResize(Uint32 width, Uint32 height);
// Re-query the surface and report whether the live swapchain no longer matches it void RecreateSwapchain();
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
Bool SwapchainIsOutOfDate();
// Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain();
private: private:
struct BlitUniformData { struct BlitUniformData {
@@ -360,10 +275,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer, void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer, GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload); const ClearAttachmentPayload& clearPayload);
void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload,
const VkClearRect& clearRect);
// ---- Submission fence tracking (GL sync objects) ---- // ---- Submission fence tracking (GL sync objects) ----
// One record per vkQueueSubmit still in flight, in ascending submit // One record per vkQueueSubmit still in flight, in ascending submit
@@ -406,55 +317,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFence AcquirePooledSubmitFence(); VkFence AcquirePooledSubmitFence();
void DestroySubmitFencePool(); void DestroySubmitFencePool();
Bool HasPendingRecordedWork() const; Bool HasPendingRecordedWork() const;
// Frame-boundary housekeeping for paths that never reach Present's
// tail (present-less readback loops, suspended presentation, blocking
// sync waits): runs the same per-frame drains Present performs, but
// only when every queue submission has been observed complete AND no
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
// is provably already zero. Never blocks (non-blocking fence poll
// only), so the presenting path's frames-in-flight pipelining is
// untouched. Returns true when the drain ran.
Bool TryDrainFrameTransients();
Vector<SubmitRecord> m_inFlightSubmits; Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences; Vector<VkFence> m_freeSubmitFences;
Uint64 m_submitCounter = 0; Uint64 m_submitCounter = 0;
Uint64 m_completedSubmitCounter = 0; Uint64 m_completedSubmitCounter = 0;
// Drains since the last Present, gating the drain's frame-boundary-equivalent
// work (arena rewind + cache aging): a presenting app's mid-frame
// readbacks/waits must neither churn the transient caches nor accelerate the
// aging clocks, while present-less loops still cross a boundary every few
// iterations. Reset in Present.
Uint32 m_drainsSinceLastPresent = 0;
NativeWindowType m_window = 0; NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr; void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr; void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr; void* m_platformCloseDisplay = nullptr;
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
// enumerated instance extensions; when false, CreateSurface() falls back to a
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
Bool m_headlessSurfaceSupported = true;
// Set when CreateSurface() had to create its own Xlib window for the fallback
// above (rather than being handed one by the caller), so Shutdown() knows it
// owns that window and must destroy it.
Bool m_ownsFallbackXlibWindow = false;
VulkanRendererConfig m_config; VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false; Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
// submitting on a signaled fence / presenting never-acquired images.
Bool m_presentSuspended = false;
// Vulkan objects // Vulkan objects
Bool m_validationLayersEnabled = false; Bool m_validationLayersEnabled = false;
Vector<VkExtensionProperties> m_extensions; Vector<VkExtensionProperties> m_extensions;
VkInstance m_instance = VK_NULL_HANDLE; VkInstance m_instance = VK_NULL_HANDLE;
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE; VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
// Fallback reporting channel for drivers that ship the validation layers but
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
PhysicalDevice m_physicalDevice; PhysicalDevice m_physicalDevice;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr; VmaAllocator m_allocator = nullptr;
@@ -467,10 +347,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_indexTypeUint8ExtensionEnabled = false; Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false; Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false; Bool m_multiDrawIndirectFeatureEnabled = false;
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false;
Bool m_unformattedFloatStorageImagesEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them. // drive a runtime fallback when the device lacks them.
@@ -492,59 +370,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 stride); Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr; static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
// VK_EXT_transform_feedback (GL transform feedback capture)
Bool m_transformFeedbackFeatureEnabled = false;
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
// Counter buffers (one 4-byte slot per capture binding) let consecutive
// draws within one glBeginTransformFeedback append GL-style.
VkBufferObject m_xfbCounterBuffer;
// Non-zero while inside a GL Begin/End with at least one captured draw
// recorded; selects counter-buffer resume on the next captured draw.
Bool m_xfbCountersValid = false;
Uint64 m_xfbLastSeenGeneration = 0;
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
// when GL transform feedback is active; binds capture buffers on demand.
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
// query is active. Returns whether a slot was begun (End must mirror it).
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
Bool m_occlusionQueryPreciseEnabled = false;
Bool m_hostQueryResetEnabled = false;
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kOcclusionQuerySlots = 8192;
Uint32 m_occlusionSlotCursor = 0;
Bool m_occlusionCaptureActive = false;
Vector<Uint32> m_occlusionActiveSlots;
// Transform feedback primitive queries: one pool slot per captured draw yields
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
// unlike the CPU fallback accounting.
Bool m_xfbQueriesSupported = false;
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kXfbQuerySlots = 8192;
Uint32 m_xfbQuerySlotCursor = 0;
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0;
public:
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE; VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkBufferManager m_bufferManager; VkBufferManager m_bufferManager;
@@ -557,30 +382,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline // gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
// state is unchanged from the previous draw. The key provably covers every pipeline field. // state is unchanged from the previous draw. The key provably covers every pipeline field.
// Reset per-frame and on pipeline destruction so the cached handle can never dangle. // Reset per-frame and on pipeline destruction so the cached handle can never dangle.
// Small N-way pipeline-resolution memo (round-robin replacement). A Bool m_lastPipelineValid = false;
// single-entry memo thrashed on draw sequences that alternate a few GLenum m_lastPipelineMode = 0;
// pipelines (GUI text/quad program ping-pong), paying the full Uint64 m_lastPipelineProgramHash = 0;
// payload-hash lookup per draw; eight entries cover such working sets Uint64 m_lastPipelineVertexInputHash = 0;
// while keeping the hit path a trivial linear scan. Uint64 m_lastPipelineRenderPassHash = 0;
struct PipelineMemoEntry { Uint m_lastPipelineRenderStateVersion = 0;
GLenum mode = 0; ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
Uint64 programHash = 0; VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
Uint64 vertexInputHash = 0;
Uint64 renderPassHash = 0;
Uint renderStateVersion = 0;
ProgramFactory::CompileOptionFlags transformFlags = {};
VkPipeline pipeline = VK_NULL_HANDLE;
};
static constexpr Uint32 kPipelineMemoSize = 8;
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
Uint32 m_pipelineMemoCount = 0;
Uint32 m_pipelineMemoNext = 0;
// Drops every memoized pipeline handle. Required at command-buffer
// boundaries and whenever any pipeline may have been destroyed.
void InvalidatePipelineMemo() {
m_pipelineMemoCount = 0;
m_pipelineMemoNext = 0;
}
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines; UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
UniquePtr<ProgramFactory> m_programFactory; UniquePtr<ProgramFactory> m_programFactory;
UniquePtr<UniformManager> m_uniformManager; UniquePtr<UniformManager> m_uniformManager;
@@ -609,125 +418,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0; Uint64 m_lastSampledSetBindGeneration = 0;
// Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
// sampled-set memo, plus the sampled textures' params-version sum so a
// level-range or filter change re-probes. On a hit the resolved
// transform flags are reused, which also collapses the two
// GetOrCreateProgram lookups into one.
Bool m_lastLodDecisionValid = false;
Uint64 m_lastLodProgramLifetimeId = 0;
Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
// render-pass caches are open-addressing maps whose entries move on
// insert, so no pointers into them are cached; the pipeline handle is
// protected by the command-buffer-boundary reset plus the mid-frame
// pipeline-destruction resets, and monotonic epochs guard everything
// that can be destroyed or recreated between draws.
struct SetupDrawSnapshot {
Bool valid = false;
Uint8 aspects = 0;
GLenum mode = 0;
Uint64 programLifetimeId = 0;
Uint32 programVersion = 0;
const void* vao = nullptr;
Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr;
Uint16 fboVersion = 0;
Bool drawFboIsDefault = false;
Uint renderStateVersion = 0;
Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0;
Uint64 renderPassHash = 0;
Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0;
Uint64 textureImageEpoch = 0;
Uint64 renderbufferImageEpoch = 0;
Uint64 sampledContentSum = 0;
Uint64 sampledParamsSum = 0;
IntVec2 renderPassExtent = {0, 0};
VkPipeline pipeline = VK_NULL_HANDLE;
};
SetupDrawSnapshot m_setupDrawSnapshot;
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every // Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate. // draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch; Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
// first sampled-texture loop: the resolved backend resources, so the
// post-transition loop can skip re-resolving textures whose layout is
// already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch; Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch; Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch; Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
Vector<Float> m_vertexConversionScratch;
Vector<Uint8> m_vertexRepackScratch;
struct ConvertedVertexStreamKey {
const MG_State::GLState::BufferObject* buffer = nullptr;
Uint64 changeSerial = 0;
SizeT baseOffset = 0;
Uint32 sourceStride = 0;
DataType type = DataType::Float32;
Int size = 0;
Bool normalized = false;
Bool isInteger = false;
VertexInputStateFactory::VertexStreamConversion conversion =
VertexInputStateFactory::VertexStreamConversion::None;
Bool operator==(const ConvertedVertexStreamKey& other) const {
return buffer == other.buffer && changeSerial == other.changeSerial &&
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
type == other.type && size == other.size && normalized == other.normalized &&
isInteger == other.isInteger && conversion == other.conversion;
}
};
struct ConvertedVertexStreamKeyHash {
SizeT operator()(const ConvertedVertexStreamKey& key) const {
SizeT hash = std::hash<const void*>{}(key.buffer);
auto combine = [&hash](SizeT value) {
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
};
combine(std::hash<Uint64>{}(key.changeSerial));
combine(std::hash<SizeT>{}(key.baseOffset));
combine(std::hash<Uint32>{}(key.sourceStride));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
combine(std::hash<Int>{}(key.size));
combine(std::hash<Bool>{}(key.normalized));
combine(std::hash<Bool>{}(key.isInteger));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
return hash;
}
};
struct ConvertedVertexStream {
BufferSlice slice;
// Number of source elements the cached slice covers. A draw needing a prefix of
// this range reuses the slice (converted streams are tightly packed); a draw
// needing more reconverts and replaces the entry, so per (buffer, layout) a
// frame converts at most the largest range any draw asked for.
SizeT elementCount = 0;
// Pins the source buffer for the frame so its heap address cannot be reused by
// a new BufferObject while this pointer-keyed entry is alive.
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
};
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
m_convertedVertexStreams;
void CreateInstance(); void CreateInstance();
VkResult SetupDebugMessenger(); VkResult SetupDebugMessenger();
VkResult DestroyDebugMessenger(); VkResult DestroyDebugMessenger();
VkResult SetupDebugReportCallback();
void DestroyDebugReportCallback();
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo(); VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
void CreateSurface(); void CreateSurface();
void PickPhysicalDevice(); void PickPhysicalDevice();
@@ -746,17 +446,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); void DestroyComputePipelines();
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
// flush the pending recording (see the body), which retires the current command buffer.
Bool PrepareStorageImageTextures(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams, const DrawCmdParam& drawParams);
const IndexBufferView* pIndexBufferView);
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame, Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView = nullptr); const IndexBufferView* pIndexBufferView = nullptr);
@@ -774,9 +467,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum filter); GLenum filter);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry); VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture, MG_State::GLState::ITextureObject& texture,
@@ -811,10 +501,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const PhysicalDevice& compareWithDevice, const PhysicalDevice& compareWithDevice,
PhysicalDevice& outBetterDevice); PhysicalDevice& outBetterDevice);
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"}; static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
Bool m_imageFormatListExtensionEnabled = false;
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
static Bool CheckValidationLayerSupport(); static Bool CheckValidationLayerSupport();
+3 -32
View File
@@ -52,48 +52,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
namespace MobileGL::MG_Backend::DirectVulkan {
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
// attachment view (and render pass format) therefore drops to the UNORM twin
// whenever the capability is off. Sampled views keep the sRGB format (decode on
// sample is unconditional in GL).
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
if (framebufferSrgbEnabled) return format;
switch (format) {
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_UNORM;
default:
return format;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
#define VK_VERIFY(expr, ...) \ #define VK_VERIFY(expr, ...) \
do { \ do { \
VkResult _vk_verify_result = (expr); \ VkResult _vk_verify_result = (expr); \
if (_vk_verify_result != VK_SUCCESS) { \ if (_vk_verify_result != VK_SUCCESS) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \ MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \ MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \ _vk_verify_result, __FILE__, __LINE__); \
} \ } \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \ MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} while (0) } while (0)
#define XXHASH_VERIFY(expr, ...) \ #define XXHASH_VERIFY(expr, ...) \
do { \ do { \
XXH_errorcode _xxh_verify_result = (expr); \ XXH_errorcode _xxh_verify_result = (expr); \
if (_xxh_verify_result != XXH_OK) { \ MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
__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) } while (0)
@@ -17,4 +17,3 @@ target_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
View File
@@ -38,7 +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)
@@ -17,4 +17,3 @@ target_link_libraries(
) )
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true) add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
set_tests_properties(ProgramBench PROPERTIES LABELS benchmark)
-33
View File
@@ -24,7 +24,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
GLint Samples = 0; GLint Samples = 0;
GLint Profile = kCGLOGLPVersion_3_2_Core; GLint Profile = kCGLOGLPVersion_3_2_Core;
GLint RendererId = 0x4d474c; GLint RendererId = 0x4d474c;
GLint DisplayMask = 0;
}; };
struct ContextObject { struct ContextObject {
@@ -135,9 +134,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID: case kCGLPFARendererID:
pixelFormat.RendererId = value; pixelFormat.RendererId = value;
break; break;
case kCGLPFADisplayMask:
pixelFormat.DisplayMask = value;
break;
default: default:
break; break;
} }
@@ -347,9 +343,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID: case kCGLPFARendererID:
*value = pixelFormat->RendererId; *value = pixelFormat->RendererId;
return kCGLNoError; return kCGLNoError;
case kCGLPFADisplayMask:
*value = pixelFormat->DisplayMask;
return kCGLNoError;
case kCGLPFAOpenGLProfile: case kCGLPFAOpenGLProfile:
*value = pixelFormat->Profile; *value = pixelFormat->Profile;
return kCGLNoError; return kCGLNoError;
@@ -488,32 +481,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
return it == currentContexts.end() ? nullptr : it->second; return it == currentContexts.end() ? nullptr : it->second;
} }
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (screen != 0) {
return kCGLBadValue;
}
object->VirtualScreen = screen;
return kCGLNoError;
}
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!screen) {
return kCGLBadAddress;
}
*screen = object->VirtualScreen;
return kCGLNoError;
}
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex()); const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx); auto* object = TryGetContext(ctx);
-2
View File
@@ -32,8 +32,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
CGLError SetCurrentContext(CGLContextObj ctx); CGLError SetCurrentContext(CGLContextObj ctx);
CGLContextObj GetCurrentContext(); CGLContextObj GetCurrentContext();
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params); CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params); CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
CGLError UpdateContext(CGLContextObj ctx); CGLError UpdateContext(CGLContextObj ctx);
@@ -71,14 +71,6 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext(); return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
} }
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params); return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
} }
@@ -10,12 +10,8 @@
#if defined(__APPLE__) #if defined(__APPLE__)
#include "MG_Impl/CGLImpl/CGLImpl.h"
#include "MG_Impl/GetProcAddress.h" #include "MG_Impl/GetProcAddress.h"
#include <CoreGraphics/CoreGraphics.h>
#include <CoreVideo/CVDisplayLink.h>
#include <cstdint>
#include <dlfcn.h> #include <dlfcn.h>
namespace { namespace {
@@ -51,52 +47,10 @@ namespace {
return dlsym(handle, symbol); return dlsym(handle, symbol);
} }
CGDirectDisplayID DisplayForMask(GLint displayMask) {
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
CGDirectDisplayID displays[MaxDisplays] = {};
std::uint32_t displayCount = 0;
if (displayMask != 0 &&
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
for (std::uint32_t i = 0; i < displayCount; ++i) {
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
return displays[i];
}
}
}
return CGMainDisplayID();
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
CVDisplayLinkRef displayLink,
CGLContextObj context,
CGLPixelFormatObj pixelFormat) {
GLint virtualScreen = 0;
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
GLint displayMask = 0;
if (!displayLink ||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
return kCVReturnInvalidArgument;
}
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
}
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
static const auto original = reinterpret_cast<OriginalFunction>(
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
}
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[] __attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
__attribute__((section("__DATA,__interpose"))) = { __attribute__((section("__DATA,__interpose"))) = {
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)}, {reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
}; };
#pragma clang diagnostic pop
} // namespace } // namespace
#endif #endif
@@ -1,10 +0,0 @@
# Public CGL entry points.
_CGL*
# Public EGL entry points.
_egl*
# Public OpenGL and GLX entry points. OpenGL function names always use an
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
# deliberately prevents glslang_* from matching this pattern.
_gl[A-Z0-9]*
+5 -28
View File
@@ -8,7 +8,6 @@
#include "EGLImpl.h" #include "EGLImpl.h"
#include "../GetProcAddress.h" #include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h> #include <MG_State/EGLState/Core.h>
#include <mutex> #include <mutex>
@@ -26,17 +25,6 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get(); return MG_State::pEGLContext.get();
} }
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) { MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get(); auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) { if (!backendObject && state) {
@@ -61,8 +49,6 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android; return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__) #elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer; return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__) #elif defined(__linux__)
return MG_Backend::WindowBackend::X11; return MG_Backend::WindowBackend::X11;
#else #else
@@ -201,7 +187,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetStateEnsureInitialized(); auto* state = GetState();
if (!state) { if (!state) {
return EGL_FALSE; return EGL_FALSE;
} }
@@ -222,7 +208,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLDisplay GetDisplay(NativeDisplayType display) { EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetStateEnsureInitialized(); auto* state = GetState();
if (!state) { if (!state) {
return EGL_NO_DISPLAY; return EGL_NO_DISPLAY;
} }
@@ -327,14 +313,6 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources(); backendObject->ReleaseEGLResources();
} }
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE; return EGL_TRUE;
} }
@@ -367,7 +345,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLBoolean BindAPI(EGLenum api) { EGLBoolean BindAPI(EGLenum api) {
auto* state = GetStateEnsureInitialized(); auto* state = GetState();
if (!state) { if (!state) {
return EGL_FALSE; return EGL_FALSE;
} }
@@ -400,7 +378,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
char const* QueryString(EGLDisplay display, EGLint name) { char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetStateEnsureInitialized(); auto* state = GetState();
if (!state) { if (!state) {
return nullptr; return nullptr;
} }
@@ -663,7 +641,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) { EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list; (void)attrib_list;
auto* state = GetStateEnsureInitialized(); auto* state = GetState();
if (!state) { if (!state) {
return EGL_NO_DISPLAY; return EGL_NO_DISPLAY;
} }
@@ -759,7 +737,6 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) { if (!name) {
return nullptr; return nullptr;
} }
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name); MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name); void* proc = MG_Impl::GetProcAddress(name);
+1 -20
View File
@@ -441,10 +441,7 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) { for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = buffers[i]; Uint bufferName = buffers[i];
if (bufferName == 0) continue; if (bufferName == 0) continue;
// GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently if (!BufferImpl::ValidateBufferName(bufferName, true)) continue;
// ignored here, so probe with the non-recording query - the shared validator would
// record INVALID_OPERATION, which is only correct on the bind path.
if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue;
MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName); MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName);
} }
} }
@@ -1351,14 +1348,6 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target); BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return; if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return; if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex); MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex); auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
@@ -1392,14 +1381,6 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target); BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return; if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return; if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index); MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index); auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
@@ -60,11 +60,6 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings; MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0))); pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
} }
if (target == BufferTarget::TransformFeedback) {
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
// binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4);
}
if (index < pointCount) { if (index < pointCount) {
return true; return true;
+10 -228
View File
@@ -48,73 +48,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
// Primitives a draw of `count` vertices in `mode` assembles (0 for
// incomplete primitives). Used for the CPU-side transform feedback
// primitive accounting.
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
if (count <= 0) return 0;
switch (mode) {
case GL_POINTS: return static_cast<Uint64>(count);
case GL_LINES: return static_cast<Uint64>(count / 2);
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
default: return 0;
}
}
// Accumulate the transform feedback primitive counter for a captured draw.
// Draws without a geometry stage write exactly the primitives they assemble,
// clamped by the capture buffers' remaining capacity (a full buffer stops
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
// Geometry amplification is not modelled here.
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
Uint64 primitives = CountPrimitivesForDraw(mode, count);
if (primitives == 0) return;
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
Uint64 verticesPerPrimitive = 1;
switch (mode) {
case GL_LINES:
case GL_LINE_STRIP:
case GL_LINE_LOOP:
verticesPerPrimitive = 2;
break;
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
verticesPerPrimitive = 3;
break;
default:
break;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) {
// Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
if (stride == 0) continue;
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
const Range1D range = point.GetRange();
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
}
if (capacityVertices != ~0ull) {
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
}
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) { static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject; const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) { if (!activeBackendObject) {
@@ -124,8 +57,17 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
return false;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) { if (MG_State::pEGLContext->IsCurrentContextOpenGLCoreProfile() && vao && vao->GetExternalIndex() == 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
@@ -133,38 +75,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// While transform feedback is active the draw's primitive type must match
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
// the constraint moves to the shader's output primitive type instead, so
// the draw mode itself is unconstrained here.
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
Bool compatible = false;
switch (feedbackMode) {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
break;
default:
break;
}
if (!compatible) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode is incompatible with the active transform feedback primitive mode."));
return false;
}
}
return true; return true;
} }
@@ -492,14 +402,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex); DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
} }
void DrawArrays(GLenum mode, GLint first, GLsizei count) { void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawArrays_Backend(mode, first, count); DrawArrays_Backend(mode, first, count);
} }
@@ -536,133 +444,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return; if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return; if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices); DrawElements_Backend(mode, count, type, indices);
} }
void BeginTransformFeedback(GLenum primitiveMode) {
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
return;
}
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
return;
}
const auto& program = MG_State::pGLContext->GetCurrentProgram();
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"No program with transform feedback varyings is active."));
return;
}
// Every capture buffer slot the program's mode uses must have a buffer bound.
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < usedBufferCount; ++i) {
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
if (point.GetBoundObject() == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
return;
}
}
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
}
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
// lengths the captured records are reordered in place: swap the first two
// vertex records of every odd triangle within each emitted strip.
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
Uint64 inputPrimitives) {
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
return;
}
const auto& stripTriangles = program->GetGsStripTriangles();
// Global triangle indices whose leading vertex pair must swap.
Vector<Uint64> swapTriangles;
Uint64 triangleBase = 0;
for (Uint64 input = 0; input < inputPrimitives; ++input) {
for (const Uint32 stripLength : stripTriangles) {
for (Uint32 t = 1; t < stripLength; t += 2) {
swapTriangles.push_back(triangleBase + t);
}
triangleBase += stripLength;
}
}
if (swapTriangles.empty()) {
return;
}
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
if (stride == 0) continue;
const auto& bindingPoint =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(bufferIndex));
const auto& buffer = bindingPoint.GetBoundObject();
if (buffer == nullptr) continue;
const Range1D range = bindingPoint.GetRange();
const Uint8* mapped = buffer->MappedData();
if (mapped == nullptr) continue;
// The geometry stage amplifies, so the CPU vertex counter does not bound
// the capture; the binding range's whole-triangle capacity does.
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
// (winding preserved by swapping the trailing pair); GL wants
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
Vector<Uint8> scratch(stride);
for (const Uint64 triangle : swapTriangles) {
if (triangle >= capturedTriangles) break;
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
const SizeT v1Offset = v0Offset + stride;
const SizeT v2Offset = v1Offset + stride;
Memcpy(scratch.data(), mapped + v2Offset, stride);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
}
}
}
void EndTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
return;
}
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
MG_State::pGLContext->EndTransformFeedback();
// Captured results must be visible to MapBuffer/GetBufferSubData after
// End; the capture targets are host-coherent GPU memory, so completing
// the GPU work is all that is required.
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
if (auto sync = backendGL.FenceSync()) {
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
if (backendGL.DeleteSync) {
backendGL.DeleteSync(sync);
}
}
}
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
}
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -11,8 +11,6 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback(void);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ); void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect); void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers); void MemoryBarrier(GLbitfield barriers);
@@ -236,12 +236,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays) DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array) DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data) DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode) DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer) DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode) DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
@@ -295,13 +295,7 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
// Transform feedback objects are not implemented, so no name is ever a live object. The shared DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
// is both truthful and what the spec requires for a name that was never generated.
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
@@ -424,7 +418,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level) DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW) DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
@@ -1004,8 +998,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
@@ -1069,7 +1063,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture,
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
@@ -2589,10 +2583,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) { DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, ) DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, ) DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id) DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
@@ -45,32 +45,10 @@ namespace MobileGL::MG_Impl::GLImpl {
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel(); depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
} }
// Mirrors the renderer-side gate: distinct depth/stencil renderbuffers (or a
// renderbuffer paired with a texture) cannot form one Vulkan depth-stencil
// attachment, and GL permits reporting such framebuffers as UNSUPPORTED.
Bool HasDistinctCompleteDepthStencilRenderbufferAttachments(
const MG_State::GLState::FramebufferObject& framebufferObject) {
if (framebufferObject.GetExternalIndex() == 0) {
return false;
}
const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth);
const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil);
if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) {
return false;
}
if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) {
return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get();
}
return (depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) &&
(depthAttachment.IsTexture() || stencilAttachment.IsTexture());
}
Bool IsUnsupportedFramebufferForDirectVulkan( Bool IsUnsupportedFramebufferForDirectVulkan(
const MG_State::GLState::FramebufferObject& framebufferObject) { const MG_State::GLState::FramebufferObject& framebufferObject) {
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands. // TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) || return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject);
HasDistinctCompleteDepthStencilRenderbufferAttachments(framebufferObject);
} }
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) { Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
@@ -82,235 +60,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// Whether the backend can actually attach this color format to a framebuffer. Preferred
// source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus
// probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend
// cannot answer for fall back to a conservative static list of formats no ES driver renders to:
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
const SizeT formatIndex = static_cast<SizeT>(format);
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
const SizeT sentinelFormat = static_cast<SizeT>(TextureInternalFormat::RGBA8);
Bool cachePopulated = false;
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated;
++targetIndex) {
cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat],
MG_Backend::FormatCapability::Creatable);
}
if (cachePopulated) {
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
++targetIndex) {
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable) ||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable)) {
return true;
}
}
return false;
}
}
switch (format) {
case TextureInternalFormat::RGB9E5:
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16
case TextureInternalFormat::RGB12: // stored as RGB16
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::SRGB8:
return false;
default:
return true;
}
}
Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
const auto& attachments = framebufferObject.GetAllAttachmentObjects();
for (SizeT i = 0; i < attachments.size(); ++i) {
const auto type = static_cast<FramebufferAttachmentType>(i);
if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) {
continue;
}
const auto& attachment = attachments[i];
if (!attachment.IsValid()) continue;
TextureInternalFormat format = TextureInternalFormat::Unknown;
if (attachment.IsTexture() && attachment.GetTexture()) {
format = attachment.GetTexture()->GetFormat();
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
format = attachment.GetRenderbuffer()->GetInternalFormat();
}
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
return true;
}
}
return false;
}
void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) { void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail)); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
} }
GLint ClassifyAttachmentComponentType(TextureInternalFormat internalFormat,
FramebufferAttachmentType attachmentType) {
// A stencil value is an unsigned integer index regardless of the depth half
// of a packed format.
if (attachmentType == FramebufferAttachmentType::Stencil) return GL_UNSIGNED_INT;
switch (internalFormat) {
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG16F:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGBA16F:
case TextureInternalFormat::R32F:
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGBA32F:
case TextureInternalFormat::R11FG11FB10F:
case TextureInternalFormat::RGB9E5:
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth32FStencil8:
return GL_FLOAT;
case TextureInternalFormat::R8I:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R32I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA32I:
return GL_INT;
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return GL_UNSIGNED_INT;
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16Snorm:
return GL_SIGNED_NORMALIZED;
case TextureInternalFormat::Unknown:
return GL_NONE;
default:
return GL_UNSIGNED_NORMALIZED;
}
}
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
// and its DSA variant. Returns true when pname was one of them.
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
GLenum pname, GLint* params, const char* caller) {
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
break;
default:
return false;
}
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
// The depth and stencil components have different types, so the combined
// attachment name has no single answer.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
"GL_DEPTH_STENCIL_ATTACHMENT."));
return true;
}
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"No image is attached to the queried attachment point."));
return true;
}
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
if (attachmentObject->IsTexture() && attachmentObject->GetTexture()) {
internalFormat = attachmentObject->GetTexture()->GetFormat();
} else if (attachmentObject->IsRenderbuffer() && attachmentObject->GetRenderbuffer()) {
internalFormat = attachmentObject->GetRenderbuffer()->GetInternalFormat();
}
const auto sizes = MG_Util::GetComponentSizesForInternalFormat(internalFormat);
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
*params = sizes.Red;
break;
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
*params = sizes.Green;
break;
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
*params = sizes.Blue;
break;
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
*params = sizes.Alpha;
break;
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
*params = sizes.Depth;
break;
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
*params = sizes.Stencil;
break;
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
*params = (internalFormat == TextureInternalFormat::SRGB8 ||
internalFormat == TextureInternalFormat::SRGB8Alpha8)
? GL_SRGB
: GL_LINEAR;
break;
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
*params = ClassifyAttachmentComponentType(internalFormat, attachmentType);
break;
}
return true;
}
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject, Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
TextureUploadTarget& outUploadTarget, TextureUploadTarget& outUploadTarget,
Bool& outLayered) { Bool& outLayered) {
@@ -576,27 +331,6 @@ namespace MobileGL::MG_Impl::GLImpl {
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target); FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return; if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
// Default-framebuffer attachment names (GL_DEPTH, GL_STENCIL, GL_FRONT/GL_BACK
// variants) alias onto the equivalent attachment points.
switch (attachment) {
case GL_DEPTH:
attachment = GL_DEPTH_ATTACHMENT;
break;
case GL_STENCIL:
attachment = GL_STENCIL_ATTACHMENT;
break;
case GL_FRONT:
case GL_FRONT_LEFT:
case GL_FRONT_RIGHT:
case GL_BACK:
case GL_BACK_LEFT:
case GL_BACK_RIGHT:
attachment = GL_COLOR_ATTACHMENT0;
break;
default:
break;
}
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT; const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
FramebufferAttachmentType attachmentType = depthStencilAlias FramebufferAttachmentType attachmentType = depthStencilAlias
? FramebufferAttachmentType::Depth ? FramebufferAttachmentType::Depth
@@ -613,40 +347,20 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* { const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) { if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType); return &framebufferObject->GetAttachment(attachmentType);
} }
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth); const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil); const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty(); if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr; return nullptr;
}(); }();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) { switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) { if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -710,10 +424,6 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE; : GL_FALSE;
break; break;
default: default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
"GetFramebufferAttachmentParameteriv_State")) {
return;
}
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -846,62 +556,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return framebufferObject; return framebufferObject;
} }
// Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer
// index; the DirectGLES backend attaches it with glFramebufferTextureLayer.
static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level, GLint layer,
TextureUploadTarget textureUploadTarget) {
if (target == GL_FRAMEBUFFER) {
target = GL_DRAW_FRAMEBUFFER;
}
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer,
textureUploadTarget);
AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer,
textureUploadTarget);
return;
}
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) {
framebufferObject->Detach(attachmentType);
return;
}
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
std::format("Texture object {} is not valid.", texture)));
return;
}
if (layer < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Layer must be non-negative."));
return;
}
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
/*layered=*/false);
}
void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) { void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
if (texture == 0) { if (texture == 0) {
const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D; const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D;
@@ -909,31 +563,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); static_cast<void>(layer);
if (!textureObject) { RecordUnsupportedFramebufferTextureAttachmentError(
MG_State::pGLContext->RecordError( __func__,
ErrorCode::InvalidOperation, "Layered framebuffer texture attachments are not represented by the current framebuffer attachment model.");
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::format("Texture object {} is not valid.", texture)));
return;
}
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
switch (textureObject->GetTarget()) {
case TextureTarget::Texture3D:
textureUploadTarget = TextureUploadTarget::Texture3D;
break;
case TextureTarget::Texture2DArray:
textureUploadTarget = TextureUploadTarget::Texture2DArray;
break;
case TextureTarget::Texture2DMultisampleArray:
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
break;
default:
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
} }
void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
@@ -945,15 +578,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
if (textarget != GL_TEXTURE_3D) { static_cast<void>(zoffset);
MG_State::pGLContext->RecordError( RecordUnsupportedFramebufferTextureAttachmentError(
ErrorCode::InvalidEnum, __func__,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model.");
"FramebufferTexture3D requires GL_TEXTURE_3D."));
return;
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset,
TextureUploadTarget::Texture3D);
} }
void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) { void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
@@ -1386,7 +1014,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
auto& fbo = bindingSlot.GetBoundObject(); auto& fbo = bindingSlot.GetBoundObject();
const bool isDefaultFBO = (fbo == FramebufferImpl::pDefaultFramebufferInfo->defaultFBO); const bool isDefaultFBO = (fbo == FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
const GLenum bufs[] = {buf}; static GLenum bufs[] = {buf};
DrawBuffersForFramebuffer_State(fbo, isDefaultFBO, 1, bufs, true); DrawBuffersForFramebuffer_State(fbo, isDefaultFBO, 1, bufs, true);
} }
} }
@@ -1573,9 +1201,7 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) { for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = renderbuffers[i]; Uint bufferName = renderbuffers[i];
if (bufferName == 0) continue; if (bufferName == 0) continue;
// GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue;
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue;
MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName); MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName);
} }
} }
@@ -1598,9 +1224,7 @@ namespace MobileGL::MG_Impl::GLImpl {
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) { for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
Uint bufferName = framebuffers[i]; Uint bufferName = framebuffers[i];
if (bufferName == 0) continue; if (bufferName == 0) continue;
// GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue;
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue;
MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName); MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName);
} }
} }
@@ -1628,9 +1252,6 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
} }
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() && if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED; return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1656,9 +1277,6 @@ namespace MobileGL::MG_Impl::GLImpl {
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
} }
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() && if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) { IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED; return GL_FRAMEBUFFER_UNSUPPORTED;
@@ -1677,40 +1295,20 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment); : MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return; if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* { const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) { if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType); return &framebufferObject->GetAttachment(attachmentType);
} }
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth); const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil); const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty(); if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr; return nullptr;
}(); }();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) { switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) { if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -1756,10 +1354,6 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE; : GL_FALSE;
break; break;
default: default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
caller)) {
return;
}
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
@@ -2032,8 +1626,8 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
// Check framebuffer completeness (including formats the ES pipeline cannot attach) // Check framebuffer completeness
if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) { if (!framebufferObject->CheckCompleteness()) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidFramebufferOperation, ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete"));
@@ -2085,40 +1679,6 @@ namespace MobileGL::MG_Impl::GLImpl {
"No color buffer for color format")); "No color buffer for color format"));
return false; return false;
} }
// GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read
// buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error
// for every *_INTEGER readback from a normalized attachment).
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown;
if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
attachmentFormat = readAttachment.GetTexture()->GetFormat();
} else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat();
}
if (attachmentFormat != TextureInternalFormat::Unknown &&
TextureImpl::IsIntegerColorInputFormat(textureInputFormat) !=
TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State",
"Integer-ness of format does not match the read buffer"));
return false;
}
}
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
return false;
}
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
return false;
} }
// Check PBO state // Check PBO state
@@ -2377,7 +1937,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
namespace FramebufferImpl { namespace FramebufferImpl {
// Leak-at-exit storage; see GlobalObjects.cpp. UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
} // namespace FramebufferImpl } // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment; SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
}; };
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo; extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
} // namespace FramebufferImpl } // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -76,13 +76,7 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
} }
Bool ValidateRenderbufferName(Uint index, Bool allowZero) { Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
if (index == 0) { if (index == 0 && !allowZero) {
// Zero is never a GenRenderbuffers name, so it must not reach the name-table lookup
// below: where it is allowed (glBindRenderbuffer / FramebufferRenderbuffer detach) it
// means "unbind", and looking it up would record a bogus INVALID_OPERATION - GL CTS's
// per-case state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) after every case.
if (allowZero) return true;
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName", MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
+9 -92
View File
@@ -213,13 +213,8 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint maxSamples = 0; GLint maxSamples = 0;
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) { for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) { if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples())); maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
} else if (attachment.IsTexture() && attachment.GetTexture()) {
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
// report 1 for any multisampled draw framebuffer).
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
}
} }
return maxSamples; return maxSamples;
} }
@@ -470,14 +465,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_STENCIL_TEST: case GL_STENCIL_TEST:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE; *params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
return; return;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
GLfloat value = 0.0f;
GetFloatv(pname, &value);
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
return;
}
default: default:
break; break;
} }
@@ -533,19 +520,6 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.ViewportBoundsRangeMax; params[1] = dynamicParameters.ViewportBoundsRangeMax;
return; return;
} }
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
} else {
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
}
return;
}
case GL_DEPTH_CLEAR_VALUE: case GL_DEPTH_CLEAR_VALUE:
params[0] = MG_State::pGLContext->GetClearDepth(); params[0] = MG_State::pGLContext->GetClearDepth();
return; return;
@@ -555,13 +529,6 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.AliasedLineWidthRangeMax; params[1] = dynamicParameters.AliasedLineWidthRangeMax;
return; return;
} }
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
// EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
// from here, so this case is the authoritative one.
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
return;
}
case GL_ALIASED_POINT_SIZE_RANGE: case GL_ALIASED_POINT_SIZE_RANGE:
case GL_POINT_SIZE_RANGE: { case GL_POINT_SIZE_RANGE: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
@@ -1201,9 +1168,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentInputComponents; *params = kFrontendMaxFragmentInputComponents;
return; return;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
// TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap.
*params = MG_Backend::pActiveBackendObject *params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms; : MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms;
return; return;
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS: case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
*params = kFrontendMaxFragmentUniformComponents; *params = kFrontendMaxFragmentUniformComponents;
@@ -1233,9 +1201,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryTextureImageUnits; *params = kFrontendMaxGeometryTextureImageUnits;
return; return;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject *params = 0;
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
return; return;
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS: case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
*params = kFrontendMaxGeometryTotalOutputComponents; *params = kFrontendMaxGeometryTotalOutputComponents;
@@ -1304,9 +1270,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxVertexAtomicCounters; *params = kFrontendMaxVertexAtomicCounters;
return; return;
case GL_MAX_VERTEX_IMAGE_UNIFORMS: case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject *params = 0;
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return; return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO *params = 16; // TODO
@@ -1763,11 +1727,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = 1024 * 1024; // TODO *params = 1024 * 1024; // TODO
return; return;
case GL_CONTEXT_PROFILE_MASK: case GL_CONTEXT_PROFILE_MASK:
// Reports the requested context profile (EGL defaults 3.x contexts to core); *params = GL_CONTEXT_CORE_PROFILE_BIT;
// MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity.
*params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile()
? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT
: GL_CONTEXT_CORE_PROFILE_BIT;
return; return;
default: default:
break; break;
@@ -1927,22 +1887,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS: case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
*params = kFrontendMaxTransformFeedbackSeparateComponents; *params = kFrontendMaxTransformFeedbackSeparateComponents;
break; break;
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
// whether the extension is advertised and requires no GL error; desktop
// drivers all accept them, so answer with the separate-attrib capacity and
// the single vertex stream the backends provide.
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
break;
case GL_MAX_VERTEX_STREAMS:
*params = 1;
break;
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
break;
case GL_TRANSFORM_FEEDBACK_PAUSED:
*params = 0;
break;
case GL_MAX_TEXTURE_IMAGE_UNITS: case GL_MAX_TEXTURE_IMAGE_UNITS:
*params = dynamicParameters.MaxTextureImageUnits; *params = dynamicParameters.MaxTextureImageUnits;
break; break;
@@ -1950,13 +1894,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = dynamicParameters.MaxTextureSize; *params = dynamicParameters.MaxTextureSize;
break; break;
case GL_MAX_UNIFORM_BUFFER_BINDINGS: case GL_MAX_UNIFORM_BUFFER_BINDINGS:
// Never advertise more bindings than the state layer's indexed-binding array can track *params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings);
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
// (36), so the clamp never under-advertises.
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
break; break;
case GL_MAX_UNIFORM_BLOCK_SIZE: case GL_MAX_UNIFORM_BLOCK_SIZE:
*params = dynamicParameters.MaxUniformBlockSize; *params = dynamicParameters.MaxUniformBlockSize;
@@ -1999,15 +1937,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SUBPIXEL_BITS: case GL_SUBPIXEL_BITS:
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits); *params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
break; break;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
break;
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
*params = dynamicParameters.FragmentInterpolationOffsetBits;
break;
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment); *params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
break; break;
@@ -2026,10 +1955,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_SAMPLES: case GL_MAX_SAMPLES:
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples); *params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
break; break;
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
break;
default: default:
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname); MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum, MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
@@ -2047,12 +1972,4 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code); return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
} }
GLenum GetGraphicsResetStatus() {
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
// device after every case (gl3cTestPackages.cpp:121) and, under the default
// --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
return GL_NO_ERROR;
}
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -21,5 +21,4 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data); void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data); void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError(); GLenum GetError();
GLenum GetGraphicsResetStatus();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+20 -186
View File
@@ -49,18 +49,10 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckProgramNameValidity(GLuint program) { static bool CheckProgramNameValidity(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) { if (!MG_State::pGLContext->ValidateProgramName(program)) {
// Programs and shaders share one name space: a name that exists but
// belongs to a shader is INVALID_OPERATION, a name GL never handed
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
error, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + std::to_string(program) + " is not a valid name."));
(error == ErrorCode::InvalidOperation ? " is not a program object."
: " is not a valid name.")));
return false; return false;
} }
return true; return true;
@@ -347,9 +339,6 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program.")); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program."));
return; return;
} }
// A shader flagged with glDeleteShader lives on while attached; this detach may
// have been its last GL-visible attachment.
MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader);
} }
void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size,
@@ -613,18 +602,6 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1; *params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params); MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break; break;
case GL_TRANSFORM_FEEDBACK_VARYINGS:
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
*params = programObject->GetTransformFeedbackVaryingMaxLength();
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3 case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) { if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -644,6 +621,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PROGRAM_BINARY_LENGTH: case GL_PROGRAM_BINARY_LENGTH:
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
case GL_TRANSFORM_FEEDBACK_VARYINGS:
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
case GL_GEOMETRY_VERTICES_OUT: case GL_GEOMETRY_VERTICES_OUT:
case GL_GEOMETRY_INPUT_TYPE: case GL_GEOMETRY_INPUT_TYPE:
case GL_GEOMETRY_OUTPUT_TYPE: case GL_GEOMETRY_OUTPUT_TYPE:
@@ -755,12 +735,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location); auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = (char*)programObject->MapUBO(); char* pUBO = (char*)programObject->MapUBO();
auto* ttype = programObject->GetUniformTType(location); auto* ttype = programObject->GetUniformTType(location);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + size > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3) if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
Memcpy(params, pUBO + offset, size); Memcpy(params, pUBO + offset, size);
@@ -810,12 +784,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto size = programObject->GetUniformSizesInBytes(location); auto size = programObject->GetUniformSizesInBytes(location);
char* pUBO = static_cast<char*>(programObject->MapUBO()); char* pUBO = static_cast<char*>(programObject->MapUBO());
auto* ttype = programObject->GetUniformTType(location); auto* ttype = programObject->GetUniformTType(location);
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + size > programObject->GetUBOSize()) {
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
program, location);
return;
}
if constexpr (std::is_same_v<T, GLfloat>) { if constexpr (std::is_same_v<T, GLfloat>) {
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) { if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
@@ -844,13 +812,19 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
GLboolean IsProgram_State(GLuint program) { GLboolean IsProgram_State(GLuint program) {
// Deletion-flagged names stay valid while the object is still GL-visible (program in /* FIXME: Handle situations that:
// use, shader attached), so name validity is exactly the Is* answer. * A program object marked for deletion with glDeleteProgram but still in use as part of current
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
*/
if (program == 0) return GL_FALSE; if (program == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE; return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
} }
GLboolean IsShader_State(GLuint shader) { GLboolean IsShader_State(GLuint shader) {
/* FIXME: Handle situations that:
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
* considered a shader object and glIsShader will return GL_TRUE.
*/
if (shader == 0) return GL_FALSE; if (shader == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE; return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
} }
@@ -860,18 +834,6 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject) return; if (!programObject) return;
MGLOG_D("%s: linking program %d", __func__, program); MGLOG_D("%s: linking program %d", __func__, program);
// Relinking the program an active transform feedback captures from would
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"The program used by active transform feedback cannot be relinked."));
return;
}
static Bool allowVSOnlyPrograms; static Bool allowVSOnlyPrograms;
static Bool initialized = false; static Bool initialized = false;
if (!initialized) { if (!initialized) {
@@ -915,16 +877,6 @@ namespace MobileGL::MG_Impl::GLImpl {
void UseProgram_State(GLuint program) { void UseProgram_State(GLuint program) {
MGLOG_D("UseProgram_State: program=%u", program); MGLOG_D("UseProgram_State: program=%u", program);
// GL 3.3 core 2.11.3: the program in use may not change while transform
// feedback is active (there is no pause in 3.3).
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The current program cannot change while transform feedback is active."));
return;
}
if (program == 0) { if (program == 0) {
MG_State::pGLContext->UseProgram(0); MG_State::pGLContext->UseProgram(0);
return; return;
@@ -948,31 +900,14 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject.IsUniformOpaqueAtLocation(location)) { if (!programObject.IsUniformOpaqueAtLocation(location)) {
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(), MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
location, programObject.GetMaxUniformLocation()); location, programObject.GetMaxUniformLocation());
const SizeT size = programObject.GetUniformSizesInBytes(location); auto size = programObject.GetUniformSizesInBytes(location);
const Uint offset = programObject.GetUniformOffset(location); auto offset = programObject.GetUniformOffset(location);
char* pUBO = static_cast<char*>(programObject.MapUBO()); MOBILEGL_ASSERT(size >= ItemCount * sizeof(T),
const SizeT uboSize = programObject.GetUBOSize(); "Uniform size mismatch, expected at least %zu bytes, got %zu bytes.", ItemCount * sizeof(T),
SizeT writeSize = ItemCount * sizeof(T); size);
if (size < writeSize) {
// Metadata bug: degrade to a clamped copy instead of killing the process.
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
"bytes; clamping",
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
writeSize = size;
}
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
// Should not happen: linking gives every settable uniform backing
// storage. Log and drop the write instead of faulting.
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
"uboSize=%zu); dropping write",
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
writeSize, uboSize);
return;
}
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(), MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
location, offset + byteOffsetInsideUniform); location, offset + byteOffsetInsideUniform);
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize); Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T));
programObject.MarkUBOContentDirty(); programObject.MarkUBOContentDirty();
} else { } else {
auto* ttype = programObject.GetUniformTType(location); auto* ttype = programObject.GetUniformTType(location);
@@ -1005,11 +940,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
for (GLint offset = 0; offset < count; offset++) { for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
// GL 3.3 §2.11.4: values for elements beyond the end of the uniform
// array are ignored. Never step onto a neighboring uniform's location.
break;
}
if (!programObject->IsValidUniformLocation(location + offset)) { if (!programObject->IsValidUniformLocation(location + offset)) {
RecordInvalidUniformLocationError(__func__, location + offset, "the current program object"); RecordInvalidUniformLocationError(__func__, location + offset, "the current program object");
return; return;
@@ -1034,10 +964,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
for (GLint offset = 0; offset < count; offset++) { for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + offset)) { if (!programObject->IsValidUniformLocation(location + offset)) {
RecordInvalidUniformLocationError(__func__, location + offset, RecordInvalidUniformLocationError(__func__, location + offset,
"program " + std::to_string(program)); "program " + std::to_string(program));
@@ -1166,10 +1092,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually // For matrix uniforms, we handle each matrix individually
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return; return;
@@ -1202,10 +1124,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually // For matrix uniforms, we handle each matrix individually
// Handle padding in mat3 correctly!! // Handle padding in mat3 correctly!!
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return; return;
@@ -1241,10 +1159,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// For matrix uniforms, we handle each matrix individually // For matrix uniforms, we handle each matrix individually
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "the current program object"); RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
return; return;
@@ -1305,10 +1219,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return; return;
@@ -1339,10 +1249,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return; return;
@@ -1377,10 +1283,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
for (GLint i = 0; i < count; i++) { for (GLint i = 0; i < count; i++) {
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
// Values for elements beyond the end of the uniform array are ignored.
break;
}
if (!programObject->IsValidUniformLocation(location + i)) { if (!programObject->IsValidUniformLocation(location + i)) {
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program)); RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
return; return;
@@ -1439,7 +1341,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName); const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
return index; return index;
} }
@@ -1463,7 +1364,6 @@ namespace MobileGL::MG_Impl::GLImpl {
std::to_string(program) + ".")); std::to_string(program) + "."));
return; return;
} }
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding); programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
} }
@@ -1535,13 +1435,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params); MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
break; break;
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: { case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
// Member entries of an arrayed block are recorded against the first instance;
// every instance of the array reports that shared member set (matches
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
GLint uniformIndexCount = 0; GLint uniformIndexCount = 0;
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) { for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) { if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast<Int>(uniformBlockIndex)) {
continue; continue;
} }
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex); params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
@@ -2250,66 +2146,4 @@ namespace MobileGL::MG_Impl::GLImpl {
void ValidateProgram(GLuint program) { void ValidateProgram(GLuint program) {
ValidateProgram_State(program); ValidateProgram_State(program);
} }
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
return;
}
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
return;
}
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
return;
}
Vector<String> names;
names.reserve(static_cast<SizeT>(count));
for (GLsizei i = 0; i < count; ++i) {
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
}
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
}
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + " has not been successfully linked."));
return;
}
const auto* varying = programObject->GetTransformFeedbackVarying(index);
if (varying == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"index is not an active transform feedback varying of the program."));
return;
}
if (size != nullptr) *size = varying->size;
if (type != nullptr) *type = varying->type;
GLsizei written = 0;
if (name != nullptr && bufSize > 0) {
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
name[written] = '\0';
}
if (length != nullptr) *length = written;
}
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -138,7 +138,4 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ValidateProgram(GLuint program); void ValidateProgram(GLuint program);
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name);
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+20 -127
View File
@@ -27,8 +27,6 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false; Bool ended = false;
Bool resultCached = false; Bool resultCached = false;
Uint64 cachedResult = 0; Uint64 cachedResult = 0;
// Transform feedback primitive counter at BeginQuery time.
Uint64 counterSnapshot = 0;
}; };
// Query calls may arrive from any thread (launchers migrate the context // Query calls may arrive from any thread (launchers migrate the context
@@ -43,11 +41,6 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_nextQueryId = 1; GLuint g_nextQueryId = 1;
// Id of the query currently active on GL_TIME_ELAPSED (0 = none). // Id of the query currently active on GL_TIME_ELAPSED (0 = none).
GLuint g_activeTimeElapsedQueryId = 0; GLuint g_activeTimeElapsedQueryId = 0;
// Ids of the queries active on the transform feedback targets (0 = none).
GLuint g_activePrimitivesWrittenQueryId = 0;
GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0;
Bool TimerQueryDisabled() { Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery; return MG_Config::Features.DisableTimerQuery;
@@ -133,11 +126,6 @@ namespace MobileGL::MG_Impl::GLImpl {
outValue = 0; outValue = 0;
return true; return true;
} }
// ANY_SAMPLES_PASSED* report a boolean.
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
result = result != 0 ? 1 : 0;
}
// Final value produced (or no GetQueryResult64 hook: the // Final value produced (or no GetQueryResult64 hook: the
// query degrades to a zero result); the backend handle is // query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads. // consumed and the value cached for later reads.
@@ -192,24 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
QueryObject* queryObject = it->second; QueryObject* queryObject = it->second;
if (queryObject->active) { if (queryObject->active) {
// Implicitly end before deletion, releasing the matching active slot. EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
g_activeSamplesPassedQueryId = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false;
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId) = 0;
} else {
EndTimeElapsedQueryLocked(queryObject);
}
} }
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
@@ -233,15 +204,10 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void BeginQuery(GLenum target, GLuint id) { void BeginQuery(GLenum target, GLuint id) {
const Bool isTransformFeedbackQuery = if (target != GL_TIME_ELAPSED) {
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED; // Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
const Bool isOcclusionQuery = // primitive queries remain stubs); GL_TIMESTAMP is not a valid
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || // BeginQuery target either.
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return; return;
} }
@@ -255,13 +221,9 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist."); RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return; return;
} }
GLuint& activeQueryId = isTransformFeedbackQuery if (g_activeTimeElapsedQueryId != 0) {
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on this target."); "A query is already active on GL_TIME_ELAPSED.");
return; return;
} }
if (queryObject->active) { if (queryObject->active) {
@@ -277,72 +239,25 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target; queryObject->target = target;
queryObject->active = true; queryObject->active = true;
if (isTransformFeedbackQuery) { const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
// Prefer real GPU transform-feedback queries (exact with geometry shaders); queryObject->backendHandle =
// the CPU accounting delta stays as the fallback when the backend lacks them. (!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery; g_activeTimeElapsedQueryId = id;
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
} else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
activeQueryId = id;
} }
void EndQuery(GLenum target) { void EndQuery(GLenum target) {
const Bool isTransformFeedbackQuery = if (target != GL_TIME_ELAPSED) {
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return; return;
} }
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex); const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
GLuint& activeQueryId = isTransformFeedbackQuery if (g_activeTimeElapsedQueryId == 0) {
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return; return;
} }
auto* queryObject = FindQueryObjectLocked(activeQueryId); auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
if (!queryObject) { if (!queryObject) {
activeQueryId = 0; // should not happen; keep state consistent g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
return;
}
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle);
}
// Result comes from the GPU query at read time.
} else {
queryObject->cachedResult =
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
queryObject->resultCached = true;
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return; return;
} }
EndTimeElapsedQueryLocked(queryObject); EndTimeElapsedQueryLocked(queryObject);
@@ -388,25 +303,9 @@ namespace MobileGL::MG_Impl::GLImpl {
switch (pname) { switch (pname) {
case GL_CURRENT_QUERY: { case GL_CURRENT_QUERY: {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex); const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
switch (target) { // Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
case GL_TIME_ELAPSED: // never are, and other targets remain unimplemented.
*params = static_cast<GLint>(g_activeTimeElapsedQueryId); *params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
break;
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
break;
case GL_PRIMITIVES_GENERATED:
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
*params = 0;
break;
}
return; return;
} }
case GL_QUERY_COUNTER_BITS: { case GL_QUERY_COUNTER_BITS: {
@@ -414,13 +313,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// time: IsTimerQuerySupported is the dynamic truth (extension / // time: IsTimerQuerySupported is the dynamic truth (extension /
// entry points / timestamp valid bits at call time, not at table // entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always // init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. // wins. Non-timer targets remain unimplemented and report 0.
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
return;
}
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP; const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported; const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
const Bool supported = const Bool supported =
@@ -649,9 +649,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void ClearDepth_State(GLclampd depth) { void ClearDepth_State(GLclampd depth) {
// GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear MG_State::pGLContext->SetClearDepth(static_cast<Float>(depth));
// values additionally require it: VUID-VkClearDepthStencilValue-depth-00022).
MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast<Float>(depth)));
} }
void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
+1 -15
View File
@@ -185,11 +185,6 @@ namespace MobileGL::MG_Impl::GLImpl {
static thread_local Vector<GLuint> names; static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenSamplerNames(count, names); MG_State::pGLContext->GenSamplerNames(count, names);
Memcpy(samplers, names.data(), count * sizeof(GLuint)); Memcpy(samplers, names.data(), count * sizeof(GLuint));
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->CreateSamplerObject(names[i]);
}
} }
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) { void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
@@ -238,16 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) { if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr); textureUnit.SetSamplerObject(nullptr);
} else { } else {
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already if (!SamplerImpl::ValidateSamplerName(sampler)) return;
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
// name, which is why this cannot go through the shared SamplerImpl validator.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler); Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) { if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler); MG_State::pGLContext->CreateSamplerObject(sampler);
-29
View File
@@ -133,33 +133,4 @@ namespace MobileGL::MG_Impl::GLImpl {
values[0] = value; values[0] = value;
} }
} }
void DestroyAllSyncObjects() {
// Detach the registry under the lock, release outside it. Entries the app
// already deleted were erased by DeleteSync, so nothing here double-frees;
// a DeleteSync racing this sweep finds an empty registry and returns. A
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
// holds a raw SyncObject* these deletes invalidate - the same undefined
// race an app-driven DeleteSync already has.
UnorderedMap<GLsync, SyncObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
orphans.swap(g_liveSyncObjects);
}
if (orphans.empty()) {
return;
}
// Both backends' DeleteSync only free the heap wrapper once their GL
// context/renderer is gone (generation/current-thread guards), so this is
// safe after the backend has released its EGL resources - but not after
// the function table itself is cleared.
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
for (const auto& [_, syncObject] : orphans) {
if (backendDeleteSync && syncObject->backendHandle) {
backendDeleteSync(syncObject->backendHandle);
}
delete syncObject;
}
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
-8
View File
@@ -16,12 +16,4 @@ namespace MobileGL::MG_Impl::GLImpl {
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout); void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(GLsync sync); void DeleteSync(GLsync sync);
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values); void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
// Destroys every still-registered sync object exactly as DeleteSync would.
// GL requires syncs to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
// process-global registry can be drained wholesale. Must run while the
// backend function table is still populated: each backend handle has to be
// released by the backend that created it, never by a later re-initialized
// one.
void DestroyAllSyncObjects();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+49 -494
View File
@@ -294,16 +294,10 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
} }
// Array targets store their layer count in z; layers never participate in mip Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
Bool DepthParticipatesInMipmapping(TextureTarget target) {
return target == TextureTarget::Texture3D;
}
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
Int maxDimension = std::max<Int>( Int maxDimension = std::max<Int>(
baseTexelSize.x(), baseTexelSize.x(),
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1)); std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
Uint mipLevelCount = 1; Uint mipLevelCount = 1;
while (maxDimension > 1) { while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1); maxDimension = std::max<Int>(maxDimension / 2, 1);
@@ -312,12 +306,11 @@ namespace MobileGL::MG_Impl::GLImpl {
return mipLevelCount; return mipLevelCount;
} }
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
return { return {
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1), std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1), std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1) std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
: std::max<Int>(baseTexelSize.z(), 1),
}; };
} }
@@ -340,20 +333,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const SizeT bytesPerTexel = baseByteSize / baseTexelCount; const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
for (Uint level = 1; level < requiredLevelCount; ++level) { for (Uint level = 1; level < requiredLevelCount; ++level) {
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips); const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level);
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) * const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
static_cast<SizeT>(levelTexelSize.y()) * static_cast<SizeT>(levelTexelSize.y()) *
static_cast<SizeT>(levelTexelSize.z()); static_cast<SizeT>(levelTexelSize.z());
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
texture.MarkStorageDirty(uploadTarget, level, false); texture.MarkStorageDirty(uploadTarget, level, false);
} }
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
// grows, so a previously longer chain (a bigger base image before respecification) would
// otherwise keep a tail of stale levels here and read as incomplete.
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
// Mip generation grows/regenerates the level set on the GPU without marking any CPU // Mip generation grows/regenerates the level set on the GPU without marking any CPU
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the // level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level // backend re-syncs: a cached sampled VkImageView built for the pre-generate level
@@ -426,17 +414,18 @@ namespace MobileGL::MG_Impl::GLImpl {
"2D multisample textures must use depth 1.")); "2D multisample textures must use depth 1."));
return false; return false;
} }
// Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) {
// == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative MG_State::pGLContext->RecordError(
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default ErrorCode::InvalidValue,
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0). MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"2D multisample array textures must have at least one layer."));
return false;
}
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) { if (samples > maxSamples) {
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
// exceeds what the format supports, and the native Adreno driver agrees.
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller, "MG_Impl/GLImpl", caller,
std::format("Sample count {} exceeds the supported maximum {} for this texture format.", std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
@@ -460,56 +449,8 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetSamples(samples); textureObject->SetSamples(samples);
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE); textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0}); textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
// Multisample textures are single-level by definition, so a name that previously held a
// mip chain must not keep its tail now that AllocateStorage only grows.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
} }
// Redefining level 0 of a texture that already had a base image drops the rest of the chain,
// which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
// behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
// any level-0 respecification leaves the chain in precisely the state it would have had
// before, while an upload to level N no longer destroys the levels beneath it.
//
// Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
// Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
// level count. A size-only test would leave the old tail in place, and because Mojang
// terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
// IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
// entirely (Managers.cpp) and the atlas samples black.
//
// The "already has a base image" test is what lets the fix work at all: a level that was
// never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
// then level 0 - must not discard the levels just uploaded. That ordering is what
// KHR-GL33.texture_repeat_mode does.
// Scoped to the respecified upload target only, which is what AllocateLevel already did.
// Cube maps keep six independent chains while reporting a single level count (face +X), so
// respecifying a face other than +X can leave the count longer than that face - but that
// asymmetry predates this change and widening the truncation to all six faces would destroy
// mip data for faces the application never touched. Left alone deliberately.
void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
TextureUploadTarget uploadTarget, Uint level) {
if (level != 0) return;
const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
const Bool hasExistingBaseImage =
existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
if (!hasExistingBaseImage) return;
texture->TruncateMipmapLevels(uploadTarget, 1);
}
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
// reports 0, so every compressed internalformat is by definition unsupported and
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
// a C++ exception through the C GL ABI and takes the process down.
void RecordUnsupportedCompressedFormat(const char* caller) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Compressed texture formats are not supported."));
}
} // namespace } // namespace
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) { const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
@@ -524,223 +465,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject; return textureObject;
} }
namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
const char* caller) {
if (texture == 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Clear texture operations require a non-zero texture name.");
return nullptr;
}
auto textureObject = GetTextureObjectByName(texture, caller);
if (!textureObject) return nullptr;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Buffer textures cannot be cleared with glClearTexImage.");
return nullptr;
}
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
if (level < 0) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
std::format("Texture level {} is negative.", level));
return nullptr;
}
// ARB_clear_texture: clearing an image that was never defined by TexImage*/
// TexStorage* is INVALID_OPERATION, not INVALID_VALUE.
if (static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
std::format("Texture level {} is not defined.", level));
return nullptr;
}
return mipmapTexture;
}
Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
!TextureImpl::ValidateTexturePixelDataType(inputType) ||
!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
inputFormat, textureObject->GetFormat(), inputType)) {
return false;
}
clearPixel.clear();
if (data == nullptr) {
// ARB_clear_texture defines a null clear value as all zeroes. Keeping the
// pattern empty lets the region writer use a fast memset path.
return true;
}
PixelStoreParameters clearPixelStore{};
clearPixelStore.Alignment = 1;
SizeT clearPixelSize = 0;
void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
{1, 1, 1}, false, clearPixelSize);
if (!converted || clearPixelSize == 0) {
if (converted) free(converted);
return false;
}
clearPixel.resize(clearPixelSize);
Memcpy(clearPixel.data(), converted, clearPixelSize);
free(converted);
return true;
}
// Writes the clear into the CPU shadow and marks the whole level dirty, exactly like
// TexSubImage*_State does. Shared limitation of the level-granular shadow sync: the
// shadow does not reflect GPU-side writes (FBO rendering, imageStore), so a PARTIAL
// clear of a GPU-written level re-uploads stale shadow bytes outside the region on
// the next sync. Full-level clears (glClearTexImage, or a sub-clear covering the
// level) rewrite the entire shadow and are always correct.
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
TextureUploadTarget uploadTarget, GLint level,
GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth,
const Vector<Uint8>& clearPixel, const char* caller) {
const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level has no storage.");
return false;
}
if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
width < 0 || height < 0 || depth < 0 ||
width > texelSize.x() - xoffset ||
height > texelSize.y() - yoffset ||
depth > texelSize.z() - zoffset) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
"The clear region lies outside the requested texture level.");
return false;
}
if (width == 0 || height == 0 || depth == 0) return true;
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(texelSize.z());
const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
if (byteSize == 0 || byteSize % texelCount != 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture storage cannot be cleared.");
return false;
}
const SizeT bytesPerTexel = byteSize / texelCount;
if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
RecordClearTextureError(
caller, ErrorCode::InvalidOperation,
std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
clearPixel.size(), bytesPerTexel));
return false;
}
auto* destination = static_cast<Uint8*>(
textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
if (!destination) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level could not be mapped.");
return false;
}
const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
Uint8* firstClearRow = nullptr;
for (GLsizei z = 0; z < depth; ++z) {
for (GLsizei y = 0; y < height; ++y) {
Uint8* row = destination +
static_cast<SizeT>(zoffset + z) * fullSliceBytes +
static_cast<SizeT>(yoffset + y) * fullRowBytes +
static_cast<SizeT>(xoffset) * bytesPerTexel;
if (firstClearRow) {
Memcpy(row, firstClearRow, clearRowBytes);
continue;
}
firstClearRow = row;
if (clearPixel.empty()) {
Memset(row, 0, clearRowBytes);
continue;
}
Memcpy(row, clearPixel.data(), bytesPerTexel);
SizeT filled = bytesPerTexel;
while (filled < clearRowBytes) {
const SizeT copySize = std::min(filled, clearRowBytes - filled);
Memcpy(row + filled, row, copySize);
filled += copySize;
}
}
}
textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
return true;
}
} // namespace
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
size.x(), size.y(), size.z(), clearPixel, __func__)) {
return;
}
}
}
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
const auto& uploadTargets = textureObject->GetUploadTargets();
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
if (zoffset < 0 || depth < 0 ||
static_cast<SizeT>(zoffset) > uploadTargets.size() ||
static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
RecordClearTextureError(__func__, ErrorCode::InvalidValue,
"The cube-map clear region selects invalid faces.");
return;
}
for (GLsizei face = 0; face < depth; ++face) {
if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
return;
}
}
return;
}
if (uploadTargets.empty()) {
RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
"The requested texture has no upload target.");
return;
}
ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
width, height, depth, clearPixel, __func__);
}
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum pname, GLint param, const char* caller) { GLenum pname, GLint param, const char* caller) {
const auto target = textureObject->GetTarget(); const auto target = textureObject->GetTarget();
@@ -847,14 +571,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: { case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
// GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
// [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue); textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break; break;
} }
@@ -1044,23 +760,6 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
// clean). Name 0 resolves to the target's real default texture object, so parameters set on
// it are stored and queryable like on any texture.
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTargetForParameter(
TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
}
if (textureTarget == TextureTarget::Unknown) {
return nullTextureObject;
}
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
return activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
}
void GenerateMipmap_Backend(GLenum target) { void GenerateMipmap_Backend(GLenum target) {
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target); MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
} }
@@ -1106,12 +805,6 @@ namespace MobileGL::MG_Impl::GLImpl {
MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()), MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
"Texture object here should always be an object with mipmap"); "Texture object here should always be an object with mipmap");
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
return;
}
const void* originalPixels = pixels; const void* originalPixels = pixels;
const auto& pixelUnpackBufferObject = const auto& pixelUnpackBufferObject =
@@ -1144,14 +837,6 @@ namespace MobileGL::MG_Impl::GLImpl {
const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp; const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;
const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize; const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize;
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
free(processedPixels);
return;
}
const auto* srcData = static_cast<const Uint8*>(processedPixels); const auto* srcData = static_cast<const Uint8*>(processedPixels);
Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level)); Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
if (destData) { if (destData) {
@@ -1368,7 +1053,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
switch (pname) { switch (pname) {
@@ -1401,12 +1086,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_A: { case GL_TEXTURE_SWIZZLE_A: {
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname); auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param); auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
if (swizzleValue == TextureSwizzleParam::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
return;
}
textureObject->SetSwizzleParam(swizzleParam, swizzleValue); textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
break; break;
} }
@@ -1457,7 +1136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
TextureParameterObject_State(textureObject, pname, param, __func__); TextureParameterObject_State(textureObject, pname, param, __func__);
@@ -1473,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
SetTextureBorderColorFromFloats(textureObject, params); SetTextureBorderColorFromFloats(textureObject, params);
break; break;
@@ -1481,7 +1160,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: { case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]), GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])}; static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1504,7 +1183,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
SetTextureBorderColorFromInts(textureObject, params); SetTextureBorderColorFromInts(textureObject, params);
break; break;
@@ -1512,7 +1191,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: { case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) { if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
return; return;
@@ -1530,7 +1209,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
SetTextureBorderColorFromIntegerInts(textureObject, params); SetTextureBorderColorFromIntegerInts(textureObject, params);
break; break;
@@ -1538,7 +1217,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_SWIZZLE_RGBA: { case GL_TEXTURE_SWIZZLE_RGBA: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]), GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])}; static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
@@ -1558,7 +1237,7 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
SetTextureBorderColorFromUnsignedInts(textureObject, params); SetTextureBorderColorFromUnsignedInts(textureObject, params);
break; break;
@@ -1569,7 +1248,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
// ======================= Processing ================================ // ======================= Processing ================================
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!textureObject) return; if (!textureObject) return;
Vec4<TextureSwizzleParam> swizzleParams; Vec4<TextureSwizzleParam> swizzleParams;
@@ -1620,8 +1299,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject(); : bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1663,8 +1340,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject(); : bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) { if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1724,21 +1399,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// RGTC is a 2D-only compression scheme, so a 3D target rejects it. This has to be tested on
// the raw enum: the RGTC formats resolve to plain R8/RG8/SNORM storage on the way in (see
// GLToMG's TextureEnumConverter), so once the internal format is converted there is nothing
// left to distinguish them from an ordinary one- or two-channel upload.
if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
(internalformat == GL_COMPRESSED_RED_RGTC1 || internalformat == GL_COMPRESSED_SIGNED_RED_RGTC1 ||
internalformat == GL_COMPRESSED_RG_RGTC2 || internalformat == GL_COMPRESSED_SIGNED_RG_RGTC2)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"RGTC compressed formats are invalid for 3D texture targets"));
return;
}
// TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the // TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
// GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped. // GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
// GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
@@ -1748,8 +1408,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// target and data is not evenly divisible into the number of bytes needed to store in memory a datum // target and data is not evenly divisible into the number of bytes needed to store in memory a datum
// indicated by type. // indicated by type.
// ======================= Processing ================================ // ======================= Processing ================================
textureInternalFormat =
MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget); Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
@@ -1758,8 +1416,6 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject(); : bindingSlot.GetBoundObject();
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1793,12 +1449,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
// Allocate in TextureObject // Allocate in TextureObject
if (isProxy) { textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
MGLOG_D("%s: isProxy = true, not allocating", __func__);
} else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
}
if (!originalPixels) { if (!originalPixels) {
MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__); MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
@@ -1825,7 +1476,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
free(processedPixels); free(processedPixels);
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
} }
void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
@@ -1880,8 +1530,6 @@ namespace MobileGL::MG_Impl::GLImpl {
: bindingSlot.GetBoundObject(); : bindingSlot.GetBoundObject();
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -1924,7 +1572,6 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("%s: isProxy = true, not allocating", __func__); MGLOG_D("%s: isProxy = true, not allocating", __func__);
} else { } else {
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level); MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{width, height, 1}, internalBytes}); {{width, height, 1}, internalBytes});
} }
@@ -1987,8 +1634,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = auto& textureObject =
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject(); : bindingSlot.GetBoundObject();
// Name 0 resolves to the target's default texture object - a real texture this call
// (re)specifies like any other; the slot is never empty anymore.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
@@ -2013,7 +1658,6 @@ namespace MobileGL::MG_Impl::GLImpl {
"Texture object here should always be an object with mipmap"); "Texture object here should always be an object with mipmap");
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
if (!isProxy) { if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
} }
@@ -2045,12 +1689,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
// TODO: make sure `internalformat` is in one of supported format for TexBuffer // TODO: make sure `internalformat` is in one of supported format for TexBuffer
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
// which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer); auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (buffer != 0 && !bufferObject) { if (!bufferObject) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -2064,8 +1704,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = bindingSlot.GetBoundObject(); auto& textureObject = bindingSlot.GetBoundObject();
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
// Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
// silent no-op; the slot is never empty now that every unit/target holds its default.
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) { if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -2086,9 +1724,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLboolean IsTexture_State(GLuint texture) { GLboolean IsTexture_State(GLuint texture) {
// ======================= Processing ================================ // ======================= Processing ================================
// GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE;
// name is just GL_FALSE. Probing with the recording validator (as every other Is* entry
// point already avoids doing) would leave a spurious INVALID_VALUE behind.
return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE; return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE;
} }
@@ -2666,13 +2302,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) { void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
// TODO: implement compressed readback. Reporting success while writing nothing hands // TODO: implement
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture level is not stored in a compressed format."));
} }
void GenTextures_State(GLsizei n, GLuint* textures) { void GenTextures_State(GLsizei n, GLuint* textures) {
@@ -2820,22 +2450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"The attachment specified by the read buffer is incomplete.")); \ "The attachment specified by the read buffer is incomplete.")); \
return false; \ return false; \
} }
if (isDepth && isStencil) { if (isDepth) {
// A combined internalformat copies both halves, so the read framebuffer
// must populate both attachment points.
const auto& stencilAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Stencil);
const auto& depthAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Depth);
if (!depthAttachment.IsValid() || depthAttachment.IsEmpty() || !stencilAttachment.IsValid() ||
stencilAttachment.IsEmpty()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "CopyTexImage2D_State",
"DEPTH_STENCIL copy requires both depth and stencil attachments in the read framebuffer."));
return false;
}
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
} else if (isDepth) {
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth); GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
} else if (isStencil) { } else if (isStencil) {
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil); GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
@@ -2874,20 +2489,20 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) { const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State. // TODO: implement
RecordUnsupportedCompressedFormat(__func__); THROW_UNIMPL_EXCEPTION;
} }
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) { GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State. // TODO: implement
RecordUnsupportedCompressedFormat(__func__); THROW_UNIMPL_EXCEPTION;
} }
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data) { GLsizei imageSize, const void* data) {
// TODO: implement compressed upload - see CompressedTexImage2D_State. // TODO: implement
RecordUnsupportedCompressedFormat(__func__); THROW_UNIMPL_EXCEPTION;
} }
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -2897,8 +2512,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload - see CompressedTexImage2D_State. // TODO: implement
RecordUnsupportedCompressedFormat(__func__); THROW_UNIMPL_EXCEPTION;
} }
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -2908,11 +2523,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload. Until then report the spec error for an // TODO: implement
// unsupported compressed format rather than throwing - a C++ exception unwinding THROW_UNIMPL_EXCEPTION;
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
RecordUnsupportedCompressedFormat(__func__);
} }
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
@@ -2922,8 +2534,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return; if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement compressed upload - see CompressedTexImage2D_State. // TODO: implement
RecordUnsupportedCompressedFormat(__func__); THROW_UNIMPL_EXCEPTION;
} }
void BindTexture_State(GLenum target, GLuint texture) { void BindTexture_State(GLenum target, GLuint texture) {
@@ -2935,13 +2547,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return; if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
// GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that // Name 0 unbinds the current target from the active texture unit.
// glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
// restores the unit/target slot to its initial state.
if (texture == 0) { if (texture == 0) {
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit); auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget)); bindingSlot.Bind(nullptr);
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit); MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
return; return;
} }
@@ -2953,15 +2563,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted - if (!TextureImpl::ValidateTextureName(texture)) return;
// is not a legal bind target in the core profile (no application-generated names), and the error
// is INVALID_OPERATION, not INVALID_VALUE.
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindTexture_State", "Invalid texture name"));
return;
}
// ======================= Processing ================================ // ======================= Processing ================================
Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture); Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture);
@@ -2982,25 +2584,14 @@ namespace MobileGL::MG_Impl::GLImpl {
void ActiveTexture_State(GLenum texture) { void ActiveTexture_State(GLenum texture) {
// ===================== Error Checking ============================== // ===================== Error Checking ==============================
// GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 + if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) {
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
// state reset iterates every advertised combined unit, so rejecting units the
// implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
// whole test batches. The backend already clamps its advertised value to the state
// layer's MAX_TEXTURE_IMAGE_UNITS capacity.
Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
if (MG_Backend::pActiveBackendObject) {
maxCombinedUnits = std::min(
maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
}
if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ActiveTexture_State", "MG_Impl/GLImpl", "ActiveTexture_State",
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got " std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got "
"invalid enum: 0x{:X}, which may stand for unit {}.", "invalid enum: 0x{:X}, which may stand for unit {}.",
maxCombinedUnits - 1, texture, texture - GL_TEXTURE0))); texture, texture - GL_TEXTURE0)));
return; return;
} }
@@ -3269,9 +2860,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
} }
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels)); textureObject->SetImmutableLevels(static_cast<Uint>(levels));
} }
@@ -3324,8 +2912,6 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize}); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
} }
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels)); textureObject->SetImmutableLevels(static_cast<Uint>(levels));
} }
@@ -3365,21 +2951,16 @@ namespace MobileGL::MG_Impl::GLImpl {
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
textureObject->SetInternalFormat(textureInternalFormat); textureObject->SetInternalFormat(textureInternalFormat);
// Array targets keep their layer count constant across levels; only true 3D
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
for (GLsizei level = 0; level < levels; ++level) { for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level); const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level); const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth; const GLsizei levelDepth = std::max<GLsizei>(1, depth >> level);
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight, const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
levelDepth); levelDepth);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{levelWidth, levelHeight, levelDepth}, byteSize}); {{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
} }
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels)); textureObject->SetImmutableLevels(static_cast<Uint>(levels));
} }
@@ -3409,7 +2990,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject(); auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width); TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
} }
@@ -3424,7 +3004,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject(); auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height); TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
} }
@@ -3440,7 +3019,6 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject(); auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth); TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
} }
@@ -3596,10 +3174,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit)); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit)); MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
if (texture == 0) { if (texture == 0) {
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
// target's default texture object (the unit's initial state).
for (auto& slot : textureUnit.GetAllBindingSlots()) { for (auto& slot : textureUnit.GetAllBindingSlots()) {
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget())); slot.Bind(nullptr);
} }
return; return;
} }
@@ -4138,27 +3714,6 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height); CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!textureObject) return;
// GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
// go through CopyTextureSubImage3D with the face as a layer.
const auto target = textureObject->GetTarget();
if (target != TextureTarget::Texture2D && target != TextureTarget::Texture1DArray &&
target != TextureTarget::TextureRectangle) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"CopyTextureSubImage2D requires a 2D, 1D-array, or "
"rectangle texture."));
return;
}
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) {
CopyTexSubImage2D_Backend(glTarget, level, xoffset, yoffset, x, y, width, height);
});
}
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
CopyTexSubImage1D_State(target, level, xoffset, x, y, width); CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
} }
@@ -11,9 +11,6 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format); GLenum format);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -98,8 +95,6 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
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 CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border); GLsizei height, GLint border);
@@ -14,8 +14,7 @@
#include <MG_State/GLState/TextureState/TextureObjectStubs.h> #include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl { namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
// Leak-at-exit storage; see GlobalObjects.cpp. UniquePtr<ProxyTextureManager> pProxyTextureManager;
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
Bool IsProxyTextureTarget(TextureUploadTarget target) { Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) { switch (target) {
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap; UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
}; };
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager; extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
+11 -45
View File
@@ -175,15 +175,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool IsIntegerColorInputFormat(TextureInputFormat format) { static Bool IsIntegerColorInputFormat(TextureInputFormat format) {
return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger ||
format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger ||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger || format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger;
format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger ||
format == TextureInputFormat::AlphaInteger;
} }
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
switch (internalFormat) { switch (internalFormat) {
case TextureInternalFormat::R8I: case TextureInternalFormat::R8I:
case TextureInternalFormat::R8UI: case TextureInternalFormat::R8UI:
@@ -237,15 +235,17 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
format == TextureInputFormat::StencilIndex; format == TextureInputFormat::StencilIndex;
} }
// Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks // Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
// (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels // isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
// (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and // integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
// integer formats reject floating-point types; violations raise GL_INVALID_OPERATION. Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) { TextureInternalFormat internalFormat,
TexturePixelDataType type) {
const auto recordInvalidOperation = [](const char* message) { const auto recordInvalidOperation = [](const char* message) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateClientFormatTypePairing", message)); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
message));
return false; return false;
}; };
@@ -288,27 +288,6 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return recordInvalidOperation("Integer format cannot be used with a floating-point type"); return recordInvalidOperation("Integer format cannot be used with a floating-point type");
} }
return true;
}
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat,
TexturePixelDataType type) {
const auto recordInvalidOperation = [](const char* message) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
message));
return false;
};
if (!ValidateClientFormatTypePairing(format, type)) {
return false;
}
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). // TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
if (format == TextureInputFormat::StencilIndex) { if (format == TextureInputFormat::StencilIndex) {
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
@@ -360,19 +339,6 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (textureObject && textureObject->GetExternalIndex() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"This operation is not allowed on the default texture (zero is "
"bound to the target)."));
return false;
}
return true;
}
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target) { TextureTarget target) {
if (!textureObject) return true; // should be created later if (!textureObject) return true; // should be created later
@@ -23,19 +23,11 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border); Bool ValidateTextureBorderNumber(Int border);
Bool IsIntegerColorInputFormat(TextureInputFormat format);
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat);
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type);
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
TextureInternalFormat internalFormat, TextureInternalFormat internalFormat,
TexturePixelDataType type); TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level); Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject); Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
// error is generated if zero is bound to target", ARB_texture_storage).
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller);
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target); TextureTarget target);
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset, Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
@@ -78,12 +78,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) { static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) {
// GL 3.3 core 2.7: VertexAttrib* sets the current value of ANY generic attribute, if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false;
// including index 0 - only an out-of-range index is an error (INVALID_VALUE). if (index == 0) {
// "Attribute 0 is immutable" was legacy immediate-mode lore; rejecting it broke GL MG_State::pGLContext->RecordError(
// CTS's per-case state reset, which writes vertexAttrib4f(0, 0,0,0,1) after every case. ErrorCode::InvalidOperation,
static_cast<void>(funcName); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
return VertexArrayImpl::ValidateVertexAttributeIndex(index); "Generic vertex attribute 0 current value cannot be modified."));
return false;
}
return true;
} }
static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) { static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) {
@@ -285,9 +288,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint vao = arrays[i]; GLuint vao = arrays[i];
if (vao == 0) continue; if (vao == 0) continue;
// GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue;
// validator would record INVALID_OPERATION instead.
if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue;
if (MG_State::pGLContext->GetBoundVertexArray() && if (MG_State::pGLContext->GetBoundVertexArray() &&
MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) { MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) {
-2
View File
@@ -85,8 +85,6 @@ namespace MobileGL::MG_Impl {
GETPROC(CGLGetPixelFormat, name); GETPROC(CGLGetPixelFormat, name);
GETPROC(CGLSetCurrentContext, name); GETPROC(CGLSetCurrentContext, name);
GETPROC(CGLGetCurrentContext, name); GETPROC(CGLGetCurrentContext, name);
GETPROC(CGLSetVirtualScreen, name);
GETPROC(CGLGetVirtualScreen, name);
GETPROC(CGLSetParameter, name); GETPROC(CGLSetParameter, name);
GETPROC(CGLGetParameter, name); GETPROC(CGLGetParameter, name);
GETPROC(CGLUpdateContext, name); GETPROC(CGLUpdateContext, name);
+4 -36
View File
@@ -29,19 +29,10 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
char kContextViewKey; char kContextViewKey;
char kContextLayerKey; char kContextLayerKey;
std::once_flag g_installOnce;
IMP g_pixelFormatDealloc = nullptr; IMP g_pixelFormatDealloc = nullptr;
IMP g_contextDealloc = nullptr; IMP g_contextDealloc = nullptr;
std::mutex& HookInstallMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
Bool& HooksInstalled() {
static auto* installed = new Bool(false);
return *installed;
}
template <typename Fn> template <typename Fn>
Fn ObjcMsgSend() { Fn ObjcMsgSend() {
return reinterpret_cast<Fn>(objc_msgSend); return reinterpret_cast<Fn>(objc_msgSend);
@@ -440,12 +431,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
method_setImplementation(method, replacement); method_setImplementation(method, replacement);
} }
Bool InstallHooksOnce() { void InstallHooksOnce() {
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat"); Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
Class contextClass = objc_getClass("NSOpenGLContext"); Class contextClass = objc_getClass("NSOpenGLContext");
if (!pixelFormatClass || !contextClass) { if (!pixelFormatClass || !contextClass) {
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed"); MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
return false; return;
} }
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:", ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
@@ -480,34 +471,11 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc); ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
MGLOG_I("NSOpenGLImpl hooks installed"); MGLOG_I("NSOpenGLImpl hooks installed");
return true;
} }
} // namespace } // namespace
void InstallHooks() { void InstallHooks() {
const std::lock_guard<std::mutex> lock(HookInstallMutex()); std::call_once(g_installOnce, InstallHooksOnce);
if (!HooksInstalled()) {
// Do not permanently consume the install attempt when the OpenGL
// framework has not registered its Objective-C classes yet. The
// dyld bootstrap normally runs after framework dependencies, but
// an explicitly loaded/static-linked MobileGL can arrive earlier.
HooksInstalled() = InstallHooksOnce();
}
} }
} // namespace MobileGL::MG_Impl::NSOpenGLImpl } // namespace MobileGL::MG_Impl::NSOpenGLImpl
namespace {
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
// its first dlsym("glGetString") or other MobileGL host-API call. Install
// only the lightweight Objective-C dispatch hooks while the injected dylib
// is loading so those first Cocoa objects are routed through CGLImpl. The
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
// the full, thread-safe MobileGL initialization outside this bootstrap.
//
// There is intentionally no matching destructor: backend teardown remains
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
}
} // namespace
#endif #endif
@@ -1,156 +0,0 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
// be defined before the first windows.h inclusion in this translation unit.
#if defined(_WIN32) && !defined(_GDI32_)
#define _GDI32_ 1
#endif
#include <Includes.h>
#if defined(_WIN32)
#include "../WGLImpl.h"
namespace WGL = MobileGL::MG_Impl::WGLImpl;
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::ChoosePixelFormat(hdc, ppfd);
}
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
LPPIXELFORMATDESCRIPTOR ppfd) {
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
}
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
return WGL::GetPixelFormat(hdc);
}
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
}
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
return WGL::SwapBuffers(hdc);
}
// ---- Context management ----
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
return WGL::CreateContext(hdc);
}
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
return WGL::DeleteContext(hglrc);
}
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
return WGL::GetCurrentContext();
}
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
return WGL::GetCurrentDC();
}
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
return WGL::MakeCurrent(hdc, hglrc);
}
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
return WGL::ShareLists(hglrcShare, hglrcDest);
}
// ---- Proc address ----
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
return FALSE;
}
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
return 0;
}
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
return 0;
}
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
return FALSE;
}
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
return WGL::SwapBuffers(hdc);
}
return FALSE;
}
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
if (!ps) {
return 0;
}
DWORD swapped = 0;
for (UINT i = 0; i < n; ++i) {
if (WGL::SwapBuffers(ps[i].hdc)) {
++swapped;
}
}
return swapped;
}
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
return FALSE;
}
#endif // _WIN32
@@ -1,30 +0,0 @@
; MobileGL WGL exports. The wgl* entry points are defined without
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
; become plain declarations, and MSVC rejects adding dllexport afterwards),
; so this .def file is what actually exports them from the DLL.
EXPORTS
wglChoosePixelFormat
wglCopyContext
wglCreateContext
wglCreateLayerContext
wglDeleteContext
wglDescribeLayerPlane
wglDescribePixelFormat
wglGetCurrentContext
wglGetCurrentDC
wglGetDefaultProcAddress
wglGetLayerPaletteEntries
wglGetPixelFormat
wglGetProcAddress
wglMakeCurrent
wglRealizeLayerPalette
wglSetLayerPaletteEntries
wglSetPixelFormat
wglShareLists
wglSwapBuffers
wglSwapLayerBuffers
wglSwapMultipleBuffers
wglUseFontBitmapsA
wglUseFontBitmapsW
wglUseFontOutlinesA
wglUseFontOutlinesW
-732
View File
@@ -1,732 +0,0 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WGLImpl.h"
#if defined(_WIN32)
#include "../EGLImpl/EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
namespace MobileGL::MG_Impl::WGLImpl {
namespace {
// WGL_ARB_pixel_format
constexpr int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000;
constexpr int WGL_DRAW_TO_WINDOW_ARB = 0x2001;
constexpr int WGL_DRAW_TO_BITMAP_ARB = 0x2002;
constexpr int WGL_ACCELERATION_ARB = 0x2003;
constexpr int WGL_NEED_PALETTE_ARB = 0x2004;
constexpr int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005;
constexpr int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006;
constexpr int WGL_SWAP_METHOD_ARB = 0x2007;
constexpr int WGL_NUMBER_OVERLAYS_ARB = 0x2008;
constexpr int WGL_NUMBER_UNDERLAYS_ARB = 0x2009;
constexpr int WGL_TRANSPARENT_ARB = 0x200A;
constexpr int WGL_SHARE_DEPTH_ARB = 0x200C;
constexpr int WGL_SHARE_STENCIL_ARB = 0x200D;
constexpr int WGL_SHARE_ACCUM_ARB = 0x200E;
constexpr int WGL_SUPPORT_GDI_ARB = 0x200F;
constexpr int WGL_SUPPORT_OPENGL_ARB = 0x2010;
constexpr int WGL_DOUBLE_BUFFER_ARB = 0x2011;
constexpr int WGL_STEREO_ARB = 0x2012;
constexpr int WGL_PIXEL_TYPE_ARB = 0x2013;
constexpr int WGL_COLOR_BITS_ARB = 0x2014;
constexpr int WGL_RED_BITS_ARB = 0x2015;
constexpr int WGL_RED_SHIFT_ARB = 0x2016;
constexpr int WGL_GREEN_BITS_ARB = 0x2017;
constexpr int WGL_GREEN_SHIFT_ARB = 0x2018;
constexpr int WGL_BLUE_BITS_ARB = 0x2019;
constexpr int WGL_BLUE_SHIFT_ARB = 0x201A;
constexpr int WGL_ALPHA_BITS_ARB = 0x201B;
constexpr int WGL_ALPHA_SHIFT_ARB = 0x201C;
constexpr int WGL_ACCUM_BITS_ARB = 0x201D;
constexpr int WGL_ACCUM_RED_BITS_ARB = 0x201E;
constexpr int WGL_ACCUM_GREEN_BITS_ARB = 0x201F;
constexpr int WGL_ACCUM_BLUE_BITS_ARB = 0x2020;
constexpr int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021;
constexpr int WGL_DEPTH_BITS_ARB = 0x2022;
constexpr int WGL_STENCIL_BITS_ARB = 0x2023;
constexpr int WGL_AUX_BUFFERS_ARB = 0x2024;
constexpr int WGL_NO_ACCELERATION_ARB = 0x2025;
constexpr int WGL_FULL_ACCELERATION_ARB = 0x2027;
constexpr int WGL_SWAP_EXCHANGE_ARB = 0x2028;
constexpr int WGL_TYPE_RGBA_ARB = 0x202B;
// WGL_ARB_multisample
constexpr int WGL_SAMPLE_BUFFERS_ARB = 0x2041;
constexpr int WGL_SAMPLES_ARB = 0x2042;
// WGL_ARB_create_context / _profile / _no_error
constexpr int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
constexpr int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
constexpr int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093;
constexpr int WGL_CONTEXT_FLAGS_ARB = 0x2094;
constexpr int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
constexpr int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
constexpr int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
constexpr int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
constexpr int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
constexpr int WGL_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
constexpr DWORD ERROR_INVALID_VERSION_ARB = 0x2095;
constexpr DWORD ERROR_INVALID_PROFILE_ARB = 0x2096;
struct PixelFormatInfo {
GLint AlphaBits;
GLint DepthBits;
GLint StencilBits;
};
// Mirrors the two EGLState configs (RGBA8 + depth24, stencil 8 / stencil 0).
constexpr PixelFormatInfo kPixelFormats[] = {
{8, 24, 8},
{8, 24, 0},
};
constexpr int kPixelFormatCount = static_cast<int>(std::size(kPixelFormats));
struct ContextObject {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT;
};
struct WindowSurface {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE;
Uint32 Width = 0;
Uint32 Height = 0;
};
std::recursive_mutex& RegistryMutex() {
static auto* mutex = new std::recursive_mutex();
return *mutex;
}
UnorderedMap<HGLRC, ContextObject>& Contexts() {
static auto* contexts = new UnorderedMap<HGLRC, ContextObject>();
return *contexts;
}
UnorderedMap<HWND, WindowSurface>& WindowSurfaces() {
static auto* surfaces = new UnorderedMap<HWND, WindowSurface>();
return *surfaces;
}
UnorderedMap<HWND, int>& WindowPixelFormats() {
static auto* formats = new UnorderedMap<HWND, int>();
return *formats;
}
Uint64& NextContextHandle() {
static auto* handle = new Uint64(0x10000);
return *handle;
}
struct ThreadCurrent {
HDC DC = nullptr;
HGLRC Context = nullptr;
};
thread_local ThreadCurrent t_current;
Int& SwapIntervalShadow() {
static auto* interval = new Int(1);
return *interval;
}
void EnsureInitialized() {
// Initialize() loads backend libraries and glslang, which must not run
// under the loader lock; first WGL call is the earliest safe moment.
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
// can follow a full teardown from the last eglTerminate.
MobileGL::EnsureInitialized();
}
EGLDisplay EnsureDisplay() {
EnsureInitialized();
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return EGL_NO_DISPLAY;
}
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
return EGL_NO_DISPLAY;
}
return display;
}
HGLRC EncodeContext(Uint64 handle) {
return reinterpret_cast<HGLRC>(static_cast<SizeT>(handle));
}
ContextObject* TryGetContext(HGLRC hglrc) {
auto& contexts = Contexts();
auto it = contexts.find(hglrc);
return it == contexts.end() ? nullptr : &it->second;
}
const PixelFormatInfo& PixelFormatForWindow(HWND hwnd) {
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
int index = it == formats.end() ? 1 : it->second;
if (index < 1 || index > kPixelFormatCount) {
index = 1;
}
return kPixelFormats[index - 1];
}
Bool QueryClientSize(HWND hwnd, Uint32& width, Uint32& height) {
RECT rect{};
if (!GetClientRect(hwnd, &rect)) {
return false;
}
width = static_cast<Uint32>(std::max<LONG>(rect.right - rect.left, 1));
height = static_cast<Uint32>(std::max<LONG>(rect.bottom - rect.top, 1));
return true;
}
// The backends never query the HWND client size themselves; the WGL layer
// owns size discovery and pushes changes through the internal resize hook
// (same contract as the macOS CGL layer).
void SyncSurfaceSize(HWND hwnd, WindowSurface& surface) {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
return;
}
if (width == surface.Width && height == surface.Height) {
return;
}
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
static_cast<EGLint>(width), static_cast<EGLint>(height))) {
surface.Width = width;
surface.Height = height;
}
}
WindowSurface* EnsureWindowSurface(HWND hwnd, const ContextObject& context) {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it != surfaces.end()) {
SyncSurfaceSize(hwnd, it->second);
return &it->second;
}
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
const EGLAttrib attribs[] = {
EGL_WIDTH, static_cast<EGLAttrib>(width),
EGL_HEIGHT, static_cast<EGLAttrib>(height),
EGL_NONE,
};
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
WindowSurface record;
record.Display = context.Display;
record.Surface = surface;
record.Width = width;
record.Height = height;
auto [inserted, _] = surfaces.emplace(hwnd, record);
return &inserted->second;
}
HGLRC CreateContextFromEGLAttribs(HDC hdc, HGLRC share, const EGLint* contextAttribs) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
EGLContext shareContext = EGL_NO_CONTEXT;
if (share) {
auto* shareObject = TryGetContext(share);
if (!shareObject) {
SetLastError(ERROR_INVALID_HANDLE);
return nullptr;
}
shareContext = shareObject->Context;
}
HWND hwnd = WindowFromDC(hdc);
const PixelFormatInfo& pixelFormat = PixelFormatForWindow(hwnd);
const EGLint configAttribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, pixelFormat.AlphaBits,
EGL_DEPTH_SIZE, pixelFormat.DepthBits,
EGL_STENCIL_SIZE, pixelFormat.StencilBits,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
return nullptr;
}
ContextObject object;
object.Display = display;
object.Config = config;
object.Context = eglContext;
const auto handle = EncodeContext(NextContextHandle()++);
Contexts()[handle] = object;
MGLOG_I("wgl: created context %p (EGL context %p)", handle, eglContext);
return handle;
}
// ---- WGL extension entry points (resolved via wglGetProcAddress only) ----
const char* WINAPI Ext_GetExtensionsStringARB(HDC) {
return "WGL_ARB_create_context WGL_ARB_create_context_no_error WGL_ARB_create_context_profile "
"WGL_ARB_extensions_string WGL_ARB_pixel_format WGL_EXT_extensions_string WGL_EXT_swap_control";
}
const char* WINAPI Ext_GetExtensionsStringEXT() {
return Ext_GetExtensionsStringARB(nullptr);
}
HGLRC WINAPI Ext_CreateContextAttribsARB(HDC hdc, HGLRC hShareContext, const int* attribList) {
EnsureInitialized();
int major = 1;
int minor = 0;
int profileMask = 0;
int flags = 0;
if (attribList) {
for (SizeT i = 0; attribList[i] != 0; i += 2) {
const int attrib = attribList[i];
const int value = attribList[i + 1];
switch (attrib) {
case WGL_CONTEXT_MAJOR_VERSION_ARB:
major = value;
break;
case WGL_CONTEXT_MINOR_VERSION_ARB:
minor = value;
break;
case WGL_CONTEXT_PROFILE_MASK_ARB:
profileMask = value;
break;
case WGL_CONTEXT_FLAGS_ARB:
flags = value;
break;
case WGL_CONTEXT_LAYER_PLANE_ARB:
if (value != 0) {
SetLastError(ERROR_INVALID_PARAMETER);
return nullptr;
}
break;
case WGL_CONTEXT_OPENGL_NO_ERROR_ARB:
// Accepted and ignored: MobileGL always validates.
break;
default:
MGLOG_D("wglCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib, value);
break;
}
}
}
if (major < 1 || (profileMask & ~(WGL_CONTEXT_CORE_PROFILE_BIT_ARB |
WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
SetLastError(profileMask ? ERROR_INVALID_PROFILE_ARB : ERROR_INVALID_VERSION_ARB);
return nullptr;
}
Vector<EGLint> attribs = {
EGL_CONTEXT_MAJOR_VERSION, major,
EGL_CONTEXT_MINOR_VERSION, minor,
};
const Bool wantsCompat = (profileMask & WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
}
if (flags & WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
attribs.push_back(EGL_TRUE);
}
if (flags & WGL_CONTEXT_DEBUG_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
attribs.push_back(EGL_TRUE);
}
attribs.push_back(EGL_NONE);
return CreateContextFromEGLAttribs(hdc, hShareContext, attribs.data());
}
BOOL WINAPI Ext_SwapIntervalEXT(int interval) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
return FALSE;
}
if (interval < 0) {
// Adaptive vsync is not supported; clamp to regular vsync.
interval = 1;
}
EGLImpl::SwapInterval(display, interval);
SwapIntervalShadow() = interval;
return TRUE;
}
int WINAPI Ext_GetSwapIntervalEXT() {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
return SwapIntervalShadow();
}
int PixelFormatAttribValue(int format, int attrib) {
const PixelFormatInfo& info = kPixelFormats[format - 1];
switch (attrib) {
case WGL_NUMBER_PIXEL_FORMATS_ARB:
return kPixelFormatCount;
case WGL_SUPPORT_OPENGL_ARB:
case WGL_DRAW_TO_WINDOW_ARB:
case WGL_DOUBLE_BUFFER_ARB:
return 1;
case WGL_ACCELERATION_ARB:
return WGL_FULL_ACCELERATION_ARB;
case WGL_PIXEL_TYPE_ARB:
return WGL_TYPE_RGBA_ARB;
case WGL_COLOR_BITS_ARB:
return 32;
case WGL_RED_BITS_ARB:
case WGL_GREEN_BITS_ARB:
case WGL_BLUE_BITS_ARB:
return 8;
case WGL_RED_SHIFT_ARB:
return 16;
case WGL_GREEN_SHIFT_ARB:
return 8;
case WGL_BLUE_SHIFT_ARB:
return 0;
case WGL_ALPHA_BITS_ARB:
return info.AlphaBits;
case WGL_ALPHA_SHIFT_ARB:
return 24;
case WGL_DEPTH_BITS_ARB:
return info.DepthBits;
case WGL_STENCIL_BITS_ARB:
return info.StencilBits;
case WGL_SWAP_METHOD_ARB:
return WGL_SWAP_EXCHANGE_ARB;
case WGL_DRAW_TO_BITMAP_ARB:
case WGL_NEED_PALETTE_ARB:
case WGL_NEED_SYSTEM_PALETTE_ARB:
case WGL_SWAP_LAYER_BUFFERS_ARB:
case WGL_NUMBER_OVERLAYS_ARB:
case WGL_NUMBER_UNDERLAYS_ARB:
case WGL_TRANSPARENT_ARB:
case WGL_SHARE_DEPTH_ARB:
case WGL_SHARE_STENCIL_ARB:
case WGL_SHARE_ACCUM_ARB:
case WGL_SUPPORT_GDI_ARB:
case WGL_STEREO_ARB:
case WGL_ACCUM_BITS_ARB:
case WGL_ACCUM_RED_BITS_ARB:
case WGL_ACCUM_GREEN_BITS_ARB:
case WGL_ACCUM_BLUE_BITS_ARB:
case WGL_ACCUM_ALPHA_BITS_ARB:
case WGL_AUX_BUFFERS_ARB:
case WGL_SAMPLE_BUFFERS_ARB:
case WGL_SAMPLES_ARB:
default:
return 0;
}
}
BOOL WINAPI Ext_GetPixelFormatAttribivARB(HDC, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, int* piValues) {
if (iLayerPlane != 0 || !piAttributes || !piValues) {
return FALSE;
}
// Format 0 is only valid for WGL_NUMBER_PIXEL_FORMATS_ARB queries.
if (iPixelFormat < 0 || iPixelFormat > kPixelFormatCount) {
return FALSE;
}
const int format = iPixelFormat == 0 ? 1 : iPixelFormat;
for (UINT i = 0; i < nAttributes; ++i) {
piValues[i] = PixelFormatAttribValue(format, piAttributes[i]);
}
return TRUE;
}
BOOL WINAPI Ext_GetPixelFormatAttribfvARB(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, FLOAT* pfValues) {
if (!pfValues) {
return FALSE;
}
Vector<int> values(nAttributes);
if (!Ext_GetPixelFormatAttribivARB(hdc, iPixelFormat, iLayerPlane, nAttributes, piAttributes,
values.data())) {
return FALSE;
}
for (UINT i = 0; i < nAttributes; ++i) {
pfValues[i] = static_cast<FLOAT>(values[i]);
}
return TRUE;
}
BOOL WINAPI Ext_ChoosePixelFormatARB(HDC, const int* piAttribIList, const FLOAT*, UINT nMaxFormats,
int* piFormats, UINT* nNumFormats) {
if (!piFormats || !nNumFormats) {
return FALSE;
}
int wantedStencil = 0;
if (piAttribIList) {
for (SizeT i = 0; piAttribIList[i] != 0; i += 2) {
if (piAttribIList[i] == WGL_STENCIL_BITS_ARB) {
wantedStencil = piAttribIList[i + 1];
}
}
}
UINT count = 0;
const int preferred = wantedStencil > 0 ? 1 : 2;
const int fallback = wantedStencil > 0 ? 2 : 1;
if (count < nMaxFormats) {
piFormats[count++] = preferred;
}
if (count < nMaxFormats) {
piFormats[count++] = fallback;
}
*nNumFormats = count;
return TRUE;
}
struct WGLExtensionProc {
const char* Name;
PROC Proc;
};
const WGLExtensionProc kWGLExtensionProcs[] = {
{"wglGetExtensionsStringARB", reinterpret_cast<PROC>(Ext_GetExtensionsStringARB)},
{"wglGetExtensionsStringEXT", reinterpret_cast<PROC>(Ext_GetExtensionsStringEXT)},
{"wglCreateContextAttribsARB", reinterpret_cast<PROC>(Ext_CreateContextAttribsARB)},
{"wglSwapIntervalEXT", reinterpret_cast<PROC>(Ext_SwapIntervalEXT)},
{"wglGetSwapIntervalEXT", reinterpret_cast<PROC>(Ext_GetSwapIntervalEXT)},
{"wglGetPixelFormatAttribivARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribivARB)},
{"wglGetPixelFormatAttribfvARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribfvARB)},
{"wglChoosePixelFormatARB", reinterpret_cast<PROC>(Ext_ChoosePixelFormatARB)},
};
} // namespace
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglChoosePixelFormat(hdc=%p)", hdc);
// Format 1 (RGBA8 + depth24/stencil8) satisfies every request; a format
// exceeding the asked-for capabilities is a legal ChoosePixelFormat answer.
(void)pfd;
return 1;
}
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglDescribePixelFormat(hdc=%p, format=%d)", hdc, format);
if (!pfd) {
return kPixelFormatCount;
}
if (size < sizeof(PIXELFORMATDESCRIPTOR) || format < 1 || format > kPixelFormatCount) {
return 0;
}
const PixelFormatInfo& info = kPixelFormats[format - 1];
std::memset(pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd->nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd->nVersion = 1;
pfd->dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_SWAP_EXCHANGE
#if defined(PFD_SUPPORT_COMPOSITION)
| PFD_SUPPORT_COMPOSITION
#endif
;
pfd->iPixelType = PFD_TYPE_RGBA;
pfd->cColorBits = 32;
pfd->cRedBits = 8;
pfd->cRedShift = 16;
pfd->cGreenBits = 8;
pfd->cGreenShift = 8;
pfd->cBlueBits = 8;
pfd->cBlueShift = 0;
pfd->cAlphaBits = static_cast<BYTE>(info.AlphaBits);
pfd->cAlphaShift = 24;
pfd->cDepthBits = static_cast<BYTE>(info.DepthBits);
pfd->cStencilBits = static_cast<BYTE>(info.StencilBits);
pfd->iLayerType = PFD_MAIN_PLANE;
return kPixelFormatCount;
}
int GetPixelFormat(HDC hdc) {
EnsureInitialized();
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
return 0;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
return it == formats.end() ? 0 : it->second;
}
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR*) {
EnsureInitialized();
MGLOG_D("wglSetPixelFormat(hdc=%p, format=%d)", hdc, format);
if (format < 1 || format > kPixelFormatCount) {
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
WindowPixelFormats()[hwnd] = format;
return TRUE;
}
BOOL SwapBuffers(HDC hdc) {
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
return EGLImpl::SwapBuffers(it->second.Display, it->second.Surface) == EGL_TRUE ? TRUE : FALSE;
}
HGLRC CreateContext(HDC hdc) {
EnsureInitialized();
MGLOG_I("wglCreateContext(hdc=%p)", hdc);
// A legacy WGL context is a compatibility-profile context; MobileGL keys
// its relaxed-semantics mode off the explicit compatibility bit.
const EGLint attribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 3,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE,
};
return CreateContextFromEGLAttribs(hdc, nullptr, attribs);
}
BOOL DeleteContext(HGLRC hglrc) {
EnsureInitialized();
MGLOG_I("wglDeleteContext(%p)", hglrc);
if (t_current.Context == hglrc) {
MakeCurrent(nullptr, nullptr);
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context);
}
Contexts().erase(hglrc);
return TRUE;
}
BOOL MakeCurrent(HDC hdc, HGLRC hglrc) {
EnsureInitialized();
MGLOG_D("wglMakeCurrent(hdc=%p, hglrc=%p)", hdc, hglrc);
if (!hglrc) {
if (!t_current.Context) {
t_current = {};
return TRUE;
}
const EGLBoolean released =
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
t_current = {};
return released == EGL_TRUE ? TRUE : FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
WindowSurface* surface = EnsureWindowSurface(hwnd, *object);
if (!surface) {
return FALSE;
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
return TRUE;
}
HGLRC GetCurrentContext() {
return t_current.Context;
}
HDC GetCurrentDC() {
return t_current.DC;
}
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
// All MobileGL contexts alias one global GL object namespace, so every
// pair of contexts already shares.
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!TryGetContext(hglrcShare) || !TryGetContext(hglrcDest)) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
return TRUE;
}
PROC GetProcAddress(const char* name) {
EnsureInitialized();
if (!name) {
return nullptr;
}
if (name[0] == 'w' && name[1] == 'g' && name[2] == 'l') {
for (const auto& entry : kWGLExtensionProcs) {
if (std::strcmp(entry.Name, name) == 0) {
return entry.Proc;
}
}
MGLOG_D("wglGetProcAddress: unknown wgl entry point %s", name);
return nullptr;
}
return reinterpret_cast<PROC>(MG_Impl::GetProcAddress(name));
}
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
-34
View File
@@ -1,34 +0,0 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#if defined(_WIN32)
namespace MobileGL::MG_Impl::WGLImpl {
// Classic opengl32.dll surface. gdi32's ChoosePixelFormat/SetPixelFormat/
// DescribePixelFormat/GetPixelFormat/SwapBuffers forward into the loaded
// opengl32.dll's wgl* exports, so these back both call paths.
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd);
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd);
int GetPixelFormat(HDC hdc);
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR* pfd);
BOOL SwapBuffers(HDC hdc);
HGLRC CreateContext(HDC hdc);
BOOL DeleteContext(HGLRC hglrc);
BOOL MakeCurrent(HDC hdc, HGLRC hglrc);
HGLRC GetCurrentContext();
HDC GetCurrentDC();
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest);
PROC GetProcAddress(const char* name);
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+1 -36
View File
@@ -368,26 +368,6 @@ namespace MobileGL {
return true; return true;
} }
Bool EGLContext::HasAnyInitializedDisplay() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [handle, displayObject] : m_displays) {
if (displayObject.Initialized) {
return true;
}
}
return false;
}
Bool EGLContext::HasAnyCurrentContext() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [threadId, current] : m_threadCurrents) {
if (current.Context != nullptr) {
return true;
}
}
return false;
}
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs, Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
EGLint configSize, EGLint* numConfig) { EGLint configSize, EGLint* numConfig) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex); const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -784,20 +764,6 @@ namespace MobileGL {
ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1); ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1);
} }
Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
if (currentIt == m_threadCurrents.end()) {
return false;
}
const auto* ctx = TryGetContext(currentIt->second.Context);
// Affirmative check: true only when the host explicitly requested the
// compatibility bit. Attrib-less contexts report false and thus read as core
// for GL_CONTEXT_PROFILE_MASK (EGL defaults 3.x contexts to the core profile).
return ctx && ctx->ClientAPI == EGL_OPENGL_API &&
(ctx->OpenGLProfileMask & EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT);
}
EGLint EGLContext::GetCurrentContextFlags() const { EGLint EGLContext::GetCurrentContextFlags() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex); const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto currentIt = m_threadCurrents.find(CurrentThreadKey()); auto currentIt = m_threadCurrents.find(CurrentThreadKey());
@@ -1435,7 +1401,6 @@ namespace MobileGL {
} }
} // namespace EGLState } // namespace EGLState
// Leak-at-exit storage; see GlobalObjects.cpp. UniquePtr<EGLState::EGLContext> pEGLContext;
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
} // namespace MG_State } // namespace MG_State
} // namespace MobileGL } // namespace MobileGL
+1 -6
View File
@@ -39,10 +39,6 @@ namespace MobileGL {
Bool IsDisplayInitialized(EGLDisplayHandle display) const; Bool IsDisplayInitialized(EGLDisplayHandle display) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor); Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display); Bool TerminateDisplay(EGLDisplayHandle display);
// Whole-library idle checks used by EGLImpl::Terminate to decide
// when the last eglTerminate may tear MobileGL down entirely.
Bool HasAnyInitializedDisplay() const;
Bool HasAnyCurrentContext() const;
// Config // Config
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs, Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
@@ -63,7 +59,6 @@ namespace MobileGL {
Bool ValidateContext(EGLContextHandle context) const; Bool ValidateContext(EGLContextHandle context) const;
Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const; Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const;
Bool IsCurrentContextOpenGLCoreProfile() const; Bool IsCurrentContextOpenGLCoreProfile() const;
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
EGLint GetCurrentContextFlags() const; EGLint GetCurrentContextFlags() const;
// Surface // Surface
@@ -266,6 +261,6 @@ namespace MobileGL {
}; };
} // namespace EGLState } // namespace EGLState
extern UniquePtr<EGLState::EGLContext>& pEGLContext; extern UniquePtr<EGLState::EGLContext> pEGLContext;
} // namespace MG_State } // namespace MG_State
} // namespace MobileGL } // namespace MobileGL
@@ -231,21 +231,6 @@ namespace MobileGL::MG_State::GLState {
return m_resource.Bytes(); return m_resource.Bytes();
} }
Bool BufferObject::EnsureGpuResidentStorage() {
if (m_resource.IsGpuResident()) {
return true;
}
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
return false;
}
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
if (base == nullptr) {
return false;
}
m_resource.AdoptPersistentMap(base);
return true;
}
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) { void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end, MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start, "AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
@@ -133,11 +133,6 @@ namespace MobileGL {
void* AcquireMemory(Bool markMapped, Bool read, Bool write); void* AcquireMemory(Bool markMapped, Bool read, Bool write);
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access); void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
// Adopt backend host-visible coherent GPU storage as the source of truth
// (used for GPU-written targets like transform feedback capture, so
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
// resident or when the backend declines.
Bool EnsureGpuResidentStorage();
void ReleaseMemory(); void ReleaseMemory();
void FlushMemoryRange(SizeT offset, SizeT length); void FlushMemoryRange(SizeT offset, SizeT length);
+2 -17
View File
@@ -9,7 +9,6 @@
#include "Core.h" #include "Core.h"
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h" #include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
#include "MG_State/EGLState/Core.h" #include "MG_State/EGLState/Core.h"
#include <Config.h>
namespace MobileGL::MG_State { namespace MobileGL::MG_State {
void Init() { void Init() {
@@ -18,11 +17,6 @@ namespace MobileGL::MG_State {
pEGLContext = MakeUnique<EGLState::EGLContext>(); pEGLContext = MakeUnique<EGLState::EGLContext>();
} }
Bool IsRelaxedSemanticsActive() {
return MG_Config::Features.RelaxedSemantics ||
!(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
}
namespace GLState { namespace GLState {
// Error // Error
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) { void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
@@ -232,16 +226,12 @@ namespace MobileGL::MG_State {
return m_textureState.GetTextureObject(index); return m_textureState.GetTextureObject(index);
} }
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
return m_textureState.GetDefaultTextureObject(target);
}
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) { const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
return m_textureState.CreateTextureObject(index, target); return m_textureState.CreateTextureObject(index, target);
} }
void GLContext::MarkTextureObjectForDeletion(Uint index) { void GLContext::MarkTextureObjectForDeletion(Uint index) {
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive()); m_textureState.MarkTextureObjectForDeletion(index);
} }
TextureUnit& GLContext::GetTextureUnitObject(Int unit) { TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
@@ -289,10 +279,6 @@ namespace MobileGL::MG_State {
return m_programState.MarkShaderObjectForDeletion(index); return m_programState.MarkShaderObjectForDeletion(index);
} }
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
return m_programState.ReleaseShaderNameIfOrphaned(index);
}
Bool GLContext::ValidateProgramName(const Uint index) const { Bool GLContext::ValidateProgramName(const Uint index) const {
return m_programState.ValidateProgramObject(index); return m_programState.ValidateProgramObject(index);
} }
@@ -721,6 +707,5 @@ namespace MobileGL::MG_State {
} }
} // namespace GLState } // namespace GLState
// Leak-at-exit storage; see GlobalObjects.cpp. UniquePtr<GLState::GLContext> pGLContext;
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
} // namespace MobileGL::MG_State } // namespace MobileGL::MG_State
+1 -62
View File
@@ -95,8 +95,6 @@ namespace MobileGL {
// Texture // Texture
void GenTextureNames(Uint number, Vector<Uint>& textures); void GenTextureNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index); const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target); const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
void MarkTextureObjectForDeletion(Uint index); void MarkTextureObjectForDeletion(Uint index);
TextureUnit& GetTextureUnitObject(Int unit); TextureUnit& GetTextureUnitObject(Int unit);
@@ -108,7 +106,6 @@ namespace MobileGL {
// texture is bound at a unit; lets a backend skip re-resolving an unchanged // texture is bound at a unit; lets a backend skip re-resolving an unchanged
// per-draw sampled-texture set. // per-draw sampled-texture set.
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); } Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
Bool ValidateTextureName(Uint index) const; Bool ValidateTextureName(Uint index) const;
Bool ValidateTextureObject(Uint index) const; Bool ValidateTextureObject(Uint index) const;
Int GetActiveTextureUnit() const; Int GetActiveTextureUnit() const;
@@ -119,9 +116,6 @@ namespace MobileGL {
Uint CreateShader(ShaderStage stage); Uint CreateShader(ShaderStage stage);
void MarkProgramForDeletion(Uint index); void MarkProgramForDeletion(Uint index);
void MarkShaderForDeletion(Uint index); void MarkShaderForDeletion(Uint index);
// Frees a deletion-flagged shader's name once it lost its last GL-visible
// attachment (call after glDetachShader).
void ReleaseShaderNameIfOrphaned(Uint index);
Bool ValidateProgramName(Uint index) const; Bool ValidateProgramName(Uint index) const;
Bool ValidateShaderName(Uint index) const; Bool ValidateShaderName(Uint index) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index); const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
@@ -211,47 +205,6 @@ namespace MobileGL {
void SetScissorBox(IntVec4 box); // x, y, width, height void SetScissorBox(IntVec4 box); // x, y, width, height
const IntVec4& GetScissorBox() const; // x, y, width, height const IntVec4& GetScissorBox() const; // x, y, width, height
// Transform feedback (GL 3.0 core Begin/End; no feedback objects yet)
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
m_transformFeedbackActive = true;
m_transformFeedbackPrimitiveMode = primitiveMode;
m_transformFeedbackProgram = program;
++m_transformFeedbackGeneration;
m_transformFeedbackCapturedVertices = 0;
m_transformFeedbackInputPrimitives = 0;
}
void EndTransformFeedback() {
m_transformFeedbackActive = false;
m_transformFeedbackProgram.reset();
}
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
return m_transformFeedbackProgram;
}
// Bumped on every BeginTransformFeedback; the backend uses it to
// distinguish "resume appending" from "fresh capture".
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
// CPU-side primitive accounting for the transform feedback queries:
// every captured draw adds its primitive count (draws without a
// geometry stage write exactly what they generate).
void AddTransformFeedbackPrimitives(Uint64 primitives) {
m_transformFeedbackPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
// Vertices already captured since BeginTransformFeedback (drives the
// buffer-capacity clamp on the primitives-written accounting).
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
m_transformFeedbackCapturedVertices += vertices;
}
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
// Raw assembled input primitives fed to the capture stage since Begin
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives;
}
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
// Framebuffer // Framebuffer
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers); void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index); const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
@@ -284,13 +237,6 @@ namespace MobileGL {
BufferState m_bufferState; BufferState m_bufferState;
VertexArrayState m_vertexArrayState; VertexArrayState m_vertexArrayState;
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{}; Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
Bool m_transformFeedbackActive = false;
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
TextureState m_textureState; TextureState m_textureState;
ProgramState m_programState; ProgramState m_programState;
RenderState m_renderState; RenderState m_renderState;
@@ -300,13 +246,6 @@ namespace MobileGL {
}; };
} // namespace GLState } // namespace GLState
extern UniquePtr<GLState::GLContext>& pGLContext; extern UniquePtr<GLState::GLContext> pGLContext;
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in
// EGL_CONTEXT_OPENGL_PROFILE_MASK, or a >=3.1 version request without the compatibility
// bit) and MOBILEGL_RELAXED_SEMANTICS is off; no current context, legacy version
// requests, and the compatibility bit all relax.
Bool IsRelaxedSemanticsActive();
} // namespace MG_State } // namespace MG_State
} // namespace MobileGL } // namespace MobileGL
+1 -10
View File
@@ -7,7 +7,6 @@
// End of Source File Header // End of Source File Header
#include "Error.h" #include "Error.h"
#include <algorithm>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h> #include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
@@ -20,15 +19,7 @@ namespace MobileGL::MG_State::GLState {
MGLOG_E("Recording OpenGL error (%s):\n%s", MGLOG_E("Recording OpenGL error (%s):\n%s",
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(), MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
info->toString().c_str()); info->toString().c_str());
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple m_errors.push_back(MakeUnique<Error>(code, Move(info)));
// error flags, each is set only while currently unset — repeated errors of the same
// code are discarded until glGetError reads the flag. Unbounded accumulation leaked
// stale errors into later, unrelated glGetError checks (GL CTS deinit noise).
const Bool alreadyPending = std::any_of(m_errors.begin(), m_errors.end(),
[code](const auto& e) { return e->code == code; });
if (!alreadyPending) {
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
}
} }
} }
@@ -8,7 +8,6 @@
#include "ProgramObject.h" #include "ProgramObject.h"
#include <atomic> #include <atomic>
#include <cstring>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h> #include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -88,19 +87,6 @@ namespace {
} }
} }
// How many consecutive uniform locations a uniform occupies. Array uniforms (opaque
// or not) span one location per element so glUniform*v(count > 1) and
// glGetUniformLocation("arr[k]") can address elements individually; everything else
// spans a single location. TObjectReflection.size only carries the element count for
// non-block arrays, so prefer the TType, which is authoritative for both.
static MobileGL::Int GetUniformLocationSpan(const glslang::TObjectReflection& uniform) {
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isSizedArray()) {
return std::max(1, type->getOuterArraySize());
}
return std::max(1, uniform.size);
}
static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) { static bool ComputeShaderDeclaresLocalSize(const MobileGL::String& source) {
bool inLineComment = false; bool inLineComment = false;
bool inBlockComment = false; bool inBlockComment = false;
@@ -170,230 +156,9 @@ namespace MobileGL::MG_State::GLState {
m_uniformNameMaxLength = 0; m_uniformNameMaxLength = 0;
m_attribInNameMaxLength = 0; m_attribInNameMaxLength = 0;
m_uniformBlockNameMaxLength = 0; m_uniformBlockNameMaxLength = 0;
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_xfbVaryingNameMaxLength = 0;
m_linkStatus = false; m_linkStatus = false;
} }
namespace {
// GL type enum for a vertex-stage output symbol captured by transform
// feedback. Covers the scalar/vector/matrix float+integer types transform
// feedback may legally capture in GL 3.3.
Bool ResolveXfbSymbolType(const glslang::TType& type, GLenum& outType, GLint& outArraySize,
Uint32& outBytesPerElement) {
outArraySize = type.isArray() ? type.getOuterArraySize() : 1;
const Int columns = type.isMatrix() ? type.getMatrixCols() : 1;
const Int components = type.isMatrix() ? type.getMatrixRows()
: (type.isVector() ? type.getVectorSize() : 1);
const glslang::TBasicType basic = type.getBasicType();
static constexpr GLenum kFloatTypes[5] = {0, GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4};
static constexpr GLenum kIntTypes[5] = {0, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4};
static constexpr GLenum kUintTypes[5] = {0, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
GL_UNSIGNED_INT_VEC4};
if (type.isMatrix()) {
if (basic != glslang::EbtFloat) return false;
static constexpr GLenum kMatTypes[5][5] = {
{}, {},
{0, 0, GL_FLOAT_MAT2, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4},
{0, 0, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3, GL_FLOAT_MAT3x4},
{0, 0, GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_FLOAT_MAT4},
};
if (columns < 2 || columns > 4 || components < 2 || components > 4) return false;
outType = kMatTypes[columns][components];
} else if (components >= 1 && components <= 4) {
switch (basic) {
case glslang::EbtFloat: outType = kFloatTypes[components]; break;
case glslang::EbtInt: outType = kIntTypes[components]; break;
case glslang::EbtUint: outType = kUintTypes[components]; break;
default: return false;
}
} else {
return false;
}
outBytesPerElement = static_cast<Uint32>(columns * components) * 4u;
return true;
}
} // namespace
Bool ProgramObject::ResolveTransformFeedbackVaryings() {
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = m_requestedXfbBufferMode;
m_xfbVaryingNameMaxLength = 0;
if (m_requestedXfbVaryings.empty()) {
return true;
}
// Capture happens at the last vertex-processing stage (geometry, then
// tessellation evaluation, then vertex).
const glslang::TIntermediate* captureIntermediate = nullptr;
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
captureIntermediate = m_program->getIntermediate(stage);
if (captureIntermediate != nullptr) {
break;
}
}
if (captureIntermediate == nullptr) {
m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage.";
return false;
}
const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects();
const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS;
Uint32 interleavedOffset = 0;
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
const String& name = m_requestedXfbVaryings[i];
for (SizeT j = 0; j < i; ++j) {
if (m_requestedXfbVaryings[j] == name) {
m_infoLog = "Transform feedback varying '" + name + "' is specified more than once.";
return false;
}
}
XfbVarying varying;
varying.name = name;
Uint32 bytesPerElement = 0;
Bool resolved = false;
if (name == "gl_Position") {
varying.type = GL_FLOAT_VEC4;
varying.size = 1;
bytesPerElement = 16;
resolved = true;
} else if (name == "gl_PointSize") {
varying.type = GL_FLOAT;
varying.size = 1;
bytesPerElement = 4;
resolved = true;
} else if (linkerObjects != nullptr) {
for (const auto* node : linkerObjects->getSequence()) {
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
continue;
}
if (symbol->getName() != name.c_str()) {
continue;
}
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
break;
}
}
if (!resolved) {
m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage.";
return false;
}
varying.byteSize = bytesPerElement * static_cast<Uint32>(varying.size);
if (interleaved) {
varying.bufferIndex = 0;
varying.offsetBytes = interleavedOffset;
interleavedOffset += varying.byteSize;
} else {
varying.bufferIndex = static_cast<Uint32>(i);
varying.offsetBytes = 0;
}
m_xfbVaryingNameMaxLength =
std::max(m_xfbVaryingNameMaxLength, static_cast<Int>(name.size()) + 1);
m_xfbVaryings.push_back(Move(varying));
}
constexpr Uint32 kMaxSeparateAttribs = 4;
constexpr Uint32 kMaxSeparateComponents = 4;
constexpr Uint32 kMaxInterleavedComponents = 64;
if (interleaved) {
if (interleavedOffset > kMaxInterleavedComponents * 4) {
m_infoLog = "Transform feedback interleaved capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
return false;
}
m_xfbStrides.assign(1, interleavedOffset);
} else {
if (m_xfbVaryings.size() > kMaxSeparateAttribs) {
m_infoLog = "Transform feedback separate capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS.";
return false;
}
m_xfbStrides.resize(m_xfbVaryings.size());
for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) {
if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) {
m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name +
"' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS.";
return false;
}
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
}
}
ResolveGsTriangleStripCapture(captureIntermediate);
return true;
}
namespace {
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
// the static strip lengths the capture buffer can be reordered after EndTF.
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
public:
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
if (node->getOp() == glslang::EOpEmitVertex) {
++emitCount;
hasEmit = true;
} else if (node->getOp() == glslang::EOpEndPrimitive) {
FlushStrip();
}
return true;
}
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
inControlFlow = true;
return true;
}
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
inControlFlow = true;
return true;
}
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
inControlFlow = true;
return true;
}
void FlushStrip() {
if (emitCount >= 3) {
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
}
emitCount = 0;
}
Vector<Uint32> stripTriangles;
Uint32 emitCount = 0;
Bool hasEmit = false;
Bool inControlFlow = false;
};
} // namespace
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
m_gsStripTriangles.clear();
m_gsStripCaptureFixup = false;
if (captureIntermediate == nullptr || m_program == nullptr) {
return;
}
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
return;
}
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
return;
}
GsEmitSequenceTraverser traverser;
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
return;
}
m_gsStripTriangles = Move(traverser.stripTriangles);
m_gsStripCaptureFixup = true;
}
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) { bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get()); MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
@@ -548,12 +313,6 @@ namespace MobileGL::MG_State::GLState {
if (!ValidateFragmentOutputLocations()) { if (!ValidateFragmentOutputLocations()) {
return; return;
} }
if (!ResolveTransformFeedbackVaryings()) {
m_linkStatus = false;
MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex,
m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex); MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
GenerateBinary(); GenerateBinary();
@@ -587,18 +346,7 @@ namespace MobileGL::MG_State::GLState {
} }
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex); MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
// GL-style reflection naming (GL CTS uniform_block relies on all four): if (!m_program->buildReflection()) {
// - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec.
// - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a",
// "s[1].a", ...) following ARB_program_interface_query rules. Default-block
// (loose) uniforms already expand per element without this option.
// - AllBlockVariables: every member of an active named block is active even when
// no shader statement reads it (ES 3.0/GL 3.3 named-block semantics).
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
// every block to std140, so this covers all of them.
if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
m_linkStatus = false; m_linkStatus = false;
m_infoLog = "Build reflection failed."; m_infoLog = "Build reflection failed.";
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex); MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
@@ -613,7 +361,8 @@ namespace MobileGL::MG_State::GLState {
for (int i = 0; i < m_activeUniformCount; i++) { for (int i = 0; i < m_activeUniformCount; i++) {
auto& uniform = m_program->getUniform(i); auto& uniform = m_program->getUniform(i);
auto location = uniform.layoutLocation(); auto location = uniform.layoutLocation();
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan =
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
requiredUniformLocations += locationSpan; requiredUniformLocations += locationSpan;
if (location != glslang::TQualifier::layoutLocationEnd) { if (location != glslang::TQualifier::layoutLocationEnd) {
m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1); m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1);
@@ -652,7 +401,8 @@ namespace MobileGL::MG_State::GLState {
m_externalIndex, uniform.name.c_str()); m_externalIndex, uniform.name.c_str());
continue; // will allocate unallocated uniforms later continue; // will allocate unallocated uniforms later
} }
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan =
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
for (Int element = 0; element < locationSpan; ++element) { for (Int element = 0; element < locationSpan; ++element) {
m_uniformIndexInTProgram[location + element] = i; m_uniformIndexInTProgram[location + element] = i;
} }
@@ -669,8 +419,8 @@ namespace MobileGL::MG_State::GLState {
}); });
for (auto index : unallocatedUniformIndex) { for (auto index : unallocatedUniformIndex) {
auto& uniform = m_program->getUniform(index); auto& uniform = m_program->getUniform(index);
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan =
Bool placed = false; (uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
for (; locNeedle <= m_maxUniformLocation; locNeedle++) { for (; locNeedle <= m_maxUniformLocation; locNeedle++) {
bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation; bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation;
for (Int element = 0; hasRoom && element < locationSpan; ++element) { for (Int element = 0; hasRoom && element < locationSpan; ++element) {
@@ -687,25 +437,8 @@ namespace MobileGL::MG_State::GLState {
"(index %d)", "(index %d)",
m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index); m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index);
locNeedle += locationSpan; locNeedle += locationSpan;
placed = true;
break; break;
} }
if (!placed) {
// Explicit-location uniforms can fragment the space so no contiguous
// span is left; grow the table instead of leaving the uniform without
// a location (which would make it unsettable via glUniform*).
const SizeT base = m_uniformIndexInTProgram.size();
m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd);
m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
m_maxUniformLocation = static_cast<Uint>(base + locationSpan - 1);
for (Int element = 0; element < locationSpan; ++element) {
m_uniformIndexInTProgram[base + element] = index;
}
m_uniformLocations[uniform.name] = static_cast<Uint>(base);
MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu",
m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1);
locNeedle = base + locationSpan;
}
} }
for (int i = 0; i < m_activeUniformCount; i++) { for (int i = 0; i < m_activeUniformCount; i++) {
@@ -721,17 +454,10 @@ namespace MobileGL::MG_State::GLState {
continue; continue;
} }
// Reflection names an array "texs[0]" while the layout(binding = N) map from the IO const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
// resolver is keyed by the declared name ("texs"); look up both spellings.
auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 &&
uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) {
explicitBinding =
m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3));
}
const int initialUnit = const int initialUnit =
explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast<int>(explicitBinding->second) : 0; explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast<int>(explicitBinding->second) : 0;
const Int locationSpan = GetUniformLocationSpan(uniform); const Int locationSpan = std::max(1, uniform.size);
for (Int element = 0; element < locationSpan && for (Int element = 0; element < locationSpan &&
location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) { location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) {
m_uniformSamplerOrImageUnitIndex[location + element] = m_uniformSamplerOrImageUnitIndex[location + element] =
@@ -896,11 +622,8 @@ namespace MobileGL::MG_State::GLState {
m_uniformSizesInBytes.clear(); m_uniformSizesInBytes.clear();
m_uniformOffsets.clear(); m_uniformOffsets.clear();
m_globalUboScratch.clear(); m_globalUboScratch.clear();
// kInvalidUniformOffset marks locations that end up without global-UBO backing m_uniformOffsets.resize(m_maxUniformLocation + 1);
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass m_uniformSizesInBytes.resize(m_maxUniformLocation + 1);
// below gives those locations tail storage so glUniform* always has a target.
m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset);
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0);
for (SizeT i = 0; i < m_generatedSpirv.size(); i++) { for (SizeT i = 0; i < m_generatedSpirv.size(); i++) {
auto& spv = m_generatedSpirv[i]; auto& spv = m_generatedSpirv[i];
@@ -930,95 +653,31 @@ namespace MobileGL::MG_State::GLState {
m_globalUboScratch.resize(size); m_globalUboScratch.resize(size);
} }
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) { for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
// reflection keys arrays as "arr[0]" (GL naming), so retry with the m_uniformOffsets[m_uniformLocations[name]] = offset;
// suffix before declaring the uniform unbacked. MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u assigned to location %u",
auto locationIt = m_uniformLocations.find(name); m_externalIndex, name.c_str(), offset, m_uniformLocations[name]);
if (locationIt == m_uniformLocations.end()) { } else {
locationIt = m_uniformLocations.find(name + "[0]"); MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
} "m_uniformLocations",
if (locationIt == m_uniformLocations.end()) { m_externalIndex, name.c_str(), offset);
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in " }
"m_uniformLocations", }
m_externalIndex, name.c_str(), offset); for (const auto& [name, size] : meta.plainUniformMemberSizesInBytes) {
continue; if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
} m_uniformSizesInBytes[m_uniformLocations[name]] = size;
const Uint baseLocation = locationIt->second; MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u assigned to location %u",
if (!IsValidUniformLocation(static_cast<Int>(baseLocation))) { m_externalIndex, name.c_str(), size, m_uniformLocations[name]);
continue; } else {
} MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u but not found in "
"m_uniformLocations",
const Int uniformIndex = m_uniformIndexInTProgram[baseLocation]; m_externalIndex, name.c_str(), size);
const GLint arraySize = GetActiveUniformArraySize(uniformIndex); }
SizeT memberSize = 0;
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
memberSize = sizeIt->second;
}
Uint arrayStride = 0;
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
arrayStride = strideIt->second;
}
// Array uniforms span one location per element (see DoReflection);
// give each element its real byte offset inside the UBO.
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
for (GLint element = 0; element < elementCount; ++element) {
const Uint location = baseLocation + static_cast<Uint>(element);
if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) {
break;
}
m_uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
}
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned "
"to locations %u..%u",
m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
baseLocation + static_cast<Uint>(elementCount) - 1);
} }
MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata", MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata",
m_externalIndex, i); m_externalIndex, i);
} }
} }
// Fallback pass: a linked program's active non-opaque uniforms must accept
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
// them (AggressiveDCE can remove a dead loop together with the only loads of a
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
// never reads these bytes, so this only keeps the GL-visible state coherent.
for (Uint location = 0; location <= m_maxUniformLocation; ++location) {
if (m_uniformOffsets[location] != kInvalidUniformOffset) continue;
if (!IsValidUniformLocation(static_cast<Int>(location))) continue;
const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
const glslang::TType* type = uniform.getType();
if (type != nullptr && type->isOpaque()) continue;
if (uniform.index >= 0 && uniform.index < m_program->getNumUniformBlocks() &&
std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(),
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
// Member of a named uniform block: not settable through glUniform*, so it
// needs no global-UBO shadow storage.
continue;
}
// std140-style slot: the matrix upload paths write column vectors at
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
if (type != nullptr && type->isMatrix()) {
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
}
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
m_globalUboScratch.resize(slotOffset + slotSize, 0);
m_uniformOffsets[location] = static_cast<Uint>(slotOffset);
m_uniformSizesInBytes[location] = slotSize;
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the "
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
}
} }
void ProgramObject::WaitUntilGenerationCompleted() const { void ProgramObject::WaitUntilGenerationCompleted() const {

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