mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eafce6e28 | ||
|
|
9b7414c0d9 | ||
|
|
7acfd19582 | ||
|
|
aa45936f33 | ||
|
|
1146188ed4 |
@@ -10,22 +10,11 @@ case_name="$1"
|
||||
fixture_dir="${2:-tools/trace_replay/fixtures}"
|
||||
python_bin="${PYTHON:-python3}"
|
||||
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
|
||||
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
|
||||
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
|
||||
|
||||
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
|
||||
python_bin=python
|
||||
fi
|
||||
|
||||
if ! [[ "${download_attempts}" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS must be a positive integer: ${download_attempts}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! [[ "${retry_delay}" =~ ^[0-9]+$ ]]; then
|
||||
echo "MOBILEGL_TRACE_FIXTURE_RETRY_DELAY must be a non-negative integer: ${retry_delay}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
|
||||
--format fixture-files \
|
||||
--case "${case_name}" \
|
||||
@@ -45,140 +34,6 @@ if [ "${case_name}" = "OpenRA" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_file_from_mirror() {
|
||||
local file="$1"
|
||||
local url="$2"
|
||||
local metadata
|
||||
local expected_oid
|
||||
local expected_size
|
||||
local tmp_file="${file}.tmp"
|
||||
local attempt
|
||||
local partial_size
|
||||
local curl_status
|
||||
|
||||
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
|
||||
|
||||
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
|
||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||
mv "${tmp_file}" "${file}"
|
||||
return 0
|
||||
fi
|
||||
echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
else
|
||||
curl_status=$?
|
||||
partial_size=0
|
||||
if [ -f "${tmp_file}" ]; then
|
||||
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
|
||||
fi
|
||||
|
||||
if [ "${partial_size}" = "${expected_size}" ]; then
|
||||
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
|
||||
mv "${tmp_file}" "${file}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
elif [ "${partial_size}" -gt "${expected_size}" ]; then
|
||||
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
elif [ "${curl_status}" -eq 33 ]; then
|
||||
echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2
|
||||
rm -f "${tmp_file}"
|
||||
partial_size=0
|
||||
fi
|
||||
|
||||
echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2
|
||||
fi
|
||||
|
||||
if [ "${attempt}" -lt "${download_attempts}" ]; then
|
||||
sleep "${retry_delay}"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -f "${tmp_file}"
|
||||
return 1
|
||||
}
|
||||
|
||||
fetch_from_mirror() {
|
||||
mkdir -p "${fixture_dir}"
|
||||
for file in "${files[@]}"; do
|
||||
@@ -187,9 +42,11 @@ fetch_from_mirror() {
|
||||
name="$(basename "${file}")"
|
||||
url="${mirror_base%/}/${name}"
|
||||
echo "Fetching trace fixture from mirror: ${url}"
|
||||
if ! fetch_file_from_mirror "${file}" "${url}"; then
|
||||
if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
|
||||
rm -f "${file}.tmp"
|
||||
return 1
|
||||
fi
|
||||
mv "${file}.tmp" "${file}"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -202,7 +59,9 @@ else
|
||||
fi
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
metadata="$(get_lfs_metadata "${file}")"
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
|
||||
test -s "${file}"
|
||||
if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
|
||||
echo "failed to hydrate LFS fixture: ${file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
+32
-60
@@ -227,8 +227,6 @@ jobs:
|
||||
env:
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -247,11 +245,11 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ env.ANDROID_AVD_HOME }}
|
||||
${{ env.ANDROID_SDK_ROOT }}/emulator
|
||||
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
||||
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
||||
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
/usr/local/lib/android/sdk/emulator
|
||||
/usr/local/lib/android/sdk/platform-tools
|
||||
/usr/local/lib/android/sdk/platforms/android-35
|
||||
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
|
||||
- name: Create AVD
|
||||
if: steps.android-avd-cache.outputs.cache-hit != 'true'
|
||||
@@ -276,8 +274,6 @@ jobs:
|
||||
env:
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
@@ -292,7 +288,7 @@ jobs:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 8
|
||||
swap-size-gb: 16
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -328,11 +324,11 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
${{ env.ANDROID_AVD_HOME }}
|
||||
${{ env.ANDROID_SDK_ROOT }}/emulator
|
||||
${{ env.ANDROID_SDK_ROOT }}/platform-tools
|
||||
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
|
||||
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
/usr/local/lib/android/sdk/emulator
|
||||
/usr/local/lib/android/sdk/platform-tools
|
||||
/usr/local/lib/android/sdk/platforms/android-35
|
||||
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
|
||||
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
|
||||
|
||||
- name: Download retrace APK
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -382,51 +378,27 @@ jobs:
|
||||
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--coherent-as-flush)
|
||||
fi
|
||||
|
||||
run_retrace() {
|
||||
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
||||
--apk-file "${apk_file}" \
|
||||
--package top.mobilegl.plugin.trace \
|
||||
--backend "${{ matrix.backend.name }}" \
|
||||
--result-root android-retrace-result \
|
||||
--fixture-root android-retrace-fixture \
|
||||
--case "${{ matrix.case.name }}" \
|
||||
--trace-archive "${{ matrix.case.trace_archive }}" \
|
||||
--trace-file "${{ matrix.case.trace_file }}" \
|
||||
--golden "${{ matrix.case.golden }}" \
|
||||
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
|
||||
--target-call "${{ matrix.case.target_call }}" \
|
||||
--width "${{ matrix.case.width }}" \
|
||||
--height "${{ matrix.case.height }}" \
|
||||
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
|
||||
--crop-x "${{ matrix.case.crop_x }}" \
|
||||
--crop-y "${{ matrix.case.crop_y }}" \
|
||||
--crop-width "${{ matrix.case.crop_width }}" \
|
||||
--crop-height "${{ matrix.case.crop_height }}" \
|
||||
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
|
||||
"${extra_retrace_args[@]}"
|
||||
}
|
||||
|
||||
retrace_status=0
|
||||
run_retrace || retrace_status=$?
|
||||
if [ "${retrace_status}" -eq 75 ]; then
|
||||
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
--pid-file "${EMULATOR_PID_FILE}"
|
||||
adb kill-server || true
|
||||
sleep 2
|
||||
sh android-plugin/run-avd-ci.sh start \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--gpu "${{ matrix.backend.gpu }}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
--pid-file "${EMULATOR_PID_FILE}" \
|
||||
--boot-timeout 300
|
||||
run_retrace
|
||||
elif [ "${retrace_status}" -ne 0 ]; then
|
||||
exit "${retrace_status}"
|
||||
fi
|
||||
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
||||
--apk-file "${apk_file}" \
|
||||
--package top.mobilegl.plugin.trace \
|
||||
--backend "${{ matrix.backend.name }}" \
|
||||
--result-root android-retrace-result \
|
||||
--fixture-root android-retrace-fixture \
|
||||
--case "${{ matrix.case.name }}" \
|
||||
--trace-archive "${{ matrix.case.trace_archive }}" \
|
||||
--trace-file "${{ matrix.case.trace_file }}" \
|
||||
--golden "${{ matrix.case.golden }}" \
|
||||
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
|
||||
--target-call "${{ matrix.case.target_call }}" \
|
||||
--width "${{ matrix.case.width }}" \
|
||||
--height "${{ matrix.case.height }}" \
|
||||
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
|
||||
--crop-x "${{ matrix.case.crop_x }}" \
|
||||
--crop-y "${{ matrix.case.crop_y }}" \
|
||||
--crop-width "${{ matrix.case.crop_width }}" \
|
||||
--crop-height "${{ matrix.case.crop_height }}" \
|
||||
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
|
||||
"${extra_retrace_args[@]}"
|
||||
|
||||
- name: Collect retrace summary inputs
|
||||
if: always()
|
||||
|
||||
@@ -107,7 +107,6 @@ jobs:
|
||||
--exclude='build.ninja' \
|
||||
--exclude='cmake_install.cmake' \
|
||||
-czf ci-artifacts/mobilegl-linux-runtime.tgz \
|
||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||
"${SHARED_LIBS[@]}"
|
||||
@@ -157,12 +156,12 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Test
|
||||
working-directory: build-linux
|
||||
working-directory: build-linux/MobileGL/MG_Test
|
||||
run: |
|
||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||
ctest -V -L unit --no-tests=error
|
||||
ctest -V
|
||||
else
|
||||
ctest --output-on-failure -L unit --no-tests=error
|
||||
ctest --output-on-failure
|
||||
fi
|
||||
|
||||
benchmark:
|
||||
@@ -203,8 +202,8 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Benchmark
|
||||
working-directory: build-linux
|
||||
run: ctest -V -C Release -L benchmark --no-tests=error
|
||||
working-directory: build-linux/MobileGL/MG_Benchmark
|
||||
run: ctest -V -C Release
|
||||
|
||||
build-retrace:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+1
-10
@@ -190,7 +190,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
@@ -476,15 +475,6 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
|
||||
endif ()
|
||||
|
||||
if (NOT ANDROID)
|
||||
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
|
||||
# at the build-tree root. This lets `ctest` be invoked from the top-level
|
||||
# build directory (IDE "run all tests", CI) and discover every test in the
|
||||
# subdirectories below, instead of having to descend into each
|
||||
# MG_Test/MG_Benchmark subdirectory. Tests are tagged with CTest labels
|
||||
# (unit / benchmark / integration), so e.g. `ctest -L unit` selects just
|
||||
# the unit suite.
|
||||
enable_testing()
|
||||
|
||||
if (MOBILEGL_BUILD_TEST)
|
||||
add_subdirectory(MobileGL/MG_Test)
|
||||
endif()
|
||||
@@ -494,6 +484,7 @@ if (NOT ANDROID)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_TRACE_REPLAY)
|
||||
enable_testing()
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -61,12 +61,6 @@ namespace MobileGL::MG_Config {
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -122,7 +122,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -232,9 +232,6 @@ namespace MobileGL {
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
Float SmoothLineWidthRangeMin = 1.0f;
|
||||
|
||||
@@ -605,9 +605,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no timer queries yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedTimerQueryExtension.
|
||||
.Extensions = BuildAdvertisedExtensions(false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -627,9 +627,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedTimerQueryExtension() {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(AreTimerQueriesSupported());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -673,11 +672,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
|
||||
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
|
||||
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
|
||||
// the extension list is first built).
|
||||
UpdateAdvertisedTimerQueryExtension();
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -819,7 +818,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported) {
|
||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
@@ -837,14 +836,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Only advertised when the host ES driver actually filters anisotropically: the sampler
|
||||
// state is accepted regardless, but forwarding it would be a no-op without the extension,
|
||||
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
|
||||
// get plain trilinear.
|
||||
if (anisotropicFilteringSupported) {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -949,7 +940,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_GLESCapabilities.SmoothLineWidthRangeMin;
|
||||
|
||||
@@ -66,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
// for a device whose timer queries are (or are not) usable. The
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,6 @@
|
||||
operation Utils::CheckGLESError();
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Re-establishes the frontend texture-unit bindings on the native ES context.
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
|
||||
@@ -1405,11 +1405,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_prevSkipPixels = s_skipPixels;
|
||||
m_prevImageHeight = s_imageHeight;
|
||||
m_prevSkipImages = s_skipImages;
|
||||
// Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits
|
||||
// width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1.
|
||||
// Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride,
|
||||
// shifting every row of a non-multiple-of-4 upload by one pixel.
|
||||
Apply(1, 0, 0, 0, 0, 0);
|
||||
Apply(4, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
~ScopedDefaultUnpackState() {
|
||||
@@ -1476,13 +1472,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return 2;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16:
|
||||
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
|
||||
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return 3;
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16:
|
||||
case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow)
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return 4;
|
||||
default:
|
||||
@@ -1577,7 +1570,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
|
||||
stateTextureObject->GetExternalIndex());
|
||||
|
||||
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
|
||||
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
|
||||
auto targetInternal = stateTextureObject->GetTarget();
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
@@ -1698,7 +1691,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
auto* pData = (levelDirty && levelByteSize != 0)
|
||||
? textureMipmapObject->MapMipmapData(uploadTarget, level)
|
||||
: nullptr;
|
||||
@@ -1709,22 +1702,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
|
||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||
switch (stateTextureObject->GetTarget()) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
g_GLESFuncs.glTexImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
|
||||
0, glFormat, glType, uploadData);
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
g_GLESFuncs.glTexImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
|
||||
static_cast<GLsizei>(levelTexelSize.x()), static_cast<GLsizei>(levelTexelSize.y()),
|
||||
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled texture target %s",
|
||||
@@ -1788,20 +1778,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
|
||||
switch (MapToBackendTextureTarget(targetInternal)) {
|
||||
switch (targetInternal) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
g_GLESFuncs.glTexStorage2D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
|
||||
static_cast<GLsizei>(storageSize.x()),
|
||||
static_cast<GLsizei>(storageSize.y()));
|
||||
static_cast<GLsizei>(baseSize.x()),
|
||||
static_cast<GLsizei>(baseSize.y()));
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
|
||||
static_cast<GLsizei>(storageSize.x()),
|
||||
static_cast<GLsizei>(storageSize.y()),
|
||||
static_cast<GLsizei>(storageSize.z()));
|
||||
static_cast<GLsizei>(baseSize.x()),
|
||||
static_cast<GLsizei>(baseSize.y()),
|
||||
static_cast<GLsizei>(baseSize.z()));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled immutable texture target %s",
|
||||
@@ -1825,7 +1813,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (levelDirty && levelByteSize != 0) {
|
||||
auto levelTexelSize =
|
||||
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
|
||||
Vector<Float> convertedUploadData;
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
@@ -1834,23 +1822,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(targetInternal, levelTexelSize);
|
||||
switch (MapToBackendTextureTarget(targetInternal)) {
|
||||
switch (targetInternal) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
g_GLESFuncs.glTexSubImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()), glFormat, glType, uploadData);
|
||||
static_cast<GLsizei>(levelTexelSize.x()),
|
||||
static_cast<GLsizei>(levelTexelSize.y()), glFormat, glType, uploadData);
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
g_GLESFuncs.glTexSubImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), glFormat, glType, uploadData);
|
||||
static_cast<GLsizei>(levelTexelSize.x()),
|
||||
static_cast<GLsizei>(levelTexelSize.y()),
|
||||
static_cast<GLsizei>(levelTexelSize.z()), glFormat, glType, uploadData);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -1877,7 +1862,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
auto* pData = (levelDirty && levelByteSize != 0)
|
||||
? textureMipmapObject->MapMipmapData(uploadTarget, level)
|
||||
: nullptr;
|
||||
@@ -1894,23 +1879,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
auto textureTarget = stateTextureObject->GetTarget();
|
||||
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
|
||||
switch (MapToBackendTextureTarget(textureTarget)) {
|
||||
// TODO: handle more texture types
|
||||
switch (textureTarget) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap: {
|
||||
g_GLESFuncs.glTexImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()), 0, glFormat, glType, uploadData);
|
||||
static_cast<GLsizei>(levelTexelSize.x()),
|
||||
static_cast<GLsizei>(levelTexelSize.y()), 0, glFormat, glType, uploadData);
|
||||
break;
|
||||
}
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray: {
|
||||
case TextureTarget::Texture3D: {
|
||||
g_GLESFuncs.glTexImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
|
||||
static_cast<GLsizei>(levelTexelSize.x()),
|
||||
static_cast<GLsizei>(levelTexelSize.y()),
|
||||
static_cast<GLsizei>(levelTexelSize.z()), 0, glFormat, glType, uploadData);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -1977,7 +1961,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(),
|
||||
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
|
||||
|
||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
auto glUploadTarget = MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
DebugImpl::ErrorLopper::Loop(
|
||||
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||
@@ -1990,22 +1974,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
|
||||
convertedUploadData);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||
switch (stateTextureObject->GetTarget()) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()), glFormat, glType,
|
||||
static_cast<GLsizei>(texelSize.x()),
|
||||
static_cast<GLsizei>(texelSize.y()), glFormat, glType,
|
||||
uploadData);
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
|
||||
static_cast<GLsizei>(texelSize.x()),
|
||||
static_cast<GLsizei>(texelSize.y()),
|
||||
static_cast<GLsizei>(texelSize.z()), glFormat, glType,
|
||||
uploadData);
|
||||
break;
|
||||
default:
|
||||
@@ -2098,7 +2079,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
|
||||
m_backendTextureId, stateTextureObject->GetExternalIndex());
|
||||
|
||||
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
|
||||
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
|
||||
auto targetInternal = stateTextureObject->GetTarget();
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
@@ -2206,7 +2187,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId,
|
||||
stateTextureObject->GetExternalIndex());
|
||||
|
||||
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
|
||||
GLenum target = MG_Util::ConvertTextureTargetToGLEnum(stateTextureObject->GetTarget());
|
||||
auto targetInternal = stateTextureObject->GetTarget();
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
@@ -2285,11 +2266,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_activeTextureUnit = unit;
|
||||
}
|
||||
|
||||
void UnbindTexture(Uint unit, GLenum target) { // Activates `unit` when an unbind is issued
|
||||
void UnbindTexture(Uint unit, GLenum target) { // Active unit will be modified
|
||||
if (unit != g_activeTextureUnit) {
|
||||
ActivateTextureUnit(unit);
|
||||
}
|
||||
|
||||
auto targetN = static_cast<SizeT>(MG_Util::ConvertGLEnumToTextureTarget(target));
|
||||
if (g_boundTexturesCache[unit][targetN] == nullptr) return;
|
||||
|
||||
ActivateTextureUnit(unit);
|
||||
g_GLESFuncs.glBindTexture(target, 0);
|
||||
g_boundTexturesCache[unit][targetN] = nullptr;
|
||||
}
|
||||
@@ -2360,21 +2344,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
|
||||
uploadTarget == TextureUploadTarget::Texture3D ||
|
||||
uploadTarget == TextureUploadTarget::Texture2DArray ||
|
||||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
|
||||
// Single slice/layer of a 3D or array texture: ES has no
|
||||
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
|
||||
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLayer()));
|
||||
} else {
|
||||
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
|
||||
attachmentObject.GetTextureUploadTarget());
|
||||
auto glTextureTarget =
|
||||
MG_Util::ConvertTextureUploadTargetToGLEnum(attachmentObject.GetTextureUploadTarget());
|
||||
if (glTextureTarget == GL_UNKNOWN_MGL) {
|
||||
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
||||
glTextureTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
|
||||
}
|
||||
backendTextureObject->Bind(glTextureTarget);
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
@@ -2488,14 +2462,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
drawBufferClean = true;
|
||||
}
|
||||
|
||||
// glDrawBuffers writes the state of the FBO bound to GL_DRAW_FRAMEBUFFER.
|
||||
// When this object is only bound as the READ target the call would land on
|
||||
// whatever framebuffer is draw-bound AND falsely stamp this object's memo,
|
||||
// so the later draw-target sync skips as "clean" while the real state is
|
||||
// stale (Minecraft 26.x OIT: the scratch clear-FBO kept draw buffers NONE
|
||||
// from its blit-destination configuration, silently dropping every
|
||||
// offscreen color clear).
|
||||
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
|
||||
if (!drawBufferClean) {
|
||||
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
|
||||
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
|
||||
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
|
||||
@@ -2520,8 +2487,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
nEffectiveBuffers = i + 1;
|
||||
}
|
||||
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
|
||||
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
|
||||
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
|
||||
}
|
||||
|
||||
if (asTarget == FramebufferTarget::Draw) {
|
||||
@@ -2544,10 +2509,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
|
||||
}
|
||||
|
||||
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
|
||||
// only apply (and stamp the memo) when this object is bound as READ.
|
||||
// 2. Remap read buffer
|
||||
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
|
||||
if (frontendReadBuf != m_frontendReadBuffer && asTarget == FramebufferTarget::Read) {
|
||||
if (frontendReadBuf != m_frontendReadBuffer) {
|
||||
m_frontendReadBuffer = frontendReadBuf;
|
||||
|
||||
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
|
||||
@@ -2654,12 +2618,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
g_backendFramebufferObjects;
|
||||
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions = {0};
|
||||
// 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.
|
||||
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions = {0};
|
||||
Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects = {};
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
@@ -2770,20 +2728,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||
// ForceSupporterOutput swaps the fragment header to highp, that member reads
|
||||
// back as highp and the ES driver refuses to link ("definitions of uniform
|
||||
// block ... do not match"). Strip the hint from block structs so both stages
|
||||
// declare the member highp; nothing else about emission changes.
|
||||
Vector<unsigned int> uboPrecisionSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||
*effectiveSpirv, uboPrecisionSpirv) &&
|
||||
!uboPrecisionSpirv.empty()) {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
@@ -2948,9 +2892,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
m_uniformBlockBackendIndices[static_cast<SizeT>(i)] = static_cast<Int>(backendBlkIdx);
|
||||
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding);
|
||||
MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx,
|
||||
lastUBOBinding);
|
||||
}
|
||||
|
||||
m_samplerUniformBindings.clear();
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||
@@ -255,48 +254,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace TextureImpl {
|
||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
||||
// coordinate padding for 1D/1D-array shaders.
|
||||
return target != TextureTarget::TextureRectangle;
|
||||
}
|
||||
|
||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
default:
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
inline GLenum ConvertTextureTargetToBackendGLEnum(TextureTarget target) {
|
||||
return MG_Util::ConvertTextureTargetToGLEnum(MapToBackendTextureTarget(target));
|
||||
}
|
||||
|
||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||
switch (uploadTarget) {
|
||||
case TextureUploadTarget::Texture1D:
|
||||
return GL_TEXTURE_2D;
|
||||
case TextureUploadTarget::Texture1DArray:
|
||||
return GL_TEXTURE_2D_ARRAY;
|
||||
default:
|
||||
return MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// 1D arrays store layers in the state-side height; the ES 2D-array image keeps height 1 and
|
||||
// moves the layer count into depth.
|
||||
inline IntVec3 GetBackendUploadSize(TextureTarget stateTarget, const IntVec3& texelSize) {
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
return {texelSize.x(), 1, texelSize.y()};
|
||||
}
|
||||
return texelSize;
|
||||
if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle ||
|
||||
target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Bool IsMultisampleTextureTarget(TextureTarget target) {
|
||||
@@ -408,12 +369,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||
g_backendFramebufferObjects;
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
||||
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
||||
// must re-sync it even when the binding-slot version has not moved.
|
||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||
g_fboSyncedObjects;
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/Math/SmallFloat.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
@@ -452,317 +448,4 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
} // namespace Utils
|
||||
|
||||
// ---- Client-format readback conversion helpers -------------------------------------------------
|
||||
// ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack
|
||||
// it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so
|
||||
// unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror
|
||||
// the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers).
|
||||
namespace ReadbackImpl {
|
||||
using MG_Util::DecodeHalfBitsToFloat;
|
||||
using MG_Util::EncodeFloatToHalfBits;
|
||||
|
||||
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) {
|
||||
switch (format) {
|
||||
case GL_RED: outMapping = {{0, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_RED_INTEGER: outMapping = {{0, 0, 0, 0}, 1, true}; return true;
|
||||
// Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue):
|
||||
// the destination holds one component sourced from the named channel of the wide RGBA read.
|
||||
// GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the
|
||||
// legacy alpha-texture upload hack.
|
||||
case GL_GREEN: outMapping = {{1, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_GREEN_INTEGER: outMapping = {{1, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_BLUE: outMapping = {{2, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_BLUE_INTEGER: outMapping = {{2, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_ALPHA: outMapping = {{3, 0, 0, 0}, 1, false}; return true;
|
||||
case GL_ALPHA_INTEGER: outMapping = {{3, 0, 0, 0}, 1, true}; return true;
|
||||
case GL_RG: outMapping = {{0, 1, 0, 0}, 2, false}; return true;
|
||||
case GL_RG_INTEGER: outMapping = {{0, 1, 0, 0}, 2, true}; return true;
|
||||
case GL_RGB: outMapping = {{0, 1, 2, 0}, 3, false}; return true;
|
||||
case GL_RGB_INTEGER: outMapping = {{0, 1, 2, 0}, 3, true}; return true;
|
||||
case GL_BGR: outMapping = {{2, 1, 0, 0}, 3, false}; return true;
|
||||
case GL_BGR_INTEGER: outMapping = {{2, 1, 0, 0}, 3, true}; return true;
|
||||
case GL_RGBA: outMapping = {{0, 1, 2, 3}, 4, false}; return true;
|
||||
case GL_RGBA_INTEGER: outMapping = {{0, 1, 2, 3}, 4, true}; return true;
|
||||
case GL_BGRA: outMapping = {{2, 1, 0, 3}, 4, false}; return true;
|
||||
case GL_BGRA_INTEGER: outMapping = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) {
|
||||
switch (type) {
|
||||
// Non-REV types pack the first format component starting at the most significant bit,
|
||||
// *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5:
|
||||
// R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15).
|
||||
case GL_UNSIGNED_BYTE_3_3_2: out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false}; return true;
|
||||
case GL_UNSIGNED_BYTE_2_3_3_REV: out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_6_5: out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_6_5_REV: out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4: out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_4_4_4_4_REV: out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_5_5_5_1: out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false}; return true;
|
||||
case GL_UNSIGNED_SHORT_1_5_5_5_REV: out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false}; return true;
|
||||
case GL_UNSIGNED_INT_8_8_8_8: out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_8_8_8_8_REV: out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_10_10_10_2: out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false}; return true;
|
||||
case GL_UNSIGNED_INT_2_10_10_10_REV: out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false}; return true;
|
||||
// Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit
|
||||
// exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field.
|
||||
case GL_UNSIGNED_INT_10F_11F_11F_REV: out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true}; return true;
|
||||
case GL_UNSIGNED_INT_5_9_9_9_REV: out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true}; return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SizeT GetReadbackComponentSize(GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (GetPackedReadbackLayout(type, packedLayout)) {
|
||||
return packedLayout.byteSize;
|
||||
}
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
case GL_BYTE:
|
||||
return 1;
|
||||
case GL_UNSIGNED_SHORT:
|
||||
case GL_SHORT:
|
||||
case GL_HALF_FLOAT:
|
||||
return 2;
|
||||
case GL_UNSIGNED_INT:
|
||||
case GL_INT:
|
||||
case GL_FLOAT:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (GetPackedReadbackLayout(type, packedLayout)) {
|
||||
if (packedLayout.fieldCount != mapping.channelCount) {
|
||||
return 0; // 3-field packed types pair with 3-component formats only, 4 with 4
|
||||
}
|
||||
if (mapping.isInteger && packedLayout.isFloatPacked) {
|
||||
return 0; // packed-float RGB types never pair with integer formats
|
||||
}
|
||||
return packedLayout.byteSize;
|
||||
}
|
||||
if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) {
|
||||
return 0;
|
||||
}
|
||||
const SizeT componentSize = GetReadbackComponentSize(type);
|
||||
return componentSize == 0 ? 0 : static_cast<SizeT>(mapping.channelCount) * componentSize;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
|
||||
switch (byteSize) {
|
||||
case 1: {
|
||||
const auto out = static_cast<Uint8>(word);
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
const auto out = static_cast<Uint16>(word);
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Memcpy(dst, &word, sizeof(word));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
|
||||
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
|
||||
Uint32 EncodeFloatToUnsignedF11(Float value) { return MG_Util::EncodeFloatToUnsignedF11(value); }
|
||||
Uint32 EncodeFloatToUnsignedF10(Float value) { return MG_Util::EncodeFloatToUnsignedF10(value); }
|
||||
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) { return MG_Util::EncodeSharedExponentRGB9E5(rgb); }
|
||||
|
||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||
const ReadbackChannelMapping& mapping, GLenum type) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPacked = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
|
||||
|
||||
for (SizeT col = 0; col < width; ++col) {
|
||||
const Uint8* srcPixel = src + col * srcPixelBytes;
|
||||
Uint8* dstPixel = dst + col * dstPixelBytes;
|
||||
if (mapping.isInteger) {
|
||||
Int64 srcValues[4];
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = wideType == GL_INT
|
||||
? static_cast<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
|
||||
: static_cast<Int64>(reinterpret_cast<const Uint32*>(srcPixel)[c]);
|
||||
}
|
||||
if (isPacked) {
|
||||
// Integer sources clamp each component to the unsigned range of its field
|
||||
// (GL 3.3 section 4.3.1 final conversion).
|
||||
Uint32 word = 0;
|
||||
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
|
||||
const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1;
|
||||
const auto v = static_cast<Uint32>(
|
||||
std::clamp<Int64>(srcValues[mapping.sourceChannel[ch]], 0, fieldMax));
|
||||
word |= v << packedLayout.shift[ch];
|
||||
}
|
||||
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
|
||||
} else {
|
||||
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
|
||||
const Int64 v = srcValues[mapping.sourceChannel[ch]];
|
||||
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
|
||||
break;
|
||||
case GL_BYTE: {
|
||||
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_SHORT: {
|
||||
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_SHORT: {
|
||||
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_INT: {
|
||||
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_INT: {
|
||||
const auto out =
|
||||
static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Float srcValues[4];
|
||||
switch (wideType) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = static_cast<Float>(srcPixel[c]) / 255.0f;
|
||||
}
|
||||
break;
|
||||
case GL_BYTE:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = std::max(
|
||||
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
|
||||
}
|
||||
break;
|
||||
case GL_UNSIGNED_SHORT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] =
|
||||
static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
|
||||
}
|
||||
break;
|
||||
case GL_SHORT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = std::max(
|
||||
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
|
||||
}
|
||||
break;
|
||||
case GL_HALF_FLOAT:
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
|
||||
}
|
||||
break;
|
||||
default: // GL_FLOAT
|
||||
for (Int c = 0; c < 4; ++c) {
|
||||
srcValues[c] = reinterpret_cast<const Float*>(srcPixel)[c];
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isPacked) {
|
||||
Uint32 word = 0;
|
||||
if (packedLayout.isFloatPacked) {
|
||||
const Float fields[3] = {srcValues[mapping.sourceChannel[0]],
|
||||
srcValues[mapping.sourceChannel[1]],
|
||||
srcValues[mapping.sourceChannel[2]]};
|
||||
word = type == GL_UNSIGNED_INT_5_9_9_9_REV
|
||||
? EncodeSharedExponentRGB9E5(fields)
|
||||
: (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) |
|
||||
(EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) |
|
||||
(EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]);
|
||||
} else {
|
||||
// Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field.
|
||||
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
|
||||
const auto fieldMax = static_cast<Float>((1u << packedLayout.width[ch]) - 1u);
|
||||
const auto v = static_cast<Uint32>(std::llround(
|
||||
std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax));
|
||||
word |= v << packedLayout.shift[ch];
|
||||
}
|
||||
}
|
||||
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
|
||||
} else {
|
||||
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
|
||||
const Float v = srcValues[mapping.sourceChannel[ch]];
|
||||
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE:
|
||||
*dstComponent =
|
||||
static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
|
||||
break;
|
||||
case GL_BYTE: {
|
||||
const auto out =
|
||||
static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_SHORT: {
|
||||
const auto out =
|
||||
static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_SHORT: {
|
||||
const auto out =
|
||||
static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_UNSIGNED_INT: {
|
||||
const auto out = static_cast<Uint32>(
|
||||
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_INT: {
|
||||
const auto out = static_cast<Int32>(
|
||||
std::llround(static_cast<Double>(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0));
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case GL_FLOAT:
|
||||
Memcpy(dstComponent, &v, sizeof(v));
|
||||
break;
|
||||
case GL_HALF_FLOAT: {
|
||||
const Uint16 out = EncodeFloatToHalfBits(v);
|
||||
Memcpy(dstComponent, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace ReadbackImpl
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -45,51 +45,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
|
||||
// Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
|
||||
// RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
|
||||
// exercise the exact packing the GL CTS packed_pixels oracle compares against.
|
||||
namespace ReadbackImpl {
|
||||
struct ReadbackChannelMapping {
|
||||
Int sourceChannel[4]; // RGBA source channel feeding each destination component
|
||||
Int channelCount; // destination component count
|
||||
Bool isInteger;
|
||||
};
|
||||
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);
|
||||
|
||||
// Byte size of one destination component of `type`; packed types report the packed word size.
|
||||
// 0 = type not supported by the conversion path.
|
||||
SizeT GetReadbackComponentSize(GLenum type);
|
||||
|
||||
// Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
|
||||
// component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
|
||||
// the packed word: non-REV types pack the first component from the MSB, *_REV types from the
|
||||
// LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
|
||||
struct PackedReadbackLayout {
|
||||
Int fieldCount; // format components stored in the packed word
|
||||
Int width[4]; // bit width of each component's field
|
||||
Int shift[4]; // LSB bit position of each component's field
|
||||
SizeT byteSize; // packed word size in bytes (1, 2 or 4)
|
||||
Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
|
||||
};
|
||||
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);
|
||||
|
||||
// Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
|
||||
Uint32 EncodeFloatToUnsignedF11(Float value);
|
||||
Uint32 EncodeFloatToUnsignedF10(Float value);
|
||||
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);
|
||||
|
||||
// Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
|
||||
// not convertible (unknown type, packed field count != format component count, floating-point
|
||||
// or packed-float type with an integer format).
|
||||
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);
|
||||
|
||||
// Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
|
||||
// client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
|
||||
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||
const ReadbackChannelMapping& mapping, GLenum type);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode);
|
||||
String ForceSupporterOutput(const String& glslCode);
|
||||
|
||||
@@ -488,15 +488,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
.IsCompatibilityProfile = false
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported) {
|
||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
@@ -517,13 +516,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
||||
extensions.push_back(E_GL_ARB_timer_query);
|
||||
}
|
||||
// Only advertised when the samplerAnisotropy device feature was granted: without it the
|
||||
// sampler state is accepted but never applied, and an app trusting the string (LWJGL builds
|
||||
// GLCapabilities from it) would think it enabled anisotropic filtering.
|
||||
if (anisotropicFilteringSupported) {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -638,8 +630,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -689,11 +680,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
// rather than a maximum the sampler manager will never apply.
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
||||
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
||||
: 1.0f;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
||||
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
||||
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
||||
|
||||
@@ -73,8 +73,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include "PipelineFactory.h"
|
||||
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||
switch (topology) {
|
||||
@@ -109,10 +108,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"vkCreatePipelineCache");
|
||||
}
|
||||
|
||||
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
|
||||
s_suppressBlendedDepthWrite = enabled;
|
||||
}
|
||||
|
||||
PipelineFactory::~PipelineFactory() {
|
||||
DestroyAll();
|
||||
if (m_pipelineCache != VK_NULL_HANDLE) {
|
||||
@@ -263,18 +258,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
colorAttachments[i] = payload.colorBlendAttachments[i];
|
||||
}
|
||||
// Suppress depth writes on blended pipelines when the active driver cannot keep
|
||||
// vertex positions invariant across the pipelines of a multi-pass depth-equality
|
||||
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
|
||||
// and the equality-dependent prepass pattern is exactly the case that breaks.
|
||||
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
if (colorAttachments[i].blendEnable == VK_TRUE) {
|
||||
depthStencil.depthWriteEnable = VK_FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
||||
blend.logicOp = payload.logicOp;
|
||||
|
||||
@@ -62,14 +62,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||
void DestroyAll();
|
||||
|
||||
// Driver quirk: suppress depth writes on 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.
|
||||
// Set at renderer initialization based on the active driver.
|
||||
static void SetSuppressBlendedDepthWrite(Bool enabled);
|
||||
|
||||
private:
|
||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||
|
||||
@@ -78,6 +70,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -1697,22 +1697,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
moduleSpirvs[i] = spv;
|
||||
}
|
||||
|
||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||
// per-pipeline compilers cannot vary the position math between passes.
|
||||
{
|
||||
Vector<Uint> invariantSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||
moduleSpirvs[i], invariantSpirv)) {
|
||||
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||
} else {
|
||||
MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; "
|
||||
"keeping the original module",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
|
||||
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
|
||||
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
|
||||
|
||||
@@ -360,12 +360,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
// default has no image it is unsampleable; report it as "unbound" so callers keep
|
||||
// taking their fallback paths instead of trying to sync a storage-less texture.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
|
||||
outTexture.reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -386,15 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
|
||||
// touching the refcount (no atomic inc/dec per binding per draw).
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
// default has no image it is unsampleable; report it as "unbound" so the caller
|
||||
// substitutes its fallback texture exactly like it did for the old null slot.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||
return nullptr;
|
||||
}
|
||||
return texture;
|
||||
return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
@@ -613,15 +599,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
||||
if (!texture) {
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
continue;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
||||
|
||||
@@ -197,20 +197,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return static_cast<VkBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
}
|
||||
|
||||
VkBufferResource* VkBufferManager::GetOrCreateResource(
|
||||
SharedPtr<VkBufferResource> VkBufferManager::GetOrCreateResource(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
|
||||
// Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's
|
||||
// backend-resource SharedPtr (already set, or set below), so callers that only dereference
|
||||
// it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind.
|
||||
const auto& existing = bufferObject->GetBackendResource();
|
||||
auto existing = std::static_pointer_cast<VkBufferResource>(bufferObject->GetBackendResource());
|
||||
if (existing) {
|
||||
return static_cast<VkBufferResource*>(existing.get());
|
||||
return existing;
|
||||
}
|
||||
auto resource = MakeShared<VkBufferResource>();
|
||||
VkBufferResource* raw = resource.get();
|
||||
bufferObject->SetBackendResource(resource);
|
||||
TrackLiveResource(resource);
|
||||
return raw;
|
||||
return resource;
|
||||
}
|
||||
|
||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
private:
|
||||
Bool InitializeTransientArenas();
|
||||
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);
|
||||
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
|
||||
VkMemoryPropertyFlags requiredFlags = 0);
|
||||
|
||||
@@ -602,7 +602,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
}
|
||||
@@ -624,15 +623,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
m_rpFastRbEpoch = m_renderbufferImageEpoch;
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
if (it != m_renderPasses.end())
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
// Color attachment
|
||||
@@ -722,7 +718,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (hasClear) {
|
||||
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
|
||||
.attachmentIndex = attachmentIndex,
|
||||
.colorAttachmentSlot = i,
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
@@ -979,52 +974,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
subpassDesc.preserveAttachmentCount = 0;
|
||||
subpassDesc.pPreserveAttachments = nullptr;
|
||||
|
||||
// External subpass dependencies. Without them there is NO execution/memory
|
||||
// dependency between consecutive render passes (or a pass and a transfer)
|
||||
// touching the same attachments when the image layout does not change — the
|
||||
// layout-transition helper no-ops on identical layouts and no other barrier
|
||||
// exists. Tile-based GPUs then race tile loads against the previous pass's
|
||||
// stores (multi-pass FBO chains like MC 26.3's OIT flicker on Adreno).
|
||||
// Conservative both-ways dependencies: prior writes (attachment output,
|
||||
// depth/stencil, transfer, shader) are made visible to this pass's loads,
|
||||
// and this pass's attachment writes to subsequent sampling/transfer/loads.
|
||||
VkSubpassDependency subpassDependencies[2];
|
||||
subpassDependencies[0] = {};
|
||||
subpassDependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||
subpassDependencies[0].dstSubpass = 0;
|
||||
subpassDependencies[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
subpassDependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
subpassDependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
subpassDependencies[0].dstAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_READ_BIT;
|
||||
subpassDependencies[0].dependencyFlags = 0;
|
||||
subpassDependencies[1] = {};
|
||||
subpassDependencies[1].srcSubpass = 0;
|
||||
subpassDependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||
subpassDependencies[1].srcStageMask =
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||
subpassDependencies[1].srcAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
subpassDependencies[1].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
subpassDependencies[1].dstAccessMask =
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
subpassDependencies[1].dependencyFlags = 0;
|
||||
|
||||
// Render Pass
|
||||
VkRenderPassCreateInfo renderPassCreateInfo;
|
||||
renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||
@@ -1034,8 +983,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.pAttachments = attachmentDescriptions.data();
|
||||
renderPassCreateInfo.subpassCount = 1;
|
||||
renderPassCreateInfo.pSubpasses = &subpassDesc;
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
renderPassCreateInfo.dependencyCount = 0;
|
||||
renderPassCreateInfo.pDependencies = nullptr;
|
||||
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
@@ -1066,7 +1015,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hasDepthStencilAttachment,
|
||||
renderPassSampleCount,
|
||||
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",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(compatibilityHash),
|
||||
@@ -1076,36 +1025,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.x(),
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
return insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
++m_frameCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last use is far past every
|
||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||
// safely (RenderPassEntry's destructor releases the handles).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
it = m_renderPasses.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||
// TODO: Transition all the attachments into proper layout before starting the render pass
|
||||
VkRenderPassBeginInfo renderPassBeginInfo;
|
||||
|
||||
@@ -27,12 +27,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
struct PendingClearAttachmentInfo {
|
||||
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
|
||||
Uint32 attachmentIndex = 0;
|
||||
// Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
|
||||
// draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
|
||||
// Only meaningful for color clears.
|
||||
Uint32 colorAttachmentSlot = 0;
|
||||
PendingClearKey key{};
|
||||
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
|
||||
Bool hasInlinePayload = false;
|
||||
@@ -73,10 +68,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool hasDepthStencilAttachment = false;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
IntVec2 extent = {0, 0};
|
||||
// VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments).
|
||||
Uint32 layers = 1;
|
||||
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
|
||||
Uint64 lastUsedFrame = 0;
|
||||
Uint32 subpass = 0;
|
||||
|
||||
RenderPassEntry() = default;
|
||||
RenderPassEntry(const RenderPassEntry&) = delete;
|
||||
@@ -92,8 +84,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
std::swap(sampleCount, that.sampleCount);
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
std::swap(subpass, that.subpass);
|
||||
}
|
||||
RenderPassEntry(
|
||||
Uint64 hash,
|
||||
@@ -106,7 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 colorAttachmentCount,
|
||||
Bool hasDepthStencilAttachment,
|
||||
VkSampleCountFlagBits sampleCount,
|
||||
IntVec2 extent, Uint32 layers):
|
||||
IntVec2 extent, int subpass):
|
||||
hash(hash),
|
||||
renderPass(renderpass),
|
||||
framebuffer(framebuffer),
|
||||
@@ -118,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hasDepthStencilAttachment(hasDepthStencilAttachment),
|
||||
sampleCount(sampleCount),
|
||||
extent(extent),
|
||||
layers(layers)
|
||||
subpass(subpass)
|
||||
{}
|
||||
|
||||
~RenderPassEntry() {
|
||||
@@ -175,9 +166,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
|
||||
// Frame boundary hook: ages the render-pass cache and evicts long-unused
|
||||
// entries (their command buffers retired many frames ago).
|
||||
void OnPresent();
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
|
||||
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
|
||||
static ActiveRenderPassInfo* GetActiveRenderPass();
|
||||
@@ -190,8 +178,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkTextureManager& m_textureManager;
|
||||
SwapchainObject& m_swapchainObject;
|
||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
|
||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||
|
||||
@@ -58,24 +58,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
m_device = initInfo.device;
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
}
|
||||
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const {
|
||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
|
||||
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
|
||||
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
|
||||
sampler.GetMagFilter() != SamplerFilterMode::Linear) {
|
||||
return 1.0f;
|
||||
}
|
||||
return std::clamp(sampler.GetMaxAnisotropy(), 1.0f, m_maxSamplerAnisotropy);
|
||||
}
|
||||
|
||||
void VkSamplerManager::Shutdown() {
|
||||
for (auto& [_, sampler] : m_samplers) {
|
||||
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
|
||||
@@ -112,11 +99,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
const auto lodBias = sampler.GetLodBias();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
|
||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
// Anisotropy is currently an accepted frontend-only state on DirectVulkan.
|
||||
// Keep it out of the key so changing this no-op does not manufacture duplicate
|
||||
// VkSamplers while sampler versioning still exposes the new frontend value.
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = ResolveCompareFunc(sampler, texture);
|
||||
@@ -143,11 +128,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||
samplerInfo.mipLodBias = sampler.GetLodBias();
|
||||
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
|
||||
// different samplers or silently create duplicates.
|
||||
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
|
||||
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||
// DirectVulkan does not yet plumb samplerAnisotropy feature/limit discovery;
|
||||
// preserve the accepted frontend state without requesting an unsupported feature.
|
||||
samplerInfo.anisotropyEnable = VK_FALSE;
|
||||
samplerInfo.maxAnisotropy = 1.0f;
|
||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
||||
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
|
||||
@@ -24,10 +24,6 @@ public:
|
||||
struct InitInfo {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig* config = nullptr;
|
||||
// The samplerAnisotropy device feature was requested and granted at vkCreateDevice.
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
@@ -53,16 +49,9 @@ private:
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
|
||||
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -21,13 +21,9 @@
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#ifdef __ANDROID__
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
@@ -950,7 +946,6 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
|
||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(
|
||||
baseTexelSize.x(),
|
||||
@@ -1888,36 +1883,13 @@ void main() {
|
||||
|
||||
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
|
||||
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
|
||||
{
|
||||
// Qualcomm's pipeline compiler does not keep vertex positions invariant across
|
||||
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
|
||||
// Invariant decoration), so a blended depth-writing prepass makes later
|
||||
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
|
||||
// clouds flicker black). Suppress blended depth writes there; the env variable
|
||||
// forces the quirk on ("0") or off ("1") on any driver.
|
||||
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||
Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm;
|
||||
if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) {
|
||||
if (env[0] == '0') {
|
||||
suppressBlendedDepthWrite = true;
|
||||
} else if (env[0] == '1') {
|
||||
suppressBlendedDepthWrite = false;
|
||||
}
|
||||
}
|
||||
if (suppressBlendedDepthWrite) {
|
||||
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
|
||||
"(driver lacks cross-pipeline position invariance)");
|
||||
}
|
||||
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
||||
}
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||
m_shaderDrawParametersFeatureEnabled);
|
||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||
|
||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
||||
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
|
||||
m_physicalDevice.properties.limits.maxSamplerAnisotropy});
|
||||
succeeded = m_samplerManager->Initialize({m_device, &m_config});
|
||||
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
|
||||
succeeded = InitializeBlitResources();
|
||||
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
|
||||
@@ -3209,30 +3181,17 @@ void main() {
|
||||
colorAttachmentFormat = textureResource->format;
|
||||
}
|
||||
|
||||
// Blending on an attachment whose format lacks
|
||||
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT is invalid pipeline state
|
||||
// (blend support is optional for e.g. 32-bit float formats on some GPUs);
|
||||
// force-disable it instead of baking undefined behavior into the pipeline.
|
||||
static UnorderedMap<Int, Bool> formatBlendSupport;
|
||||
auto blendSupportIt = formatBlendSupport.find(static_cast<Int>(colorAttachmentFormat));
|
||||
if (blendSupportIt == formatBlendSupport.end()) {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat,
|
||||
&formatProperties);
|
||||
const Bool blendable =
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0;
|
||||
blendSupportIt =
|
||||
formatBlendSupport.emplace(static_cast<Int>(colorAttachmentFormat), blendable).first;
|
||||
if (!blendable) {
|
||||
MGLOG_E("GetOrCreatePipeline: format=%d lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; "
|
||||
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
|
||||
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
if (!blendSupportIt->second) {
|
||||
effectiveBlendEnabled = false;
|
||||
}
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat, &formatProperties);
|
||||
MOBILEGL_ASSERT(
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0,
|
||||
"GetOrCreatePipeline: blend is enabled on color attachment %u for format=%d textureId=%d, but the format lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT (program=%u)",
|
||||
i,
|
||||
static_cast<Int>(colorAttachmentFormat),
|
||||
textureExternalIndex,
|
||||
program.GetExternalIndex());
|
||||
#endif
|
||||
}
|
||||
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
||||
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected at
|
||||
@@ -3625,73 +3584,8 @@ void main() {
|
||||
1, &memoryBarrier, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
VulkanRenderer::ScissoredClearPrep VulkanRenderer::PrepareScissoredClear(
|
||||
const MG_State::GLState::FramebufferObject& framebuffer, VkClearRect& outClearRect) {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
|
||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
if (renderPassEntry->attachmentCount == 0 ||
|
||||
renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
|
||||
return ScissoredClearPrep::NoOp;
|
||||
}
|
||||
|
||||
VkClearRect clearRect{};
|
||||
clearRect.rect = framebuffer.IsDefaultFramebuffer()
|
||||
? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),
|
||||
renderPassEntry->extent,
|
||||
m_swapchainObject.GetPreTransform())
|
||||
: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);
|
||||
clearRect.baseArrayLayer = 0;
|
||||
// GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer.
|
||||
clearRect.layerCount = renderPassEntry->layers;
|
||||
if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) {
|
||||
return ScissoredClearPrep::NoOp;
|
||||
}
|
||||
// A scissor that covers the whole target is a whole-surface clear; the deferred loadOp
|
||||
// path is equivalent and cheaper (no render pass churn, loadOp=CLEAR on tilers).
|
||||
if (clearRect.rect.offset.x == 0 && clearRect.rect.offset.y == 0 &&
|
||||
clearRect.rect.extent.width == static_cast<Uint32>(renderPassEntry->extent.x()) &&
|
||||
clearRect.rect.extent.height == static_cast<Uint32>(renderPassEntry->extent.y())) {
|
||||
return ScissoredClearPrep::NotNeeded;
|
||||
}
|
||||
|
||||
if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
activeRenderPass = nullptr;
|
||||
// Re-resolve: ending the pass updates tracked attachment layouts, which feed the
|
||||
// entry's load ops and initial layouts.
|
||||
renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
}
|
||||
// A still-active pass is necessarily compatible here: the block above ended any
|
||||
// incompatible one and nothing since can change the active pass.
|
||||
if (activeRenderPass) {
|
||||
// Materialize any older whole-attachment clear before applying this
|
||||
// ordered, scissored clear.
|
||||
ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry);
|
||||
} else {
|
||||
const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry);
|
||||
MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__);
|
||||
if (!began) {
|
||||
return ScissoredClearPrep::NoOp;
|
||||
}
|
||||
}
|
||||
outClearRect = clearRect;
|
||||
return ScissoredClearPrep::Ready;
|
||||
}
|
||||
|
||||
void VulkanRenderer::Clear(GLbitfield mask) {
|
||||
m_clearManager->CollectGarbage();
|
||||
if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) {
|
||||
return;
|
||||
}
|
||||
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return;
|
||||
}
|
||||
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)");
|
||||
if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) {
|
||||
@@ -3704,360 +3598,72 @@ void main() {
|
||||
.depth = MG_State::pGLContext->GetClearDepth(),
|
||||
.stencil = MG_State::pGLContext->GetClearStencil()
|
||||
};
|
||||
|
||||
// A render-pass loadOp clear always covers the complete attachment, while
|
||||
// OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for
|
||||
// GuiItemAtlas: animated items clear only their atlas slot before being
|
||||
// redrawn. Queueing that clear as a loadOp erases every cached static item.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
VkClearRect clearRect{};
|
||||
switch (PrepareScissoredClear(*fbo, clearRect)) {
|
||||
case ScissoredClearPrep::NoOp:
|
||||
return;
|
||||
case ScissoredClearPrep::NotNeeded:
|
||||
break; // full-coverage scissor: the deferred whole-surface path below is equivalent
|
||||
case ScissoredClearPrep::Ready: {
|
||||
VkClearAttachment clearAttachments[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS + 1];
|
||||
Uint32 clearAttachmentCount = 0;
|
||||
|
||||
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
const auto& drawBuffers = fbo->GetDrawBuffers();
|
||||
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
|
||||
const auto attachmentType = drawBuffers[drawBufferIndex];
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const auto& attachment = fbo->GetAttachment(attachmentType);
|
||||
if (!attachment.IsComplete()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
continue;
|
||||
}
|
||||
if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) {
|
||||
MGLOG_W("DirectVulkan: scissored glClear with a partial color mask is not supported");
|
||||
continue;
|
||||
}
|
||||
|
||||
MG_State::GLState::ITextureObject* colorTexture = nullptr;
|
||||
if (attachment.IsTexture()) {
|
||||
colorTexture = attachment.GetTexture().get();
|
||||
}
|
||||
VkClearAttachment clearAttachment{};
|
||||
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
clearAttachment.colorAttachment = drawBufferIndex;
|
||||
clearAttachment.clearValue.color = {
|
||||
payload.color.x(), payload.color.y(), payload.color.z(),
|
||||
ResolveColorClearAlpha(colorTexture, payload.color.w())
|
||||
};
|
||||
clearAttachments[clearAttachmentCount++] = clearAttachment;
|
||||
}
|
||||
}
|
||||
|
||||
VkImageAspectFlags depthStencilAspects = 0;
|
||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {
|
||||
const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth);
|
||||
if (depthAttachment.IsComplete()) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
}
|
||||
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil);
|
||||
if (stencilAttachment.IsComplete()) {
|
||||
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask.
|
||||
// vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or
|
||||
// zero mask can be expressed; treat a partial mask like a partial color mask.
|
||||
const Uint32 stencilWriteMask =
|
||||
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
} else if (stencilWriteMask != 0) {
|
||||
MGLOG_W("DirectVulkan: scissored glClear with a partial stencil write mask is not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depthStencilAspects != 0) {
|
||||
VkClearAttachment clearAttachment{};
|
||||
clearAttachment.aspectMask = depthStencilAspects;
|
||||
clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil};
|
||||
clearAttachments[clearAttachmentCount++] = clearAttachment;
|
||||
}
|
||||
|
||||
if (clearAttachmentCount != 0) {
|
||||
vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer,
|
||||
clearAttachmentCount, clearAttachments,
|
||||
1, &clearRect);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GL 3.3 §4.2.3: glClear honors the write masks. Mirror the scissored path's
|
||||
// gating for the deferred path: drop fully-masked planes, warn on partial
|
||||
// masks vkCmdClear*/loadOp clears cannot express.
|
||||
GLbitfield deferredMask = mask;
|
||||
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {
|
||||
deferredMask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) != 0xFFu) {
|
||||
if (stencilWriteMask != 0) {
|
||||
MGLOG_W("DirectVulkan: deferred glClear with a partial stencil write mask is not supported");
|
||||
}
|
||||
deferredMask &= ~static_cast<GLbitfield>(GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
const auto& drawBuffers = fbo->GetDrawBuffers();
|
||||
Bool anyFullMask = false;
|
||||
Bool anyRestrictedMask = false;
|
||||
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
|
||||
if (drawBuffers[drawBufferIndex] == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
const Bool full = colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a();
|
||||
if (full) {
|
||||
anyFullMask = true;
|
||||
} else {
|
||||
anyRestrictedMask = true;
|
||||
if (colorMask.r() || colorMask.g() || colorMask.b() || colorMask.a()) {
|
||||
MGLOG_W("DirectVulkan: deferred glClear with a partial color mask is not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!anyFullMask) {
|
||||
deferredMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
||||
} else if (anyRestrictedMask) {
|
||||
// Mixed per-buffer masks: queue only the fully-writable texture targets
|
||||
// individually and drop the framebuffer-level color clear.
|
||||
for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) {
|
||||
const auto attachmentType = drawBuffers[drawBufferIndex];
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
continue;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
|
||||
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
|
||||
continue;
|
||||
}
|
||||
const auto& attachment = fbo->GetAttachment(attachmentType);
|
||||
if (attachment.IsRenderbuffer()) {
|
||||
m_renderPassManager->QueueRenderbufferClear(
|
||||
{.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment);
|
||||
} else if (attachment.IsTexture()) {
|
||||
m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color},
|
||||
attachment);
|
||||
}
|
||||
}
|
||||
deferredMask &= ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
if (deferredMask == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_clearManager->QueueClear(deferredMask, payload, *fbo);
|
||||
m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo);
|
||||
m_clearManager->QueueClear(mask, payload, *fbo);
|
||||
m_renderPassManager->QueueRenderbufferClear(mask, payload, *fbo);
|
||||
}
|
||||
|
||||
void VulkanRenderer::QueueClearBufferPayloadForFramebuffer(
|
||||
const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload) {
|
||||
m_clearManager->CollectGarbage();
|
||||
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
|
||||
return;
|
||||
}
|
||||
if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) {
|
||||
RecordUnsupportedFramebufferError(__func__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate (buffer, drawbuffer) up front so GL errors fire regardless of which clear
|
||||
// path is taken below.
|
||||
switch (buffer) {
|
||||
case GL_COLOR:
|
||||
if (drawbuffer < 0 ||
|
||||
drawbuffer >= static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case GL_DEPTH:
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case GL_STENCIL:
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case GL_DEPTH_STENCIL:
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target");
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
VkClearRect clearRect{};
|
||||
switch (PrepareScissoredClear(framebuffer, clearRect)) {
|
||||
case ScissoredClearPrep::NoOp:
|
||||
return;
|
||||
case ScissoredClearPrep::NotNeeded:
|
||||
break; // full-coverage scissor: the deferred whole-surface path below is equivalent
|
||||
case ScissoredClearPrep::Ready:
|
||||
RecordScissoredClearBuffer(framebuffer, buffer, drawbuffer, clearPayload, clearRect);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType,
|
||||
const ClearAttachmentPayload& payload) {
|
||||
if (attachmentType == FramebufferAttachmentType::None || payload.mask == 0) {
|
||||
auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType) {
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
return;
|
||||
}
|
||||
const auto& attachment = framebuffer.GetAttachment(attachmentType);
|
||||
if (attachment.IsRenderbuffer()) {
|
||||
m_renderPassManager->QueueRenderbufferClear(payload, attachment);
|
||||
m_renderPassManager->QueueRenderbufferClear(clearPayload, attachment);
|
||||
return;
|
||||
}
|
||||
if (!attachment.IsTexture()) {
|
||||
return;
|
||||
}
|
||||
m_clearManager->QueueClear(payload, attachment);
|
||||
};
|
||||
|
||||
// GL 3.3 §4.2.3: ClearBuffer* honors the write masks like Clear. Deferred
|
||||
// clears cannot express partial masks; warn and skip those.
|
||||
const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };
|
||||
const auto stencilClearAllowed = [&]() -> Bool {
|
||||
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
return true;
|
||||
}
|
||||
if (stencilWriteMask != 0) {
|
||||
MGLOG_W("DirectVulkan: deferred glClearBuffer with a partial stencil write mask is not supported");
|
||||
}
|
||||
return false;
|
||||
m_clearManager->QueueClear(clearPayload, attachment);
|
||||
};
|
||||
|
||||
switch (buffer) {
|
||||
case GL_COLOR: {
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
if (drawbuffer < 0 ||
|
||||
drawbuffer >= static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range");
|
||||
return;
|
||||
}
|
||||
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
|
||||
MGLOG_W("DirectVulkan: deferred glClearBuffer with a partial color mask is not supported");
|
||||
return;
|
||||
}
|
||||
queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer], clearPayload);
|
||||
queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer]);
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH:
|
||||
if (depthClearAllowed()) {
|
||||
queueAttachmentClear(FramebufferAttachmentType::Depth, clearPayload);
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
queueAttachmentClear(FramebufferAttachmentType::Depth);
|
||||
return;
|
||||
case GL_STENCIL:
|
||||
if (stencilClearAllowed()) {
|
||||
queueAttachmentClear(FramebufferAttachmentType::Stencil, clearPayload);
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
queueAttachmentClear(FramebufferAttachmentType::Stencil);
|
||||
return;
|
||||
case GL_DEPTH_STENCIL: {
|
||||
ClearAttachmentPayload allowedPayload = clearPayload;
|
||||
if (!depthClearAllowed()) {
|
||||
allowedPayload.mask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
if (!stencilClearAllowed()) {
|
||||
allowedPayload.mask &= ~static_cast<GLbitfield>(GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
if ((allowedPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
queueAttachmentClear(FramebufferAttachmentType::Depth, allowedPayload);
|
||||
}
|
||||
if ((allowedPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
queueAttachmentClear(FramebufferAttachmentType::Stencil, allowedPayload);
|
||||
case GL_DEPTH_STENCIL:
|
||||
if (drawbuffer != 0) {
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0");
|
||||
return;
|
||||
}
|
||||
queueAttachmentClear(FramebufferAttachmentType::Depth);
|
||||
queueAttachmentClear(FramebufferAttachmentType::Stencil);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload,
|
||||
const VkClearRect& clearRect) {
|
||||
VkClearAttachment clearAttachment{};
|
||||
|
||||
if (buffer == GL_COLOR) {
|
||||
const auto attachmentType = framebuffer.GetDrawBuffers()[drawbuffer];
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
return;
|
||||
}
|
||||
const auto& attachment = framebuffer.GetAttachment(attachmentType);
|
||||
if (!attachment.IsComplete()) {
|
||||
return;
|
||||
}
|
||||
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
|
||||
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
|
||||
return;
|
||||
}
|
||||
if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) {
|
||||
MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial color mask is not supported");
|
||||
return;
|
||||
}
|
||||
MG_State::GLState::ITextureObject* colorTexture = nullptr;
|
||||
if (attachment.IsTexture()) {
|
||||
colorTexture = attachment.GetTexture().get();
|
||||
}
|
||||
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
clearAttachment.colorAttachment = static_cast<Uint32>(drawbuffer);
|
||||
clearAttachment.clearValue.color = {
|
||||
clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(),
|
||||
ResolveColorClearAlpha(colorTexture, clearPayload.color.w())
|
||||
};
|
||||
} else {
|
||||
VkImageAspectFlags aspects = 0;
|
||||
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&
|
||||
framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) {
|
||||
aspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0 &&
|
||||
framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) {
|
||||
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear).
|
||||
const Uint32 stencilWriteMask =
|
||||
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
|
||||
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
|
||||
aspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
} else if (stencilWriteMask != 0) {
|
||||
MGLOG_W("DirectVulkan: scissored glClearBuffer with a partial stencil write mask is not supported");
|
||||
}
|
||||
}
|
||||
if (aspects == 0) {
|
||||
return;
|
||||
}
|
||||
clearAttachment.aspectMask = aspects;
|
||||
clearAttachment.clearValue.depthStencil = {clearPayload.depth, clearPayload.stencil};
|
||||
}
|
||||
|
||||
vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, 1, &clearAttachment, 1, &clearRect);
|
||||
}
|
||||
|
||||
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload) {
|
||||
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
|
||||
@@ -4070,8 +3676,7 @@ void main() {
|
||||
void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
ClearAttachmentPayload payload{};
|
||||
payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||
// Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022).
|
||||
payload.depth = std::clamp(depth, 0.0f, 1.0f);
|
||||
payload.depth = depth;
|
||||
payload.stencil = static_cast<Uint32>(stencil);
|
||||
QueueClearBufferPayload(buffer, drawbuffer, payload);
|
||||
}
|
||||
@@ -4088,7 +3693,7 @@ void main() {
|
||||
break;
|
||||
case GL_DEPTH:
|
||||
payload.mask = GL_DEPTH_BUFFER_BIT;
|
||||
payload.depth = std::clamp(value[0], 0.0f, 1.0f);
|
||||
payload.depth = value[0];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -4110,7 +3715,7 @@ void main() {
|
||||
break;
|
||||
case GL_DEPTH:
|
||||
payload.mask = GL_DEPTH_BUFFER_BIT;
|
||||
payload.depth = std::clamp(value[0], 0.0f, 1.0f);
|
||||
payload.depth = value[0];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -4126,8 +3731,7 @@ void main() {
|
||||
}
|
||||
ClearAttachmentPayload payload{};
|
||||
payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||
// Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022).
|
||||
payload.depth = std::clamp(depth, 0.0f, 1.0f);
|
||||
payload.depth = depth;
|
||||
payload.stencil = static_cast<Uint32>(stencil);
|
||||
QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload);
|
||||
}
|
||||
@@ -4497,21 +4101,6 @@ void main() {
|
||||
sourceTexture->GetExternalIndex());
|
||||
}
|
||||
|
||||
if (!drawIsDefaultFbo) {
|
||||
// A clear queued for the destination predates this blit in API order;
|
||||
// execute it now, or its deferred materialization would later stomp the
|
||||
// copied contents (MC 26.3 OIT clears cloud_depth, then blits the main
|
||||
// depth into it - the stale loadOp=CLEAR erased the copy).
|
||||
const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask);
|
||||
const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType);
|
||||
auto destTexture = destAttachment.GetTexture();
|
||||
MOBILEGL_ASSERT(destTexture != nullptr, "BlitFramebuffer: depth destination texture attachment is null");
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady,
|
||||
"BlitFramebuffer: failed to materialize pending clear for depth destination textureId=%d",
|
||||
destTexture->GetExternalIndex());
|
||||
}
|
||||
|
||||
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
|
||||
? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired)
|
||||
: *srcBinding.trackedLayout;
|
||||
@@ -4648,19 +4237,6 @@ void main() {
|
||||
sourceTexture->GetExternalIndex());
|
||||
}
|
||||
|
||||
if (!drawIsDefaultFbo) {
|
||||
// A clear queued for the destination predates this blit in API order; execute
|
||||
// it now, or its deferred materialization would later stomp the blitted color.
|
||||
const auto& destAttachment = drawFbo->GetAttachment(drawFbo->GetDrawBuffers()[0]);
|
||||
auto destTexture = destAttachment.GetTexture();
|
||||
if (destTexture != nullptr) {
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady,
|
||||
"BlitFramebuffer: failed to materialize pending clear for destination textureId=%d",
|
||||
destTexture->GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
VkImageLayout srcLayout = readIsDefaultFbo
|
||||
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
|
||||
: *srcBinding.trackedLayout;
|
||||
@@ -4855,15 +4431,6 @@ void main() {
|
||||
sourceTexture->GetExternalIndex());
|
||||
}
|
||||
|
||||
{
|
||||
// A clear queued for the destination predates this copy in API order;
|
||||
// execute it now so the deferred materialization cannot stomp the copy.
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destinationTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady,
|
||||
"CopyTexSubImage2D: failed to materialize pending clear for destination textureId=%d",
|
||||
destinationTexture->GetExternalIndex());
|
||||
}
|
||||
|
||||
const Bool srcUsesSwapchainDepth = readIsDefaultFbo && (srcBinding.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) == 0;
|
||||
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
|
||||
? (srcUsesSwapchainDepth
|
||||
@@ -6294,7 +5861,6 @@ void main() {
|
||||
void VulkanRenderer::Present() {
|
||||
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
||||
"Present, acquired image index out of range");
|
||||
m_renderPassManager->OnPresent();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (activeRenderPass)
|
||||
@@ -6691,10 +6257,6 @@ void main() {
|
||||
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
|
||||
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
|
||||
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
|
||||
// Backs GL_TEXTURE_MAX_ANISOTROPY_EXT; optional in Vulkan, so the sampler manager falls back
|
||||
// to isotropic filtering (and the extension goes unadvertised) when the device lacks it.
|
||||
deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy;
|
||||
m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE;
|
||||
|
||||
VkDeviceCreateInfo deviceCreateInfo{};
|
||||
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||
@@ -7190,8 +6752,7 @@ void main() {
|
||||
static_cast<Uint32>(activeRenderPass->extent.y())
|
||||
};
|
||||
clearRect.baseArrayLayer = 0;
|
||||
// Compatible entries share the framebuffer layer count; layered attachments clear every layer.
|
||||
clearRect.layerCount = compatibleRenderPassEntry.layers;
|
||||
clearRect.layerCount = 1;
|
||||
|
||||
for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) {
|
||||
if (!pending.hasInlinePayload && pending.key.texture == nullptr) {
|
||||
@@ -7212,9 +6773,7 @@ void main() {
|
||||
clearAttachment.clearValue.depthStencil = {1.0f, 0};
|
||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
// VkClearAttachment::colorAttachment indexes the subpass pColorAttachments (draw-buffer
|
||||
// slot space, with UNUSED holes), not the compacted attachment descriptions.
|
||||
clearAttachment.colorAttachment = pending.colorAttachmentSlot;
|
||||
clearAttachment.colorAttachment = pending.attachmentIndex;
|
||||
clearAttachment.clearValue.color = {
|
||||
clearPayload.color.x(),
|
||||
clearPayload.color.y(),
|
||||
|
||||
@@ -131,14 +131,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
|
||||
const RenderPassEntry& compatibleRenderPassEntry);
|
||||
|
||||
enum class ScissoredClearPrep {
|
||||
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
|
||||
NoOp, // nothing to clear (degenerate target or empty scissor rect)
|
||||
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
|
||||
};
|
||||
ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
VkClearRect& outClearRect);
|
||||
|
||||
void Clear(GLbitfield mask);
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
@@ -227,9 +219,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// frontend. Timestamp support (queue timestampValidBits > 0 and a
|
||||
// non-zero timestampPeriod) is cached at device creation.
|
||||
Bool IsTimerQuerySupported() const;
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -286,10 +275,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload);
|
||||
void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload,
|
||||
const VkClearRect& clearRect);
|
||||
|
||||
// ---- Submission fence tracking (GL sync objects) ----
|
||||
// One record per vkQueueSubmit still in flight, in ascending submit
|
||||
@@ -362,7 +347,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool m_indexTypeUint8ExtensionEnabled = false;
|
||||
Bool m_logicOpFeatureEnabled = false;
|
||||
Bool m_multiDrawIndirectFeatureEnabled = false;
|
||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||
|
||||
@@ -17,4 +17,3 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
add_test(NAME BufferBench COMMAND BufferBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(BufferBench PROPERTIES LABELS benchmark)
|
||||
@@ -38,7 +38,6 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
@@ -17,4 +17,3 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
add_test(NAME ProgramBench COMMAND ProgramBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(ProgramBench PROPERTIES LABELS benchmark)
|
||||
@@ -441,10 +441,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = buffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
// GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently
|
||||
// ignored here, so probe with the non-recording query - the shared validator would
|
||||
// record INVALID_OPERATION, which is only correct on the bind path.
|
||||
if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue;
|
||||
if (!BufferImpl::ValidateBufferName(bufferName, true)) continue;
|
||||
MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
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(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
|
||||
@@ -60,87 +60,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the backend can actually attach this color format to a framebuffer. Preferred
|
||||
// source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus
|
||||
// probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend
|
||||
// cannot answer for fall back to a conservative static list of formats no ES driver renders to:
|
||||
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
|
||||
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
|
||||
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
|
||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
|
||||
const SizeT sentinelFormat = static_cast<SizeT>(TextureInternalFormat::RGBA8);
|
||||
Bool cachePopulated = false;
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount && !cachePopulated;
|
||||
++targetIndex) {
|
||||
cachePopulated = MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][sentinelFormat],
|
||||
MG_Backend::FormatCapability::Creatable);
|
||||
}
|
||||
if (cachePopulated) {
|
||||
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
|
||||
++targetIndex) {
|
||||
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable) ||
|
||||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
|
||||
MG_Backend::FormatCapability::FramebufferRenderable)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
case TextureInternalFormat::RGB16:
|
||||
case TextureInternalFormat::RGB10: // stored as RGB16
|
||||
case TextureInternalFormat::RGB12: // stored as RGB16
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Bool HasNonRenderableColorAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
|
||||
const auto& attachments = framebufferObject.GetAllAttachmentObjects();
|
||||
for (SizeT i = 0; i < attachments.size(); ++i) {
|
||||
const auto type = static_cast<FramebufferAttachmentType>(i);
|
||||
if (type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::Color31) {
|
||||
continue;
|
||||
}
|
||||
const auto& attachment = attachments[i];
|
||||
if (!attachment.IsValid()) continue;
|
||||
TextureInternalFormat format = TextureInternalFormat::Unknown;
|
||||
if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
format = attachment.GetTexture()->GetFormat();
|
||||
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
format = attachment.GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RecordUnsupportedFramebufferTextureAttachmentError(const char* functionName, const char* detail) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -637,62 +556,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return framebufferObject;
|
||||
}
|
||||
|
||||
// Attaches a single layer/slice of a 3D or array texture. The attachment model stores the layer
|
||||
// index; the DirectGLES backend attaches it with glFramebufferTextureLayer.
|
||||
static void AttachFramebufferTextureLayer(const char* functionName, GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level, GLint layer,
|
||||
TextureUploadTarget textureUploadTarget) {
|
||||
if (target == GL_FRAMEBUFFER) {
|
||||
target = GL_DRAW_FRAMEBUFFER;
|
||||
}
|
||||
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
AttachFramebufferTextureLayer(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, layer,
|
||||
textureUploadTarget);
|
||||
AttachFramebufferTextureLayer(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, layer,
|
||||
textureUploadTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
const FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (layer < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Layer must be non-negative."));
|
||||
return;
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
}
|
||||
|
||||
void FramebufferTextureLayer_State(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) {
|
||||
if (texture == 0) {
|
||||
const TextureUploadTarget detachTarget = TextureUploadTarget::Texture2D;
|
||||
@@ -700,31 +563,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
|
||||
switch (textureObject->GetTarget()) {
|
||||
case TextureTarget::Texture3D:
|
||||
textureUploadTarget = TextureUploadTarget::Texture3D;
|
||||
break;
|
||||
case TextureTarget::Texture2DArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture2DArray;
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
|
||||
break;
|
||||
default:
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
|
||||
return;
|
||||
}
|
||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
|
||||
static_cast<void>(layer);
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__,
|
||||
"Layered framebuffer texture attachments are not represented by the current framebuffer attachment model.");
|
||||
}
|
||||
|
||||
void FramebufferTexture3D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level,
|
||||
@@ -736,15 +578,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (textarget != GL_TEXTURE_3D) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"FramebufferTexture3D requires GL_TEXTURE_3D."));
|
||||
return;
|
||||
}
|
||||
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, zoffset,
|
||||
TextureUploadTarget::Texture3D);
|
||||
static_cast<void>(zoffset);
|
||||
RecordUnsupportedFramebufferTextureAttachmentError(
|
||||
__func__,
|
||||
"3D framebuffer texture slice attachments are not represented by the current framebuffer attachment model.");
|
||||
}
|
||||
|
||||
void FramebufferTexture2D_State(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
|
||||
@@ -1177,7 +1014,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
|
||||
auto& fbo = bindingSlot.GetBoundObject();
|
||||
const bool isDefaultFBO = (fbo == FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
|
||||
const GLenum bufs[] = {buf};
|
||||
static GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(fbo, isDefaultFBO, 1, bufs, true);
|
||||
}
|
||||
}
|
||||
@@ -1364,9 +1201,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = renderbuffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
// GL 3.3 core 4.4.2: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateRenderbufferName(bufferName)) continue;
|
||||
if (!FramebufferImpl::ValidateRenderbufferName(bufferName)) continue;
|
||||
MG_State::pGLContext->MarkRenderbufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
@@ -1389,9 +1224,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
|
||||
Uint bufferName = framebuffers[i];
|
||||
if (bufferName == 0) continue;
|
||||
// GL 3.3 core 4.4.1: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateFramebufferName(bufferName)) continue;
|
||||
if (!FramebufferImpl::ValidateFramebufferName(bufferName)) continue;
|
||||
MG_State::pGLContext->MarkFramebufferObjectForDeletion(bufferName);
|
||||
}
|
||||
}
|
||||
@@ -1419,9 +1252,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
|
||||
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
|
||||
}
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
@@ -1447,9 +1277,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT :
|
||||
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
|
||||
}
|
||||
if (HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
}
|
||||
if (IsActiveBackendDirectVulkan() &&
|
||||
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
|
||||
return GL_FRAMEBUFFER_UNSUPPORTED;
|
||||
@@ -1799,8 +1626,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check framebuffer completeness (including formats the ES pipeline cannot attach)
|
||||
if (!framebufferObject->CheckCompleteness() || HasNonRenderableColorAttachment(*framebufferObject)) {
|
||||
// Check framebuffer completeness
|
||||
if (!framebufferObject->CheckCompleteness()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidFramebufferOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State", "Framebuffer is incomplete"));
|
||||
@@ -1852,40 +1679,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"No color buffer for color format"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL 3.3 section 4.3.1: GL_INVALID_OPERATION if format is an integer format and the read
|
||||
// buffer is not an integer format, or vice versa (GL CTS packed_pixels expects the error
|
||||
// for every *_INTEGER readback from a normalized attachment).
|
||||
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
|
||||
TextureInternalFormat attachmentFormat = TextureInternalFormat::Unknown;
|
||||
if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
|
||||
attachmentFormat = readAttachment.GetTexture()->GetFormat();
|
||||
} else if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
|
||||
attachmentFormat = readAttachment.GetRenderbuffer()->GetInternalFormat();
|
||||
}
|
||||
if (attachmentFormat != TextureInternalFormat::Unknown &&
|
||||
TextureImpl::IsIntegerColorInputFormat(textureInputFormat) !=
|
||||
TextureImpl::IsIntegerColorInternalFormat(attachmentFormat)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ReadPixels_State",
|
||||
"Integer-ness of format does not match the read buffer"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
|
||||
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
|
||||
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
|
||||
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Packed-type/format pairing (GL CTS packed_pixels: e.g. GL_RED with GL_UNSIGNED_SHORT_5_6_5 must
|
||||
// raise an error instead of reaching the backend). Shared with the TexImage/GetTexImage validators;
|
||||
// runs after the depth-stencil branch above so DEPTH_STENCIL with a wrong type keeps GL_INVALID_ENUM.
|
||||
if (!TextureImpl::ValidateClientFormatTypePairing(textureInputFormat, texturePixelDataType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check PBO state
|
||||
|
||||
@@ -76,13 +76,7 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferName(Uint index, Bool allowZero) {
|
||||
if (index == 0) {
|
||||
// Zero is never a GenRenderbuffers name, so it must not reach the name-table lookup
|
||||
// below: where it is allowed (glBindRenderbuffer / FramebufferRenderbuffer detach) it
|
||||
// means "unbind", and looking it up would record a bogus INVALID_OPERATION - GL CTS's
|
||||
// per-case state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) after every case.
|
||||
if (allowZero) return true;
|
||||
|
||||
if (index == 0 && !allowZero) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", "ValidateRenderbufferName",
|
||||
|
||||
@@ -529,13 +529,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.AliasedLineWidthRangeMax;
|
||||
return;
|
||||
}
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: {
|
||||
// EXT_texture_filter_anisotropic queries this as a float; the integer path below widens
|
||||
// from here, so this case is the authoritative one.
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.MaxTextureMaxAnisotropy;
|
||||
return;
|
||||
}
|
||||
case GL_ALIASED_POINT_SIZE_RANGE:
|
||||
case GL_POINT_SIZE_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
@@ -1734,11 +1727,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 1024 * 1024; // TODO
|
||||
return;
|
||||
case GL_CONTEXT_PROFILE_MASK:
|
||||
// Reports the requested context profile (EGL defaults 3.x contexts to core);
|
||||
// MOBILEGL_RELAXED_SEMANTICS loosens behavior without changing the identity.
|
||||
*params = MG_State::pEGLContext && MG_State::pEGLContext->IsCurrentContextOpenGLCompatibilityProfile()
|
||||
? GL_CONTEXT_COMPATIBILITY_PROFILE_BIT
|
||||
: GL_CONTEXT_CORE_PROFILE_BIT;
|
||||
*params = GL_CONTEXT_CORE_PROFILE_BIT;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
@@ -1905,13 +1894,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxTextureSize;
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
|
||||
// Never advertise more bindings than the state layer's indexed-binding array can track
|
||||
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
|
||||
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
|
||||
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
|
||||
// (36), so the clamp never under-advertises.
|
||||
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
|
||||
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
|
||||
*params = std::max(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings);
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BLOCK_SIZE:
|
||||
*params = dynamicParameters.MaxUniformBlockSize;
|
||||
@@ -1972,10 +1955,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
|
||||
break;
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
|
||||
@@ -339,9 +339,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program."));
|
||||
return;
|
||||
}
|
||||
// A shader flagged with glDeleteShader lives on while attached; this detach may
|
||||
// have been its last GL-visible attachment.
|
||||
MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader);
|
||||
}
|
||||
|
||||
void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size,
|
||||
@@ -738,12 +735,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = (char*)programObject->MapUBO();
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
|
||||
Memcpy(params, pUBO + offset, size);
|
||||
@@ -793,12 +784,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||
auto* ttype = programObject->GetUniformTType(location);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + size > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<T, GLfloat>) {
|
||||
if (ttype->isMatrix() && ttype->getMatrixCols() == 3) {
|
||||
@@ -915,31 +900,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, programObject.GetMaxUniformLocation());
|
||||
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
||||
const Uint offset = programObject.GetUniformOffset(location);
|
||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||
const SizeT uboSize = programObject.GetUBOSize();
|
||||
SizeT writeSize = ItemCount * sizeof(T);
|
||||
if (size < writeSize) {
|
||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
"bytes; clamping",
|
||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||
writeSize = size;
|
||||
}
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||
// Should not happen: linking gives every settable uniform backing
|
||||
// storage. Log and drop the write instead of faulting.
|
||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
"uboSize=%zu); dropping write",
|
||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||
writeSize, uboSize);
|
||||
return;
|
||||
}
|
||||
auto size = programObject.GetUniformSizesInBytes(location);
|
||||
auto offset = programObject.GetUniformOffset(location);
|
||||
MOBILEGL_ASSERT(size >= ItemCount * sizeof(T),
|
||||
"Uniform size mismatch, expected at least %zu bytes, got %zu bytes.", ItemCount * sizeof(T),
|
||||
size);
|
||||
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, offset + byteOffsetInsideUniform);
|
||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||
Memcpy((char*)programObject.MapUBO() + offset + byteOffsetInsideUniform, value, ItemCount * sizeof(T));
|
||||
programObject.MarkUBOContentDirty();
|
||||
} else {
|
||||
auto* ttype = programObject.GetUniformTType(location);
|
||||
@@ -972,11 +940,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
// GL 3.3 §2.11.4: values for elements beyond the end of the uniform
|
||||
// array are ignored. Never step onto a neighboring uniform's location.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + offset)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + offset, "the current program object");
|
||||
return;
|
||||
@@ -1001,10 +964,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + offset)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + offset,
|
||||
"program " + std::to_string(program));
|
||||
@@ -1133,10 +1092,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1169,10 +1124,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
// Handle padding in mat3 correctly!!
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1208,10 +1159,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
// For matrix uniforms, we handle each matrix individually
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
||||
return;
|
||||
@@ -1272,10 +1219,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1306,10 +1249,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1344,10 +1283,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
break;
|
||||
}
|
||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
||||
return;
|
||||
@@ -1406,7 +1341,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
|
||||
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -1430,7 +1364,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(program) + "."));
|
||||
return;
|
||||
}
|
||||
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
||||
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
|
||||
}
|
||||
|
||||
@@ -1502,13 +1435,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
||||
break;
|
||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
||||
// Member entries of an arrayed block are recorded against the first instance;
|
||||
// every instance of the array reports that shared member set (matches
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
|
||||
GLint uniformIndexCount = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
|
||||
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast<Int>(uniformBlockIndex)) {
|
||||
continue;
|
||||
}
|
||||
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
||||
|
||||
@@ -649,9 +649,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ClearDepth_State(GLclampd depth) {
|
||||
// GL 3.3 §4.2.3: the clear depth is clamped to [0,1] at specification time (Vulkan clear
|
||||
// values additionally require it: VUID-VkClearDepthStencilValue-depth-00022).
|
||||
MG_State::pGLContext->SetClearDepth(ClampUnitFloat(static_cast<Float>(depth)));
|
||||
MG_State::pGLContext->SetClearDepth(static_cast<Float>(depth));
|
||||
}
|
||||
|
||||
void ClearColor_State(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
|
||||
|
||||
@@ -233,16 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sampler == 0) {
|
||||
textureUnit.SetSamplerObject(nullptr);
|
||||
} else {
|
||||
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
|
||||
// name, which is why this cannot go through the shared SamplerImpl validator.
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return;
|
||||
}
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
|
||||
if (!doesSamplerObjectCreated) {
|
||||
MG_State::pGLContext->CreateSamplerObject(sampler);
|
||||
|
||||
@@ -294,16 +294,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
|
||||
}
|
||||
|
||||
// Array targets store their layer count in z; layers never participate in mip
|
||||
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
|
||||
Bool DepthParticipatesInMipmapping(TextureTarget target) {
|
||||
return target == TextureTarget::Texture3D;
|
||||
}
|
||||
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(
|
||||
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;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
@@ -312,12 +306,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) {
|
||||
return {
|
||||
std::max<Int>(baseTexelSize.x() >> 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(), 1),
|
||||
std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,10 +333,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize);
|
||||
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()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
@@ -422,10 +414,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"2D multisample textures must use depth 1."));
|
||||
return false;
|
||||
}
|
||||
// Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height
|
||||
// == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative
|
||||
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
|
||||
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
|
||||
if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"2D multisample array textures must have at least one layer."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
||||
if (samples > maxSamples) {
|
||||
@@ -576,14 +571,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_A: {
|
||||
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
|
||||
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
|
||||
if (swizzleValue == TextureSwizzleParam::Unknown) {
|
||||
// GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
|
||||
// [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
|
||||
return;
|
||||
}
|
||||
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
|
||||
break;
|
||||
}
|
||||
@@ -773,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) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
|
||||
}
|
||||
@@ -835,12 +805,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
|
||||
"Texture object here should always be an object with mipmap");
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
|
||||
return;
|
||||
}
|
||||
|
||||
const void* originalPixels = pixels;
|
||||
const auto& pixelUnpackBufferObject =
|
||||
@@ -873,14 +837,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;
|
||||
const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize;
|
||||
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
||||
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* srcData = static_cast<const Uint8*>(processedPixels);
|
||||
Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
|
||||
if (destData) {
|
||||
@@ -1097,7 +1053,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
switch (pname) {
|
||||
@@ -1130,12 +1086,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_A: {
|
||||
auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
|
||||
auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
|
||||
if (swizzleValue == TextureSwizzleParam::Unknown) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
|
||||
return;
|
||||
}
|
||||
textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
|
||||
break;
|
||||
}
|
||||
@@ -1186,7 +1136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
TextureParameterObject_State(textureObject, pname, param, __func__);
|
||||
@@ -1202,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromFloats(textureObject, params);
|
||||
break;
|
||||
@@ -1210,7 +1160,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
|
||||
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
|
||||
@@ -1233,7 +1183,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromInts(textureObject, params);
|
||||
break;
|
||||
@@ -1241,7 +1191,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
|
||||
return;
|
||||
@@ -1259,7 +1209,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromIntegerInts(textureObject, params);
|
||||
break;
|
||||
@@ -1267,7 +1217,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_SWIZZLE_RGBA: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
|
||||
static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
|
||||
@@ -1287,7 +1237,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
SetTextureBorderColorFromUnsignedInts(textureObject, params);
|
||||
break;
|
||||
@@ -1298,7 +1248,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
|
||||
Vec4<TextureSwizzleParam> swizzleParams;
|
||||
@@ -1349,8 +1299,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1392,8 +1340,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1462,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
|
||||
// indicated by type.
|
||||
// ======================= Processing ================================
|
||||
textureInternalFormat =
|
||||
MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
|
||||
@@ -1472,8 +1416,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1507,11 +1449,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
|
||||
// Allocate in TextureObject
|
||||
if (isProxy) {
|
||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||
} else {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
}
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
|
||||
if (!originalPixels) {
|
||||
MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
|
||||
@@ -1538,7 +1476,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
|
||||
free(processedPixels);
|
||||
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
|
||||
}
|
||||
|
||||
void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
|
||||
@@ -1593,8 +1530,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1699,8 +1634,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject =
|
||||
isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
|
||||
: bindingSlot.GetBoundObject();
|
||||
// Name 0 resolves to the target's default texture object - a real texture this call
|
||||
// (re)specifies like any other; the slot is never empty anymore.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
@@ -1756,12 +1689,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
|
||||
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
|
||||
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
|
||||
// nonzero name that is not an existing buffer object is an error. This is reachable on
|
||||
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
|
||||
// which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
|
||||
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
if (buffer != 0 && !bufferObject) {
|
||||
if (!bufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
@@ -1775,8 +1704,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
// Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
|
||||
// silent no-op; the slot is never empty now that every unit/target holds its default.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1797,9 +1724,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
GLboolean IsTexture_State(GLuint texture) {
|
||||
// ======================= Processing ================================
|
||||
// GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved
|
||||
// name is just GL_FALSE. Probing with the recording validator (as every other Is* entry
|
||||
// point already avoids doing) would leave a spurious INVALID_VALUE behind.
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return GL_FALSE;
|
||||
return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -2622,13 +2547,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ===================== Error Checking ==============================
|
||||
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
|
||||
|
||||
// GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that
|
||||
// glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
|
||||
// restores the unit/target slot to its initial state.
|
||||
// Name 0 unbinds the current target from the active texture unit.
|
||||
if (texture == 0) {
|
||||
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
|
||||
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
|
||||
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
|
||||
bindingSlot.Bind(nullptr);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
|
||||
return;
|
||||
}
|
||||
@@ -2640,15 +2563,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted -
|
||||
// is not a legal bind target in the core profile (no application-generated names), and the error
|
||||
// is INVALID_OPERATION, not INVALID_VALUE.
|
||||
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindTexture_State", "Invalid texture name"));
|
||||
return;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureName(texture)) return;
|
||||
|
||||
// ======================= Processing ================================
|
||||
Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture);
|
||||
@@ -2669,25 +2584,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ActiveTexture_State(GLenum texture) {
|
||||
// ===================== Error Checking ==============================
|
||||
// GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 +
|
||||
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
|
||||
// state reset iterates every advertised combined unit, so rejecting units the
|
||||
// implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
|
||||
// whole test batches. The backend already clamps its advertised value to the state
|
||||
// layer's MAX_TEXTURE_IMAGE_UNITS capacity.
|
||||
Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
maxCombinedUnits = std::min(
|
||||
maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
|
||||
}
|
||||
if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
|
||||
if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ActiveTexture_State",
|
||||
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, 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 {}.",
|
||||
maxCombinedUnits - 1, texture, texture - GL_TEXTURE0)));
|
||||
texture, texture - GL_TEXTURE0)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3047,13 +2951,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
|
||||
textureObject->SetInternalFormat(textureInternalFormat);
|
||||
// Array targets keep their layer count constant across levels; only true 3D
|
||||
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
|
||||
for (GLsizei level = 0; level < levels; ++level) {
|
||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||
const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth;
|
||||
const GLsizei levelDepth = std::max<GLsizei>(1, depth >> level);
|
||||
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
|
||||
levelDepth);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
@@ -3089,7 +2990,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
|
||||
}
|
||||
@@ -3104,7 +3004,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
|
||||
}
|
||||
@@ -3120,7 +3019,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
|
||||
auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
|
||||
|
||||
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
|
||||
}
|
||||
@@ -3276,10 +3174,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
if (texture == 0) {
|
||||
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
|
||||
// target's default texture object (the unit's initial state).
|
||||
for (auto& slot : textureUnit.GetAllBindingSlots()) {
|
||||
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
|
||||
slot.Bind(nullptr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,15 +175,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool IsIntegerColorInputFormat(TextureInputFormat format) {
|
||||
static Bool IsIntegerColorInputFormat(TextureInputFormat format) {
|
||||
return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger ||
|
||||
format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger ||
|
||||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger ||
|
||||
format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger ||
|
||||
format == TextureInputFormat::AlphaInteger;
|
||||
format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger;
|
||||
}
|
||||
|
||||
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
|
||||
static Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R8UI:
|
||||
@@ -237,15 +235,17 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
format == TextureInputFormat::StencilIndex;
|
||||
}
|
||||
|
||||
// Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks
|
||||
// (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels
|
||||
// (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and
|
||||
// integer formats reject floating-point types; violations raise GL_INVALID_OPERATION.
|
||||
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) {
|
||||
// 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", "ValidateClientFormatTypePairing", message));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
|
||||
message));
|
||||
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 true;
|
||||
}
|
||||
|
||||
// Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests
|
||||
// isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and
|
||||
// integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path.
|
||||
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type) {
|
||||
const auto recordInvalidOperation = [](const char* message) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput",
|
||||
message));
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!ValidateClientFormatTypePairing(format, type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
|
||||
if (format == TextureInputFormat::StencilIndex) {
|
||||
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
|
||||
@@ -360,19 +339,6 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller) {
|
||||
if (textureObject && textureObject->GetExternalIndex() == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"This operation is not allowed on the default texture (zero is "
|
||||
"bound to the target)."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
TextureTarget target) {
|
||||
if (!textureObject) return true; // should be created later
|
||||
|
||||
@@ -23,19 +23,11 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
|
||||
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
|
||||
Bool ValidateTextureBorderNumber(Int border);
|
||||
Bool IsIntegerColorInputFormat(TextureInputFormat format);
|
||||
Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat);
|
||||
Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type);
|
||||
Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format,
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
// error is generated if zero is bound to target", ARB_texture_storage).
|
||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller);
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
||||
|
||||
@@ -78,12 +78,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
static bool ValidateCurrentVertexAttribIndex(GLuint index, const char* funcName) {
|
||||
// GL 3.3 core 2.7: VertexAttrib* sets the current value of ANY generic attribute,
|
||||
// including index 0 - only an out-of-range index is an error (INVALID_VALUE).
|
||||
// "Attribute 0 is immutable" was legacy immediate-mode lore; rejecting it broke GL
|
||||
// CTS's per-case state reset, which writes vertexAttrib4f(0, 0,0,0,1) after every case.
|
||||
static_cast<void>(funcName);
|
||||
return VertexArrayImpl::ValidateVertexAttributeIndex(index);
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return false;
|
||||
if (index == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"Generic vertex attribute 0 current value cannot be modified."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryGetVertexAttribute(GLuint index, const MG_State::GLState::VertexAttribute** outAttr) {
|
||||
@@ -285,9 +288,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint vao = arrays[i];
|
||||
if (vao == 0) continue;
|
||||
|
||||
// GL 3.3 core 2.10: unknown names are silently ignored on delete; the shared bind-path
|
||||
// validator would record INVALID_OPERATION instead.
|
||||
if (!MG_State::pGLContext->ValidateVertexArrayName(vao)) continue;
|
||||
if (!VertexArrayImpl::ValidateVertexArrayName(vao)) continue;
|
||||
|
||||
if (MG_State::pGLContext->GetBoundVertexArray() &&
|
||||
MG_State::pGLContext->GetBoundVertexArray() == MG_State::pGLContext->GetVertexArrayObject(vao)) {
|
||||
|
||||
@@ -764,20 +764,6 @@ namespace MobileGL {
|
||||
ctx->MajorVersion > 3 || (ctx->MajorVersion == 3 && ctx->MinorVersion >= 1);
|
||||
}
|
||||
|
||||
Bool EGLContext::IsCurrentContextOpenGLCompatibilityProfile() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||
if (currentIt == m_threadCurrents.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* ctx = TryGetContext(currentIt->second.Context);
|
||||
// Affirmative check: true only when the host explicitly requested the
|
||||
// compatibility bit. Attrib-less contexts report false and thus read as core
|
||||
// for GL_CONTEXT_PROFILE_MASK (EGL defaults 3.x contexts to the core profile).
|
||||
return ctx && ctx->ClientAPI == EGL_OPENGL_API &&
|
||||
(ctx->OpenGLProfileMask & EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT);
|
||||
}
|
||||
|
||||
EGLint EGLContext::GetCurrentContextFlags() const {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
auto currentIt = m_threadCurrents.find(CurrentThreadKey());
|
||||
|
||||
@@ -59,7 +59,6 @@ namespace MobileGL {
|
||||
Bool ValidateContext(EGLContextHandle context) const;
|
||||
Bool ValidateContextOnDisplay(EGLDisplayHandle display, EGLContextHandle context) const;
|
||||
Bool IsCurrentContextOpenGLCoreProfile() const;
|
||||
Bool IsCurrentContextOpenGLCompatibilityProfile() const;
|
||||
EGLint GetCurrentContextFlags() const;
|
||||
|
||||
// Surface
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "Core.h"
|
||||
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
|
||||
#include "MG_State/EGLState/Core.h"
|
||||
#include <Config.h>
|
||||
|
||||
namespace MobileGL::MG_State {
|
||||
void Init() {
|
||||
@@ -18,11 +17,6 @@ namespace MobileGL::MG_State {
|
||||
pEGLContext = MakeUnique<EGLState::EGLContext>();
|
||||
}
|
||||
|
||||
Bool IsRelaxedSemanticsActive() {
|
||||
return MG_Config::Features.RelaxedSemantics ||
|
||||
!(pEGLContext && pEGLContext->IsCurrentContextOpenGLCoreProfile());
|
||||
}
|
||||
|
||||
namespace GLState {
|
||||
// Error
|
||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
@@ -232,16 +226,12 @@ namespace MobileGL::MG_State {
|
||||
return m_textureState.GetTextureObject(index);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
|
||||
return m_textureState.GetDefaultTextureObject(target);
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
|
||||
return m_textureState.CreateTextureObject(index, target);
|
||||
}
|
||||
|
||||
void GLContext::MarkTextureObjectForDeletion(Uint index) {
|
||||
m_textureState.MarkTextureObjectForDeletion(index, IsRelaxedSemanticsActive());
|
||||
m_textureState.MarkTextureObjectForDeletion(index);
|
||||
}
|
||||
|
||||
TextureUnit& GLContext::GetTextureUnitObject(Int unit) {
|
||||
@@ -289,10 +279,6 @@ namespace MobileGL::MG_State {
|
||||
return m_programState.MarkShaderObjectForDeletion(index);
|
||||
}
|
||||
|
||||
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
|
||||
return m_programState.ReleaseShaderNameIfOrphaned(index);
|
||||
}
|
||||
|
||||
Bool GLContext::ValidateProgramName(const Uint index) const {
|
||||
return m_programState.ValidateProgramObject(index);
|
||||
}
|
||||
|
||||
@@ -95,8 +95,6 @@ namespace MobileGL {
|
||||
// Texture
|
||||
void GenTextureNames(Uint number, Vector<Uint>& textures);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
@@ -108,7 +106,6 @@ namespace MobileGL {
|
||||
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
|
||||
// per-draw sampled-texture set.
|
||||
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
|
||||
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
|
||||
Bool ValidateTextureName(Uint index) const;
|
||||
Bool ValidateTextureObject(Uint index) const;
|
||||
Int GetActiveTextureUnit() const;
|
||||
@@ -119,9 +116,6 @@ namespace MobileGL {
|
||||
Uint CreateShader(ShaderStage stage);
|
||||
void MarkProgramForDeletion(Uint index);
|
||||
void MarkShaderForDeletion(Uint index);
|
||||
// Frees a deletion-flagged shader's name once it lost its last GL-visible
|
||||
// attachment (call after glDetachShader).
|
||||
void ReleaseShaderNameIfOrphaned(Uint index);
|
||||
Bool ValidateProgramName(Uint index) const;
|
||||
Bool ValidateShaderName(Uint index) const;
|
||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
||||
@@ -253,12 +247,5 @@ namespace MobileGL {
|
||||
} // namespace GLState
|
||||
|
||||
extern UniquePtr<GLState::GLContext> pGLContext;
|
||||
|
||||
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
||||
// current EGL context explicitly requested a core profile (core bit in
|
||||
// EGL_CONTEXT_OPENGL_PROFILE_MASK, or a >=3.1 version request without the compatibility
|
||||
// bit) and MOBILEGL_RELAXED_SEMANTICS is off; no current context, legacy version
|
||||
// requests, and the compatibility bit all relax.
|
||||
Bool IsRelaxedSemanticsActive();
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Error.h"
|
||||
#include <algorithm>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.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",
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
|
||||
info->toString().c_str());
|
||||
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
|
||||
// error flags, each is set only while currently unset — repeated errors of the same
|
||||
// code are discarded until glGetError reads the flag. Unbounded accumulation leaked
|
||||
// stale errors into later, unrelated glGetError checks (GL CTS deinit noise).
|
||||
const Bool alreadyPending = std::any_of(m_errors.begin(), m_errors.end(),
|
||||
[code](const auto& e) { return e->code == code; });
|
||||
if (!alreadyPending) {
|
||||
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
}
|
||||
m_errors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include "ProgramObject.h"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -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) {
|
||||
bool inLineComment = false;
|
||||
bool inBlockComment = false;
|
||||
@@ -360,18 +346,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
|
||||
// GL-style reflection naming (GL CTS uniform_block relies on all four):
|
||||
// - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec.
|
||||
// - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a",
|
||||
// "s[1].a", ...) following ARB_program_interface_query rules. Default-block
|
||||
// (loose) uniforms already expand per element without this option.
|
||||
// - AllBlockVariables: every member of an active named block is active even when
|
||||
// no shader statement reads it (ES 3.0/GL 3.3 named-block semantics).
|
||||
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
|
||||
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
|
||||
// every block to std140, so this covers all of them.
|
||||
if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
|
||||
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
|
||||
if (!m_program->buildReflection()) {
|
||||
m_linkStatus = false;
|
||||
m_infoLog = "Build reflection failed.";
|
||||
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
|
||||
@@ -386,7 +361,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (int i = 0; i < m_activeUniformCount; i++) {
|
||||
auto& uniform = m_program->getUniform(i);
|
||||
auto location = uniform.layoutLocation();
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
const Int locationSpan =
|
||||
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
|
||||
requiredUniformLocations += locationSpan;
|
||||
if (location != glslang::TQualifier::layoutLocationEnd) {
|
||||
m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1);
|
||||
@@ -425,7 +401,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_externalIndex, uniform.name.c_str());
|
||||
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) {
|
||||
m_uniformIndexInTProgram[location + element] = i;
|
||||
}
|
||||
@@ -442,8 +419,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
});
|
||||
for (auto index : unallocatedUniformIndex) {
|
||||
auto& uniform = m_program->getUniform(index);
|
||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||
Bool placed = false;
|
||||
const Int locationSpan =
|
||||
(uniform.getType() && uniform.getType()->isOpaque()) ? std::max(1, uniform.size) : 1;
|
||||
for (; locNeedle <= m_maxUniformLocation; locNeedle++) {
|
||||
bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation;
|
||||
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
|
||||
@@ -460,25 +437,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
"(index %d)",
|
||||
m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index);
|
||||
locNeedle += locationSpan;
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (!placed) {
|
||||
// Explicit-location uniforms can fragment the space so no contiguous
|
||||
// span is left; grow the table instead of leaving the uniform without
|
||||
// a location (which would make it unsettable via glUniform*).
|
||||
const SizeT base = m_uniformIndexInTProgram.size();
|
||||
m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd);
|
||||
m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
|
||||
m_maxUniformLocation = static_cast<Uint>(base + locationSpan - 1);
|
||||
for (Int element = 0; element < locationSpan; ++element) {
|
||||
m_uniformIndexInTProgram[base + element] = index;
|
||||
}
|
||||
m_uniformLocations[uniform.name] = static_cast<Uint>(base);
|
||||
MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu",
|
||||
m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1);
|
||||
locNeedle = base + locationSpan;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_activeUniformCount; i++) {
|
||||
@@ -494,17 +454,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reflection names an array "texs[0]" while the layout(binding = N) map from the IO
|
||||
// resolver is keyed by the declared name ("texs"); look up both spellings.
|
||||
auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
|
||||
if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 &&
|
||||
uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) {
|
||||
explicitBinding =
|
||||
m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3));
|
||||
}
|
||||
const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
|
||||
const int initialUnit =
|
||||
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 &&
|
||||
location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) {
|
||||
m_uniformSamplerOrImageUnitIndex[location + element] =
|
||||
@@ -669,11 +622,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_uniformSizesInBytes.clear();
|
||||
m_uniformOffsets.clear();
|
||||
m_globalUboScratch.clear();
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset);
|
||||
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0);
|
||||
m_uniformOffsets.resize(m_maxUniformLocation + 1);
|
||||
m_uniformSizesInBytes.resize(m_maxUniformLocation + 1);
|
||||
for (SizeT i = 0; i < m_generatedSpirv.size(); i++) {
|
||||
auto& spv = m_generatedSpirv[i];
|
||||
|
||||
@@ -703,95 +653,31 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
auto locationIt = m_uniformLocations.find(name);
|
||||
if (locationIt == m_uniformLocations.end()) {
|
||||
locationIt = m_uniformLocations.find(name + "[0]");
|
||||
}
|
||||
if (locationIt == m_uniformLocations.end()) {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
const Uint baseLocation = locationIt->second;
|
||||
if (!IsValidUniformLocation(static_cast<Int>(baseLocation))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Int uniformIndex = m_uniformIndexInTProgram[baseLocation];
|
||||
const GLint arraySize = GetActiveUniformArraySize(uniformIndex);
|
||||
SizeT memberSize = 0;
|
||||
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
|
||||
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
|
||||
memberSize = sizeIt->second;
|
||||
}
|
||||
Uint arrayStride = 0;
|
||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||
arrayStride = strideIt->second;
|
||||
}
|
||||
|
||||
// Array uniforms span one location per element (see DoReflection);
|
||||
// give each element its real byte offset inside the UBO.
|
||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||
for (GLint element = 0; element < elementCount; ++element) {
|
||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||
if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) {
|
||||
break;
|
||||
}
|
||||
m_uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
|
||||
m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u stride=%u size=%zu assigned "
|
||||
"to locations %u..%u",
|
||||
m_externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
|
||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
|
||||
m_uniformOffsets[m_uniformLocations[name]] = offset;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u assigned to location %u",
|
||||
m_externalIndex, name.c_str(), offset, m_uniformLocations[name]);
|
||||
} else {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), offset);
|
||||
}
|
||||
}
|
||||
for (const auto& [name, size] : meta.plainUniformMemberSizesInBytes) {
|
||||
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
|
||||
m_uniformSizesInBytes[m_uniformLocations[name]] = size;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u assigned to location %u",
|
||||
m_externalIndex, name.c_str(), size, m_uniformLocations[name]);
|
||||
} else {
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' size=%u but not found in "
|
||||
"m_uniformLocations",
|
||||
m_externalIndex, name.c_str(), size);
|
||||
}
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - finished parsing module %zu metadata",
|
||||
m_externalIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||
for (Uint location = 0; location <= m_maxUniformLocation; ++location) {
|
||||
if (m_uniformOffsets[location] != kInvalidUniformOffset) continue;
|
||||
if (!IsValidUniformLocation(static_cast<Int>(location))) continue;
|
||||
const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type != nullptr && type->isOpaque()) continue;
|
||||
if (uniform.index >= 0 && uniform.index < m_program->getNumUniformBlocks() &&
|
||||
std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(),
|
||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||
// Member of a named uniform block: not settable through glUniform*, so it
|
||||
// needs no global-UBO shadow storage.
|
||||
continue;
|
||||
}
|
||||
|
||||
// std140-style slot: the matrix upload paths write column vectors at
|
||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||
if (type != nullptr && type->isMatrix()) {
|
||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||
}
|
||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||
const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||
m_globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||
m_uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||
m_uniformSizesInBytes[location] = slotSize;
|
||||
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' location %u has no UBO backing in the "
|
||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||
m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramObject::WaitUntilGenerationCompleted() const {
|
||||
|
||||
@@ -18,13 +18,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
public:
|
||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
|
||||
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
|
||||
// defers the actual removal to the next link).
|
||||
Bool ShaderIsAttachedGLVisible(const SharedPtr<ShaderObject>& shader) const {
|
||||
const auto matches = [&shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); };
|
||||
if (std::none_of(m_shaders.begin(), m_shaders.end(), matches)) return false;
|
||||
return std::none_of(m_detachedShaders.begin(), m_detachedShaders.end(), matches);
|
||||
}
|
||||
bool AttachShader(const SharedPtr<ShaderObject>& shader);
|
||||
SizeT DetachShader(const SharedPtr<ShaderObject>& shader);
|
||||
SizeT RemoveShader(const SharedPtr<ShaderObject>& shader);
|
||||
@@ -50,51 +43,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
const auto it = m_uniformLocations.find(name);
|
||||
if (it != m_uniformLocations.end()) return (Int)it->second;
|
||||
|
||||
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
|
||||
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
|
||||
// to base + k because DoReflection reserves one location per array element.
|
||||
if (name.empty()) return -1;
|
||||
if (name.back() != ']') {
|
||||
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
|
||||
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
|
||||
return -1;
|
||||
}
|
||||
if (name.length() < 4) return -1;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
// Require at least one digit between the brackets.
|
||||
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return -1;
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0FFFFFFFu) return -1;
|
||||
}
|
||||
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
|
||||
if (baseIt == m_uniformLocations.end()) {
|
||||
// Legacy key without the "[0]" suffix (defensive; reflection normally
|
||||
// stores the suffixed form for arrays).
|
||||
baseIt = m_uniformLocations.find(name.substr(0, bracket));
|
||||
if (baseIt == m_uniformLocations.end()) return -1;
|
||||
}
|
||||
const Int base = (Int)baseIt->second;
|
||||
if (!IsValidUniformLocation(base)) return -1;
|
||||
const Int index = m_uniformIndexInTProgram[base];
|
||||
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
||||
// in-range elements.
|
||||
const glslang::TType* type = m_program->getUniform(index).getType();
|
||||
if (type == nullptr || !type->isArray()) return -1;
|
||||
if (static_cast<GLint>(element) >= GetActiveUniformArraySize(index)) return -1;
|
||||
const Int location = base + (Int)element;
|
||||
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
||||
return location;
|
||||
}
|
||||
|
||||
// True when both locations are element slots of the same uniform variable.
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
|
||||
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
|
||||
if (it == m_uniformLocations.end()) return -1;
|
||||
return (Int)it->second;
|
||||
}
|
||||
|
||||
Int GetActiveUniformIndex(const String& name) const {
|
||||
@@ -104,19 +54,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return uniformIndex;
|
||||
}
|
||||
|
||||
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
||||
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
|
||||
// robustness against non-suffixed reflection entries.
|
||||
if (!name.empty() && name.back() != ']') {
|
||||
const String suffixedName = name + "[0]";
|
||||
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
|
||||
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
|
||||
m_program->getUniform(suffixedIndex).name == suffixedName) {
|
||||
return suffixedIndex;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
||||
const String baseName = name.substr(0, name.length() - 3);
|
||||
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
|
||||
@@ -167,24 +104,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
|
||||
// block member; -1 for a default-block uniform (glslang yields arrayStride==0 there, so gate
|
||||
// on block membership for the spec-mandated -1). The stride itself is derived from the type
|
||||
// instead of glslang's reflected arrayStride: for an array nested inside a struct member,
|
||||
// glslang computes that field against the enclosing STRUCT's (unset) packing and reports a
|
||||
// tight std430-like stride (ivec2 a[7] -> 8), even though its own member offsets and the
|
||||
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
||||
// layout is always std140, where every array element stride rounds up to a vec4.
|
||||
// block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the
|
||||
// default-block case, so gate on block membership to return the spec-mandated -1.
|
||||
GLint GetActiveUniformArrayStride(Uint index) const {
|
||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
||||
if (uniform.index < 0) return -1;
|
||||
const glslang::TType* type = uniform.getType();
|
||||
if (type == nullptr || !type->isArray()) return 0;
|
||||
if (type->isMatrix()) {
|
||||
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
||||
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols();
|
||||
return GetActiveUniformMatrixStride(index) * vectors;
|
||||
}
|
||||
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
|
||||
return (uniform.index < 0) ? -1 : uniform.arrayStride;
|
||||
}
|
||||
|
||||
// GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The
|
||||
@@ -246,9 +170,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
||||
return uniform.name;
|
||||
}
|
||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||
Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; }
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
|
||||
@@ -371,11 +292,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
auto it = m_uniformBlockIndexByName.find(name);
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
|
||||
// a bare "Block" query resolves to the first instance per GL semantics.
|
||||
const String suffixedName = String(name) + "[0]";
|
||||
it = m_uniformBlockIndexByName.find(suffixedName);
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
return 0xFFFFFFFFu; // GL_INVALID_INDEX
|
||||
}
|
||||
Bool IsActiveUniformBlock(Uint index) const {
|
||||
@@ -384,11 +300,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsActiveUniformBlock(index)) return 0;
|
||||
// glslang reports the unpadded end offset of the last member, but a std140 block
|
||||
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
||||
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
||||
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
||||
return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u;
|
||||
return m_program->getUniformBlock((Int)index).size;
|
||||
}
|
||||
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
@@ -396,30 +308,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
return ubo.name;
|
||||
}
|
||||
|
||||
// Uniform entries that belong to an arrayed uniform block are reflected once, against
|
||||
// the first instance ("Block[0]"); per GL semantics every other instance shares that
|
||||
// member set. Maps any instance's block index to the index owning the member entries.
|
||||
Uint GetUniformBlockMemberOwnerIndex(Uint index) const {
|
||||
const String& name = GetUniformBlockName(index);
|
||||
if (name.empty() || name.back() != ']') return index;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos) return index;
|
||||
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
|
||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
||||
return index;
|
||||
}
|
||||
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: derived from the same active-uniform scan that
|
||||
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
|
||||
// (glslang's numMembers counts declared members, which diverges from the reflected
|
||||
// entry list for struct arrays and arrayed block instances).
|
||||
Int GetUniformBlockActiveUniformCount(Uint index) const {
|
||||
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
|
||||
Int count = 0;
|
||||
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
|
||||
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
|
||||
}
|
||||
return count;
|
||||
return m_program->getUniformBlock((Int)index).numMembers;
|
||||
}
|
||||
|
||||
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
||||
|
||||
@@ -29,18 +29,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
|
||||
auto& programObject = m_programObjects[program];
|
||||
if (programObject != nullptr) {
|
||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||
// that were flagged with glDeleteShader while still attached.
|
||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||
programObject->MarkAsDeleted();
|
||||
programObject.reset();
|
||||
m_programIndexGenerator.Delete(program);
|
||||
for (const auto& shader : attachedShaders) {
|
||||
const Uint shaderName = shader->GetExternalIndex();
|
||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||
ReleaseShaderNameIfOrphaned(shaderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,35 +66,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||
auto& shaderObject = m_shaderObjects[shader];
|
||||
if (shaderObject != nullptr) {
|
||||
// glDeleteShader on an attached shader only FLAGS it; the name stays valid (and
|
||||
// glShaderSource/glCompileShader keep working on it) until the shader is detached
|
||||
// from every program. The GL CTS compiles shaders through exactly this
|
||||
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
|
||||
shaderObject->MarkAsDeleted();
|
||||
ReleaseShaderNameIfOrphaned(shader);
|
||||
m_shaderObjects[shader]->MarkAsDeleted();
|
||||
m_shaderObjects[shader].reset();
|
||||
m_shaderIndexGenerator.Delete(shader);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const {
|
||||
for (const auto& programObject : m_programObjects) {
|
||||
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A program deleted while current vacates its table slot but stays alive as the
|
||||
// current program; its attachments still count.
|
||||
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject);
|
||||
}
|
||||
|
||||
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) {
|
||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||
auto& shaderObject = m_shaderObjects[shader];
|
||||
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
||||
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
|
||||
shaderObject.reset();
|
||||
m_shaderIndexGenerator.Delete(shader);
|
||||
}
|
||||
|
||||
Bool ProgramState::ValidateShaderObject(Uint shader) const {
|
||||
return CheckIndexAvail(shader, m_shaderObjects) && m_shaderObjects[shader] != nullptr;
|
||||
}
|
||||
|
||||
@@ -26,16 +26,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint CreateShader(ShaderStage stage);
|
||||
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
|
||||
void MarkShaderObjectForDeletion(Uint shader);
|
||||
// Frees a deletion-flagged shader's name once no program holds a GL-visible
|
||||
// attachment to it (the deferred half of glDeleteShader-while-attached).
|
||||
void ReleaseShaderNameIfOrphaned(Uint shader);
|
||||
Bool ValidateShaderObject(Uint shader) const;
|
||||
|
||||
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
|
||||
|
||||
private:
|
||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
||||
|
||||
template <typename T>
|
||||
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
|
||||
return idx < vec.size();
|
||||
|
||||
@@ -76,14 +76,6 @@ namespace MobileGL {
|
||||
BGRInteger,
|
||||
RGBAInteger,
|
||||
BGRAInteger,
|
||||
// Desktop-GL single-channel client formats (table 3.3): the data holds one component that
|
||||
// feeds the G, B or A channel; the remaining channels default to 0 (color) / 1 (alpha).
|
||||
Green,
|
||||
Blue,
|
||||
Alpha,
|
||||
GreenInteger,
|
||||
BlueInteger,
|
||||
AlphaInteger,
|
||||
StencilIndex,
|
||||
DepthComponent,
|
||||
DepthStencil,
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "TextureObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
@@ -53,20 +52,6 @@ namespace MobileGL {
|
||||
void TextureObjectBase::SetInternalFormat(TextureInternalFormat format) {
|
||||
if (format == m_internalFormat) return;
|
||||
|
||||
// A default texture (name 0) changes IsUndefinedDefaultTexture on the
|
||||
// Unknown<->defined transition, which changes per-draw sampled-set membership
|
||||
// without any bind happening; bump the bind generation so cached sampled sets
|
||||
// re-resolve instead of replaying the stale membership. The identity check
|
||||
// excludes the other externalIndex-0 objects (proxy textures, default-FBO
|
||||
// attachments) whose definedness never feeds sampled-set membership, so e.g.
|
||||
// proxy probes cannot churn the cache.
|
||||
if (m_externalIndex == 0 && pGLContext &&
|
||||
(m_internalFormat == TextureInternalFormat::Unknown) !=
|
||||
(format == TextureInternalFormat::Unknown) &&
|
||||
pGLContext->GetDefaultTextureObject(GetTarget()).get() == this) {
|
||||
pGLContext->BumpTextureBindGeneration();
|
||||
}
|
||||
|
||||
m_internalFormat = format;
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
|
||||
@@ -154,17 +154,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// The per-target default texture objects (name 0) sit permanently in every texture unit's
|
||||
// binding slots, so "nothing useful bound" is no longer a null slot. While a default texture
|
||||
// has never been given an image (its internal format is still Unknown) it can contribute
|
||||
// nothing to sampling; backends treat such a binding exactly like the old empty slot and
|
||||
// skip per-draw sync/bind work for it. Once an application defines an image on a default
|
||||
// texture it loses this shortcut and is synced like any other texture.
|
||||
inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) {
|
||||
return texture != nullptr && texture->GetExternalIndex() == 0 &&
|
||||
texture->GetFormat() == TextureInternalFormat::Unknown;
|
||||
}
|
||||
|
||||
class TextureObjectWithOneMipmap : public TextureObjectMipmap {
|
||||
public:
|
||||
TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
|
||||
|
||||
@@ -18,51 +18,9 @@
|
||||
#include "TextureObjectStubs.h"
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
return MakeShared<TextureObject1D>(index);
|
||||
case TextureTarget::TextureCubeMap:
|
||||
return MakeShared<TextureObject2DCube>(index);
|
||||
case TextureTarget::Texture2D:
|
||||
return MakeShared<TextureObject2D>(index);
|
||||
case TextureTarget::Texture3D:
|
||||
return MakeShared<TextureObject3D>(index);
|
||||
case TextureTarget::TextureBuffer:
|
||||
return MakeShared<TextureObjectBuffer>(index);
|
||||
|
||||
// These texture types are still stubbed:
|
||||
case TextureTarget::TextureRectangle:
|
||||
return MakeShared<TextureObjectRectangle>(index);
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
return MakeShared<TextureObject2DMultisample>(index);
|
||||
case TextureTarget::Texture1DArray:
|
||||
return MakeShared<TextureObject1DArray>(index);
|
||||
case TextureTarget::Texture2DArray:
|
||||
return MakeShared<TextureObject2DArray>(index);
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return MakeShared<TextureObjectCubeMapArray>(index);
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
return MakeShared<TextureObject2DMultisampleArray>(index);
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
TextureState::TextureState() : m_indexGenerator(1024, 1) {
|
||||
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
||||
// shared across all texture units, and it is the initial binding of every unit/target
|
||||
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
|
||||
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
|
||||
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
||||
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
|
||||
}
|
||||
for (int i = 0; i < MAX_TEXTURE_IMAGE_UNITS; ++i) {
|
||||
m_textureUnits[i] = TextureUnit();
|
||||
for (auto& bindingSlot : m_textureUnits[i].GetAllBindingSlots()) {
|
||||
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +33,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
return nullTextureObject;
|
||||
}
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
|
||||
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
|
||||
"GetDefaultTextureObject: invalid texture target %d", (int)target);
|
||||
return m_defaultTextureObjects[(int)target];
|
||||
}
|
||||
|
||||
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) {
|
||||
textures.resize(number);
|
||||
m_indexGenerator.Generate(number, textures.data());
|
||||
@@ -88,15 +40,52 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) {
|
||||
auto& textureObject = m_textureObjects[index];
|
||||
textureObject = MakeTextureObjectForTarget(index, target);
|
||||
if (!textureObject) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
textureObject = MakeShared<TextureObject1D>(index);
|
||||
break;
|
||||
case TextureTarget::TextureCubeMap:
|
||||
textureObject = MakeShared<TextureObject2DCube>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2D:
|
||||
textureObject = MakeShared<TextureObject2D>(index);
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
textureObject = MakeShared<TextureObject3D>(index);
|
||||
break;
|
||||
case TextureTarget::TextureBuffer:
|
||||
textureObject = MakeShared<TextureObjectBuffer>(index);
|
||||
break;
|
||||
|
||||
// These texture types are still stubbed:
|
||||
case TextureTarget::TextureRectangle:
|
||||
textureObject = MakeShared<TextureObjectRectangle>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
textureObject = MakeShared<TextureObject2DMultisample>(index);
|
||||
break;
|
||||
case TextureTarget::Texture1DArray:
|
||||
textureObject = MakeShared<TextureObject1DArray>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DArray:
|
||||
textureObject = MakeShared<TextureObject2DArray>(index);
|
||||
break;
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
textureObject = MakeShared<TextureObjectCubeMapArray>(index);
|
||||
break;
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
textureObject = MakeShared<TextureObject2DMultisampleArray>(index);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
|
||||
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
|
||||
return nullTextureObject;
|
||||
}
|
||||
|
||||
return textureObject;
|
||||
}
|
||||
|
||||
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
|
||||
void TextureState::MarkTextureObjectForDeletion(Uint index) {
|
||||
if (m_indexGenerator.IsValid(index)) {
|
||||
auto it = m_textureObjects.find(index);
|
||||
if (it != m_textureObjects.end()) {
|
||||
@@ -105,9 +94,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
|
||||
for (auto& bindingSlot : bindingSlots) {
|
||||
if (bindingSlot.GetBoundObject() == it->second) {
|
||||
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
|
||||
// target's default texture object, on every unit it was bound to.
|
||||
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
|
||||
bindingSlot.Bind(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,15 +110,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
BumpTextureBindGeneration();
|
||||
m_textureObjects.erase(index);
|
||||
m_indexGenerator.Delete(index);
|
||||
} else if (!keepUnboundReservation) {
|
||||
// GL 3.3 core 3.8.1 makes a deleted name unused again even when GenTextures only
|
||||
// reserved it and no bind ever instantiated an object (so a later bind of it must
|
||||
// fail), and the reservation has to return to the free list.
|
||||
m_indexGenerator.Delete(index);
|
||||
}
|
||||
// Relaxed semantics: legacy apps may delete a generated name before its first
|
||||
// bind, then bind and populate that same name. Keep such a reservation alive there;
|
||||
// a real texture object reaching deletion still releases its index above.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,18 +49,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
void GenerateNames(Uint number, Vector<Uint>& textures);
|
||||
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
|
||||
// zero names a real, per-target texture object shared by every texture unit; binding 0
|
||||
// binds it, and image/parameter calls on it must work like on any texture. It is not a
|
||||
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE
|
||||
// and by-name lookups keep failing for 0) and can never be deleted.
|
||||
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
|
||||
TextureUnit& GetUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
||||
Int GetActiveTextureUnit() const;
|
||||
void SetActiveTextureUnit(Int unit);
|
||||
void MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation);
|
||||
void MarkTextureObjectForDeletion(Uint index);
|
||||
Bool ValidateName(Uint index) const;
|
||||
Bool ValidateTextureObject(Uint index) const;
|
||||
|
||||
@@ -89,8 +83,5 @@ namespace MobileGL::MG_State::GLState {
|
||||
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
|
||||
IndexGenerator<Uint> m_indexGenerator;
|
||||
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
|
||||
// One default texture object (external name 0) per target, created with the context and
|
||||
// immortal for its lifetime; the initial binding of every unit/target slot.
|
||||
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
|
||||
};
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -40,16 +40,10 @@ if (APPLE)
|
||||
target_link_libraries(DirectVulkanSanityTest PRIVATE objc)
|
||||
target_link_libraries(DirectVulkanSanityTest PRIVATE ${QUARTZCORE_FRAMEWORK})
|
||||
endif()
|
||||
if (MSVC)
|
||||
# The GLES headers declare gl* as dllimport on Windows, so objects like GetProcAddress.cpp
|
||||
# reference __imp_gl*. Those resolve against the in-library GL entry-point definitions only
|
||||
# if their objects are part of the link; pull the whole static library like the DLL does.
|
||||
target_link_options(DirectVulkanSanityTest PRIVATE /WHOLEARCHIVE:MobileGL_s)
|
||||
endif()
|
||||
target_compile_definitions(DirectVulkanSanityTest PRIVATE -DNOMINMAX)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration)
|
||||
gtest_discover_tests(DirectVulkanSanityTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
add_executable(
|
||||
DirectVulkanTestExec
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
// ProbeIndirectInstanceIdIncludesBaseInstance is driven against a fake GLES driver:
|
||||
@@ -35,11 +31,6 @@ namespace {
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
std::vector<std::string> extensions;
|
||||
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT the fake reports, and whether it was ever asked:
|
||||
// querying it on a driver without the extension would raise GL_INVALID_ENUM.
|
||||
GLfloat maxTextureMaxAnisotropy = 16.0f;
|
||||
bool maxTextureMaxAnisotropyQueried = false;
|
||||
|
||||
GLuint nextBufferId = 1;
|
||||
GLuint nextShaderId = 1;
|
||||
GLuint nextProgramId = 1;
|
||||
@@ -131,10 +122,6 @@ namespace {
|
||||
};
|
||||
funcs.glGetFloatv = [](GLenum pname, GLfloat* data) {
|
||||
switch (pname) {
|
||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
g_fake.maxTextureMaxAnisotropyQueried = true;
|
||||
data[0] = g_fake.maxTextureMaxAnisotropy;
|
||||
break;
|
||||
// Two-component range queries.
|
||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
@@ -417,51 +404,6 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
|
||||
ExpectProbeReleasedAllObjects();
|
||||
}
|
||||
|
||||
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
|
||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities absentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(absentCaps, funcs));
|
||||
// Never probed (it would be GL_INVALID_ENUM), and reported as "no anisotropy".
|
||||
EXPECT_FALSE(g_fake.maxTextureMaxAnisotropyQueried);
|
||||
EXPECT_FLOAT_EQ(absentCaps.MaxTextureMaxAnisotropy, 1.0f);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.maxTextureMaxAnisotropy = 16.0f;
|
||||
g_fake.extensions.emplace_back("GL_EXT_texture_filter_anisotropic");
|
||||
MobileGL::MG_External::GLESCapabilities presentCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(presentCaps, funcs));
|
||||
EXPECT_TRUE(g_fake.maxTextureMaxAnisotropyQueried);
|
||||
EXPECT_FLOAT_EQ(presentCaps.MaxTextureMaxAnisotropy, 16.0f);
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionPresenceIsDetectedExactly) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(BackendLoaderTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <Config.h>
|
||||
@@ -22,31 +20,9 @@ using namespace MobileGL;
|
||||
|
||||
class BufferTest : public ::testing::Test {
|
||||
protected:
|
||||
// GL error flags are sticky per error code and the context outlives an individual test in this
|
||||
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
|
||||
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
|
||||
static void DrainPendingGlErrors() {
|
||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
|
||||
// The call under test must raise exactly the expected error and nothing more: a second pending
|
||||
// error means one entry point queued several, which GetError() would hand out at an unrelated
|
||||
// call site later on.
|
||||
static void ExpectSingleGlError(GLenum expected) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
void TearDown() override {}
|
||||
};
|
||||
|
||||
TEST_F(BufferTest, Binding) {
|
||||
@@ -129,53 +105,6 @@ TEST_F(BufferTest, GenerateManyNames_NoPrematureCreation) {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 3.3 core 2.9 name lifecycle. The same three rules are asserted per object family (see the
|
||||
// texture/vertex-array/framebuffer/renderbuffer suites): a deleted or never-generated name is
|
||||
// INVALID_OPERATION to bind, deleting one is silent, and a generated-but-never-bound reservation
|
||||
// is still released so the name gets recycled.
|
||||
TEST_F(BufferTest, DeleteOfUnknownOrAlreadyDeletedBufferNameIsSilent) {
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
ASSERT_NE(buffer, 0u);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Double delete, name 0 and a never-generated name must all be ignored without an error.
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
|
||||
MG_Impl::GLImpl::DeleteBuffers(2, unknownNames);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, DeleteGeneratedButUnboundBufferNameReleasesReservationAndBindFails) {
|
||||
GLuint buffer = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
ASSERT_NE(buffer, 0u);
|
||||
ASSERT_TRUE(MG_State::pGLContext->ValidateBufferName(buffer));
|
||||
|
||||
MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateBufferName(buffer));
|
||||
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenBuffers(1, &recycled);
|
||||
EXPECT_EQ(recycled, buffer);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, BindNeverGeneratedBufferNameIsInvalidOperation) {
|
||||
// Not a small literal: other tests in this binary share the context and generate names in
|
||||
// bulk, so a low number may well be a legitimately reserved name here.
|
||||
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, std::numeric_limits<GLuint>::max());
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, AcquireMemory) {
|
||||
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||
Vector<Uint> bufferNames;
|
||||
|
||||
@@ -24,4 +24,4 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -62,7 +62,7 @@ set(LINK_LIBRARIES
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(SanityTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
add_subdirectory(BackendLoader)
|
||||
add_subdirectory(Buffer)
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(EGLStateTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -102,33 +102,3 @@ TEST(EGLStateMakeCurrent, SameThreadRepeatedAttachReleaseDoesNotLeaveStaleOwner)
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
|
||||
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
|
||||
}
|
||||
|
||||
// The compatibility-profile accessor is affirmative-only (it backs GL_CONTEXT_PROFILE_MASK
|
||||
// reporting): attrib-less contexts (profile mask 0) and released threads both answer "not
|
||||
// compat" and therefore read as core-profile contexts.
|
||||
TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
|
||||
auto fixture = CreateFixture();
|
||||
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, fixture->Context));
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
|
||||
const EGLint compatAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_MINOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||
EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
|
||||
EGL_NONE};
|
||||
const auto compatContext =
|
||||
fixture->State.CreateContext(fixture->Display, fixture->Config, EGL_NO_CONTEXT, compatAttribs);
|
||||
ASSERT_NE(compatContext, EGL_NO_CONTEXT);
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(fixture->Display, fixture->Surface, fixture->Surface, compatContext));
|
||||
EXPECT_TRUE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCoreProfile());
|
||||
|
||||
EXPECT_TRUE(fixture->State.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
|
||||
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
|
||||
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(FramebufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(FramebufferTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
@@ -35,8 +32,6 @@ namespace {
|
||||
Int g_clearNamedFramebufferfvCallCount = 0;
|
||||
Int g_clearNamedFramebufferfiCallCount = 0;
|
||||
Int g_readPixelsCallCount = 0;
|
||||
GLenum g_lastReadPixelsFormat = GL_NONE;
|
||||
GLenum g_lastReadPixelsType = GL_NONE;
|
||||
|
||||
void RecordBlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
@@ -71,39 +66,15 @@ namespace {
|
||||
++g_clearNamedFramebufferfiCallCount;
|
||||
}
|
||||
|
||||
void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum format, GLenum type, void*) {
|
||||
void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*) {
|
||||
++g_readPixelsCallCount;
|
||||
g_lastReadPixelsFormat = format;
|
||||
g_lastReadPixelsType = type;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class FramebufferTest : public ::testing::Test {
|
||||
protected:
|
||||
// GL error flags are sticky per error code and the context outlives an individual test in this
|
||||
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
|
||||
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
|
||||
static void DrainPendingGlErrors() {
|
||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
|
||||
// The call under test must raise exactly the expected error and nothing more: a second pending
|
||||
// error means one entry point queued several, which GetError() would hand out at an unrelated
|
||||
// call site later on.
|
||||
static void ExpectSingleGlError(GLenum expected) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
DrainPendingGlErrors();
|
||||
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
|
||||
ASSERT_NE(defaultFramebuffer, nullptr);
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(defaultFramebuffer);
|
||||
@@ -126,8 +97,6 @@ protected:
|
||||
g_clearNamedFramebufferfvCallCount = 0;
|
||||
g_clearNamedFramebufferfiCallCount = 0;
|
||||
g_readPixelsCallCount = 0;
|
||||
g_lastReadPixelsFormat = GL_NONE;
|
||||
g_lastReadPixelsType = GL_NONE;
|
||||
MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer = nullptr;
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv = nullptr;
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi = nullptr;
|
||||
@@ -146,83 +115,6 @@ TEST_F(FramebufferTest, CreateFramebuffersCreatesObjectsImmediately) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 3.3 core 4.4.1/4.4.2 name lifecycle - mirrors the rules asserted for the other object
|
||||
// families: deleting an unknown name is silent, a released reservation is recycled, and binding
|
||||
// a dead name is INVALID_OPERATION.
|
||||
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedFramebufferNameIsSilent) {
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
|
||||
ASSERT_NE(framebuffer, 0u);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Not a small literal: other tests in this binary share the context and generate names in
|
||||
// bulk, so a low number may well be a legitimately reserved name here.
|
||||
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
|
||||
MG_Impl::GLImpl::DeleteFramebuffers(2, unknownNames);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DeleteGeneratedButUnboundFramebufferNameReleasesReservationAndBindFails) {
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
|
||||
ASSERT_NE(framebuffer, 0u);
|
||||
ASSERT_TRUE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
|
||||
|
||||
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
|
||||
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenFramebuffers(1, &recycled);
|
||||
EXPECT_EQ(recycled, framebuffer);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedRenderbufferNameIsSilent) {
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
|
||||
ASSERT_NE(renderbuffer, 0u);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Not a small literal: other tests in this binary share the context and generate names in
|
||||
// bulk, so a low number may well be a legitimately reserved name here.
|
||||
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
|
||||
MG_Impl::GLImpl::DeleteRenderbuffers(2, unknownNames);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DeleteGeneratedButUnboundRenderbufferNameReleasesReservationAndBindFails) {
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
|
||||
ASSERT_NE(renderbuffer, 0u);
|
||||
ASSERT_TRUE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
|
||||
|
||||
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
|
||||
|
||||
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenRenderbuffers(1, &recycled);
|
||||
EXPECT_EQ(recycled, renderbuffer);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DefaultFramebufferIdentityTracksFramebufferNameZero) {
|
||||
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
|
||||
ASSERT_NE(defaultFramebuffer, nullptr);
|
||||
@@ -329,77 +221,6 @@ TEST_F(FramebufferTest, ReadPixelsAllowsPersistentMappedPixelPackBuffer) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, ReadPixelsRejectsMismatchedPackedTypeFormatPairs) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
|
||||
Uint8 pixelStorage[4 * 4 * 4] = {};
|
||||
|
||||
// Packed RGB type with a non-RGB format must never reach the backend (GL CTS packed_pixels
|
||||
// reads GL_RED with GL_UNSIGNED_SHORT_5_6_5 and expects an error).
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RED, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// Packed RGBA type with a non-RGBA/BGRA format is rejected as well.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGB, GL_UNSIGNED_INT_8_8_8_8, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// Packed depth-stencil type requires the DEPTH_STENCIL format.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_INT_24_8, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// A plain RGBA/UNSIGNED_BYTE readback keeps working.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, ReadPixelsForwardsSingleChannelDesktopClientFormats) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
|
||||
Uint8 pixelStorage[4 * 4 * 4] = {};
|
||||
|
||||
// Desktop GL treats GL_GREEN/GL_BLUE/GL_ALPHA as valid ReadPixels client formats (GL CTS
|
||||
// packed_pixels rgba8_format_green failed with GL_INVALID_ENUM before). The state layer must
|
||||
// validate them and forward the raw enum to the backend, which extracts the source channel
|
||||
// from a wide RGBA read.
|
||||
const GLenum singleChannelFormats[] = {GL_GREEN, GL_BLUE, GL_ALPHA};
|
||||
Int expectedCallCount = 0;
|
||||
for (const GLenum format : singleChannelFormats) {
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, format, GL_UNSIGNED_BYTE, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, ++expectedCallCount);
|
||||
EXPECT_EQ(g_lastReadPixelsFormat, format);
|
||||
EXPECT_EQ(g_lastReadPixelsType, static_cast<GLenum>(GL_UNSIGNED_BYTE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Packed-type pairing still applies: packed RGB/RGBA types never pair with single-channel formats.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// Integer client formats reject floating-point types.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN_INTEGER, GL_FLOAT, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, NamedRenderbufferStorageAndFramebufferAttachDoNotChangeBindings) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint renderbuffer = 0;
|
||||
@@ -598,263 +419,3 @@ TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) {
|
||||
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---- Packed-type readback encoding ------------------------------------------------------------------
|
||||
// Oracle-independent guard for the DirectGLES client-format readback conversion: feeds known wide RGBA
|
||||
// rows through ReadbackImpl::ConvertWideReadbackRow and asserts the exact packed words. Field positions
|
||||
// were hand-computed from GL 3.3 table 3.6 and match the GL CTS packed_pixels comparison functions
|
||||
// (glcPackedPixelsTests.cpp pack_UNSIGNED_*): non-REV types pack the first format component from the
|
||||
// most significant bit, *_REV types from the least significant bit.
|
||||
|
||||
namespace {
|
||||
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
|
||||
|
||||
// Converts a row of wide pixels (4 components of wideType each) into `format`/`type` words.
|
||||
template <typename WordT, typename SrcT>
|
||||
Vector<WordT> ConvertWideRowToPackedWords(const Vector<SrcT>& wide, GLenum wideType, GLenum format,
|
||||
GLenum type) {
|
||||
ReadbackImpl::ReadbackChannelMapping mapping{};
|
||||
EXPECT_TRUE(ReadbackImpl::GetReadbackChannelMapping(format, mapping));
|
||||
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(mapping, type), sizeof(WordT));
|
||||
const SizeT width = wide.size() / 4;
|
||||
Vector<WordT> out(width, static_cast<WordT>(0));
|
||||
ReadbackImpl::ConvertWideReadbackRow(reinterpret_cast<const Uint8*>(wide.data()),
|
||||
reinterpret_cast<Uint8*>(out.data()), width, wideType, mapping,
|
||||
type);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Normalized encodes read the wide row as RGBA8 (values are v / 255).
|
||||
template <typename WordT>
|
||||
Vector<WordT> ConvertRGBA8Row(const Vector<Uint8>& rgba, GLenum format, GLenum type) {
|
||||
return ConvertWideRowToPackedWords<WordT>(rgba, GL_UNSIGNED_BYTE, format, type);
|
||||
}
|
||||
|
||||
// Wide RGBA8 pattern shared by the normalized-encode tests. Expected fields below are
|
||||
// round(v / 255 * (2^bits - 1)), computed by hand per pixel.
|
||||
// R G B A
|
||||
const Vector<Uint8> kRGBA8Row{255, 0, 128, 64, // P0
|
||||
10, 250, 33, 200, // P1
|
||||
85, 170, 255, 0}; // P2
|
||||
} // namespace
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565) {
|
||||
// P0: R=31 G=0 B=round(128*31/255)=16 -> 31<<11 | 0<<5 | 16 = 0xF810
|
||||
// P1: R=round(10*31/255)=1 G=round(250*63/255)=62 B=round(33*31/255)=4 -> 1<<11|62<<5|4 = 0x0FC4
|
||||
// P2: R=round(85*31/255)=10 G=round(170*63/255)=42 B=31 -> 10<<11|42<<5|31 = 0x555F
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5);
|
||||
EXPECT_EQ(words[0], 0xF810u);
|
||||
EXPECT_EQ(words[1], 0x0FC4u);
|
||||
EXPECT_EQ(words[2], 0x555Fu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565Rev) {
|
||||
// REV packs R from the LSB: P2 -> 10 | 42<<5 | 31<<11 = 0xFD4A
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV);
|
||||
EXPECT_EQ(words[2], 0xFD4Au);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444) {
|
||||
// P0: R=15 G=0 B=round(128*15/255)=8 A=round(64*15/255)=4 -> 0xF084
|
||||
// P2: R=round(85*15/255)=5 G=round(170*15/255)=10 B=15 A=0 -> 0x5AF0
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4);
|
||||
EXPECT_EQ(words[0], 0xF084u);
|
||||
EXPECT_EQ(words[2], 0x5AF0u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444Rev) {
|
||||
// P0 fields R=15 G=0 B=8 A=4 packed from the LSB -> 15 | 0<<4 | 8<<8 | 4<<12 = 0x480F
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4_REV);
|
||||
EXPECT_EQ(words[0], 0x480Fu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort5551) {
|
||||
// P0: R=31 G=0 B=16 A=round(64/255)=0 -> 31<<11 | 16<<1 = 0xF820
|
||||
// P1: R=1 G=round(250*31/255)=30 B=4 A=round(200/255)=1 -> 1<<11|30<<6|4<<1|1 = 0x0F89
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1);
|
||||
EXPECT_EQ(words[0], 0xF820u);
|
||||
EXPECT_EQ(words[1], 0x0F89u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort1555Rev) {
|
||||
// P1 fields R=1 G=30 B=4 A=1 packed from the LSB -> 1 | 30<<5 | 4<<10 | 1<<15 = 0x93C1
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV);
|
||||
EXPECT_EQ(words[1], 0x93C1u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt2101010Rev) {
|
||||
// P0: R=1023 G=0 B=round(128*1023/255)=514 A=round(64*3/255)=1 -> 1023|514<<20|1<<30 = 0x602003FF
|
||||
// P2: R=round(85*1023/255)=341 G=round(170*1023/255)=682 B=1023 A=0 -> 0x3FFAA955
|
||||
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV);
|
||||
EXPECT_EQ(words[0], 0x602003FFu);
|
||||
EXPECT_EQ(words[2], 0x3FFAA955u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt1010102) {
|
||||
// P2 fields R=341 G=682 B=1023 A=0 packed from the MSB -> 341<<22 | 682<<12 | 1023<<2 = 0x556AAFFC
|
||||
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2);
|
||||
EXPECT_EQ(words[2], 0x556AAFFCu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte332) {
|
||||
// P0: R=7 G=0 B=round(128*3/255)=2 -> 7<<5 | 2 = 0xE2
|
||||
// P1: R=round(10*7/255)=0 G=round(250*7/255)=7 B=round(33*3/255)=0 -> 7<<2 = 0x1C
|
||||
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_3_3_2);
|
||||
EXPECT_EQ(words[0], 0xE2u);
|
||||
EXPECT_EQ(words[1], 0x1Cu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte233Rev) {
|
||||
// P0 fields R=7 G=0 B=2 packed from the LSB -> 7 | 0<<3 | 2<<6 = 0x87
|
||||
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_2_3_3_REV);
|
||||
EXPECT_EQ(words[0], 0x87u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, Encodes8888KeepsLegacyByteOrder) {
|
||||
// Regression for the previously supported types: P0 = (255, 0, 128, 64).
|
||||
const auto msbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8);
|
||||
EXPECT_EQ(msbFirst[0], 0xFF008040u);
|
||||
const auto lsbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV);
|
||||
EXPECT_EQ(lsbFirst[0], 0x408000FFu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesBGRAWithChannelMapping) {
|
||||
// BGRA's first format component is Blue: P0 fields B=8 G=0 R=15 A=4 -> 8<<12 | 15<<4 | 4 = 0x80F4
|
||||
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4);
|
||||
EXPECT_EQ(words[0], 0x80F4u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesIntegerRGBA2101010RevWithFieldClamp) {
|
||||
// Integer sources clamp to each field's unsigned range (10/10/10/2 bits).
|
||||
const Vector<Uint32> wide{1023u, 1024u, 5u, 4u};
|
||||
const auto words =
|
||||
ConvertWideRowToPackedWords<Uint32>(wide, GL_UNSIGNED_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
|
||||
EXPECT_EQ(words[0], 0xC05FFFFFu); // 1023 | 1023<<10 | 5<<20 | 3<<30
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesIntegerNegativeValuesClampToZero) {
|
||||
const Vector<Int32> wide{-5, 2, 100000, 1};
|
||||
const auto words =
|
||||
ConvertWideRowToPackedWords<Uint32>(wide, GL_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
|
||||
EXPECT_EQ(words[0], 0x7FF00800u); // 0 | 2<<10 | 1023<<20 | 1<<30
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesIntegerRGB565) {
|
||||
const Vector<Uint32> wide{31u, 64u, 2u, 0u};
|
||||
const auto words =
|
||||
ConvertWideRowToPackedWords<Uint16>(wide, GL_UNSIGNED_INT, GL_RGB_INTEGER, GL_UNSIGNED_SHORT_5_6_5);
|
||||
EXPECT_EQ(words[0], 0xFFE2u); // 31<<11 | 63<<5 | 2 (G clamps 64 -> 63)
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesPackedFloat10F11F11FRev) {
|
||||
// F11(1.0)=0x3C0 F11(0.5)=0x380 F10(0.25)=0x1A0 -> 0x3C0 | 0x380<<11 | 0x1A0<<22 = 0x681C03C0.
|
||||
// Second pixel: values above 65024 clamp to the max finite F11 (0x7BF), negatives go to zero.
|
||||
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f, 100000.0f, -1.0f, 0.25f, 1.0f};
|
||||
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV);
|
||||
EXPECT_EQ(words[0], 0x681C03C0u);
|
||||
EXPECT_EQ(words[1], 0x680007BFu);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) {
|
||||
// (1.0, 0.5, 0.25): shared exponent 16, fields 256/128/64 -> 256 | 128<<9 | 64<<18 | 16<<27
|
||||
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f};
|
||||
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV);
|
||||
EXPECT_EQ(words[0], 0x81010100u);
|
||||
}
|
||||
|
||||
TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) {
|
||||
ReadbackImpl::ReadbackChannelMapping rgba{};
|
||||
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba));
|
||||
ReadbackImpl::ReadbackChannelMapping rgbInteger{};
|
||||
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger));
|
||||
|
||||
// 3-field packed types never pair with 4-component formats and vice versa.
|
||||
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u);
|
||||
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_SHORT_4_4_4_4), 0u);
|
||||
// Packed-float RGB types never pair with integer formats.
|
||||
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u);
|
||||
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u);
|
||||
}
|
||||
|
||||
// ---- GL CTS packed_pixels readback root-cause regressions --------------------------------------
|
||||
|
||||
TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4);
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
|
||||
Uint8 pixelStorage[4 * 4 * 4] = {};
|
||||
|
||||
// GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION
|
||||
// (GL CTS packed_pixels expects the error for every mismatched combination).
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// The matching integer readback stays valid.
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// And the inverse mismatch: integer format on a normalized attachment.
|
||||
GLuint normalizedTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4);
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0);
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
|
||||
EXPECT_EQ(g_readPixelsCallCount, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) {
|
||||
// The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error;
|
||||
// name 0 used to be reported as an invalid renderbuffer name.
|
||||
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) {
|
||||
// glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky
|
||||
// GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D).
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2);
|
||||
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
|
||||
ASSERT_NE(framebufferObject, nullptr);
|
||||
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
|
||||
ASSERT_TRUE(attachment.IsTexture());
|
||||
EXPECT_EQ(attachment.GetTextureLayer(), 1);
|
||||
EXPECT_FALSE(attachment.IsLayered());
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) {
|
||||
// Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so
|
||||
// attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects
|
||||
// read errors instead of silent unwritten readbacks).
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4);
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
|
||||
EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER),
|
||||
static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
|
||||
|
||||
Uint8 pixelStorage[4 * 4 * 4] = {};
|
||||
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION);
|
||||
}
|
||||
|
||||
@@ -39,5 +39,5 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30)
|
||||
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -1845,440 +1845,3 @@ TEST_F(ProgramTest, GetActiveUniformsivErrors) {
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(params[0], -999);
|
||||
}
|
||||
|
||||
namespace {
|
||||
GLuint LinkVsFsProgram(const char* vsSource, const char* fsSource) {
|
||||
char infoLog[4096] = "";
|
||||
GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(vs, 1, &vsSource, nullptr);
|
||||
CompileShader(vs);
|
||||
GLint vsStatus = GL_FALSE;
|
||||
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
|
||||
GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog);
|
||||
EXPECT_EQ(vsStatus, GL_TRUE) << infoLog;
|
||||
|
||||
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &fsSource, nullptr);
|
||||
CompileShader(fs);
|
||||
GLint fsStatus = GL_FALSE;
|
||||
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
|
||||
GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog);
|
||||
EXPECT_EQ(fsStatus, GL_TRUE) << infoLog;
|
||||
|
||||
GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
|
||||
return program;
|
||||
}
|
||||
|
||||
const char* kPassthroughCoordsVs = R"(#version 330
|
||||
in vec4 a_position;
|
||||
in vec4 a_coords;
|
||||
out vec4 coords_in;
|
||||
void main() {
|
||||
gl_Position = a_position;
|
||||
coords_in = a_coords;
|
||||
})";
|
||||
} // namespace
|
||||
|
||||
// Repro for KHR-GL33.shaders.loops.do_while_dynamic_iterations.empty_body_* (and the
|
||||
// only_continue / unconditional_break variants): the loop is dead code, so the SPIR-V
|
||||
// optimizer eliminates it together with the only loads of `one` / `ui_one` -- and with
|
||||
// them the entire global UBO. The uniforms stay active in link reflection, so
|
||||
// glUniform1i on them must still have backing storage instead of memcpy-ing to null.
|
||||
TEST_F(ProgramTest, DoWhileDeadLoopUniformsKeepBackingStorage) {
|
||||
const char* loopBodies[] = {"", "continue;", "break;"};
|
||||
for (const char* body : loopBodies) {
|
||||
const String fsSource = String(R"(#version 330
|
||||
uniform int ui_one;
|
||||
uniform mediump int one;
|
||||
in vec4 coords_in;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 res = coords_in;
|
||||
mediump int i = 0;
|
||||
do {)") + body + R"(} while (i++ < one*ui_one);
|
||||
o_color = res;
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource.c_str());
|
||||
|
||||
const GLint locOne = GetUniformLocation(program, "one");
|
||||
const GLint locUiOne = GetUniformLocation(program, "ui_one");
|
||||
ASSERT_GE(locOne, 0) << "body: '" << body << "'";
|
||||
ASSERT_GE(locUiOne, 0) << "body: '" << body << "'";
|
||||
|
||||
UseProgram(program);
|
||||
Uniform1i(locOne, 1); // crashed with a null MapUBO() before the fallback storage
|
||||
Uniform1i(locUiOne, 2);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
|
||||
|
||||
GLint readback = -1;
|
||||
GetUniformiv(program, locOne, &readback);
|
||||
EXPECT_EQ(readback, 1) << "body: '" << body << "'";
|
||||
readback = -1;
|
||||
GetUniformiv(program, locUiOne, &readback);
|
||||
EXPECT_EQ(readback, 2) << "body: '" << body << "'";
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
|
||||
}
|
||||
}
|
||||
|
||||
// Repro for KHR-GL33.shaders.struct.uniform.*nested_struct_array_*: leaf uniforms of
|
||||
// nested struct arrays need (a) one location per array element and (b) real byte
|
||||
// offsets inside the global UBO. Before the fix every leaf had a single location and
|
||||
// offset 0, so glUniform2fv(loc, 2, ...) tripped the size assert on the neighboring
|
||||
// float uniform (and corrupted it in release builds).
|
||||
TEST_F(ProgramTest, NestedStructArrayUniformElementWrites) {
|
||||
// Struct shape from CTS glcShaderStructTests nested_struct_array (uniform case).
|
||||
const char* fsSource = R"(#version 330
|
||||
struct T {
|
||||
mediump float a;
|
||||
mediump vec2 b[2];
|
||||
};
|
||||
struct S {
|
||||
mediump float a;
|
||||
T b[3];
|
||||
int c;
|
||||
};
|
||||
uniform S s[2];
|
||||
in vec4 coords_in;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
mediump float r = (s[0].b[1].b[0].x + s[1].b[2].b[1].y) * s[0].b[0].a;
|
||||
mediump float g = s[1].b[0].b[0].y * s[0].b[2].a * s[1].b[2].a;
|
||||
mediump float b = (s[0].b[2].b[1].y + s[0].b[1].b[0].y + s[1].a) * s[0].b[1].a;
|
||||
mediump float a = float(s[0].c) + s[1].b[2].a - s[1].b[1].a;
|
||||
o_color = vec4(r, g, b, a);
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
UseProgram(program);
|
||||
|
||||
const GLint locVecArray = GetUniformLocation(program, "s[0].b[1].b");
|
||||
ASSERT_GE(locVecArray, 0);
|
||||
// Element locations are consecutive and reachable via the "[k]" suffix.
|
||||
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[0]"), locVecArray);
|
||||
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[1]"), locVecArray + 1);
|
||||
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[2]"), -1);
|
||||
|
||||
// Distinct scalar leaves must land at distinct UBO offsets (they all aliased
|
||||
// offset 0 before the fix).
|
||||
const char* scalarLeaves[] = {"s[0].b[0].a", "s[0].b[1].a", "s[0].b[2].a", "s[1].a", "s[1].b[1].a",
|
||||
"s[1].b[2].a"};
|
||||
const GLfloat scalarValues[] = {0.5f, 0.25f, 0.125f, 7.0f, 3.0f, 4.0f};
|
||||
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
|
||||
const GLint loc = GetUniformLocation(program, scalarLeaves[i]);
|
||||
ASSERT_GE(loc, 0) << scalarLeaves[i];
|
||||
Uniform1f(loc, scalarValues[i]);
|
||||
}
|
||||
|
||||
// CTS-style whole-array write: glUniform2fv with count = 2 on a vec2[2] leaf.
|
||||
// Before the fix this asserted/corrupted the next uniform ("s[0].b[2].a").
|
||||
const GLfloat vecData[4] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
Uniform2fv(locVecArray, 2, vecData);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
GLfloat vecReadback[2] = {};
|
||||
GetUniformfv(program, locVecArray, vecReadback);
|
||||
EXPECT_EQ(vecReadback[0], 1.0f);
|
||||
EXPECT_EQ(vecReadback[1], 2.0f);
|
||||
GetUniformfv(program, locVecArray + 1, vecReadback);
|
||||
EXPECT_EQ(vecReadback[0], 3.0f);
|
||||
EXPECT_EQ(vecReadback[1], 4.0f);
|
||||
|
||||
// All scalar leaves survived the array write intact.
|
||||
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
|
||||
GLfloat readback = -1.0f;
|
||||
GetUniformfv(program, GetUniformLocation(program, scalarLeaves[i]), &readback);
|
||||
EXPECT_EQ(readback, scalarValues[i]) << scalarLeaves[i];
|
||||
}
|
||||
|
||||
// std140: vec2 array elements inside the struct are 16 bytes apart, and the
|
||||
// per-element offsets differ.
|
||||
auto programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
ASSERT_NE(programObject, nullptr);
|
||||
const Uint offsetElement0 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray));
|
||||
const Uint offsetElement1 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray + 1));
|
||||
EXPECT_EQ(offsetElement1, offsetElement0 + 16u);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Plain top-level uniform arrays share the same per-element location machinery.
|
||||
TEST_F(ProgramTest, PlainArrayUniformElementLocationsAndWrites) {
|
||||
const char* fsSource = R"(#version 330
|
||||
uniform float arr[4];
|
||||
uniform float guard;
|
||||
in vec4 coords_in;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(arr[0] + arr[1], arr[2] + arr[3], guard, 1.0);
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
UseProgram(program);
|
||||
|
||||
const GLint locArr = GetUniformLocation(program, "arr");
|
||||
ASSERT_GE(locArr, 0);
|
||||
EXPECT_EQ(GetUniformLocation(program, "arr[0]"), locArr);
|
||||
EXPECT_EQ(GetUniformLocation(program, "arr[2]"), locArr + 2);
|
||||
EXPECT_EQ(GetUniformLocation(program, "arr[4]"), -1);
|
||||
|
||||
const GLint locGuard = GetUniformLocation(program, "guard");
|
||||
ASSERT_GE(locGuard, 0);
|
||||
EXPECT_EQ(GetUniformLocation(program, "guard[0]"), -1); // not an array
|
||||
|
||||
Uniform1f(locGuard, 9.0f);
|
||||
|
||||
const GLfloat values[4] = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
Uniform1fv(locArr, 4, values);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
GLfloat readback = -1.0f;
|
||||
GetUniformfv(program, locArr + i, &readback);
|
||||
EXPECT_EQ(readback, values[i]) << "arr[" << i << "]";
|
||||
}
|
||||
|
||||
// Overlong writes stop at the end of the array (GL 3.3 §2.11.4) instead of
|
||||
// spilling into the next uniform.
|
||||
const GLfloat tail[3] = {30.0f, 40.0f, 50.0f};
|
||||
Uniform1fv(GetUniformLocation(program, "arr[2]"), 3, tail);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
GLfloat readback = -1.0f;
|
||||
GetUniformfv(program, locArr + 2, &readback);
|
||||
EXPECT_EQ(readback, 30.0f);
|
||||
GetUniformfv(program, locArr + 3, &readback);
|
||||
EXPECT_EQ(readback, 40.0f);
|
||||
GetUniformfv(program, locGuard, &readback);
|
||||
EXPECT_EQ(readback, 9.0f); // untouched by the overlong write
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V
|
||||
// pipeline lays every uniform block out as std140; the frontend implements the
|
||||
// GL-visible consequences of that choice: packed/shared qualifiers compile (as
|
||||
// std140), reflection uses GL naming ("arr[0]", per-element struct arrays),
|
||||
// unused block members stay active, block sizes are vec4-padded, and array
|
||||
// strides are std140 even for arrays nested inside struct members.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProgramTest, UniformBlockPackedAndSharedLayoutsCompileAsStd140) {
|
||||
const char* fsSource = R"(#version 330
|
||||
layout(packed) uniform PackedBlock {
|
||||
vec4 pv;
|
||||
};
|
||||
layout(shared, row_major) uniform SharedBlock {
|
||||
float sf;
|
||||
mat4 sm;
|
||||
};
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = pv + vec4(sf) + vec4(sm[0][0]);
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
|
||||
// The blocks land on the implementation's chosen layout: std140 offsets.
|
||||
const GLuint pv = UniformIndexByName(program, "pv");
|
||||
const GLuint sf = UniformIndexByName(program, "sf");
|
||||
const GLuint sm = UniformIndexByName(program, "sm");
|
||||
ASSERT_NE(pv, GL_INVALID_INDEX);
|
||||
ASSERT_NE(sf, GL_INVALID_INDEX);
|
||||
ASSERT_NE(sm, GL_INVALID_INDEX);
|
||||
EXPECT_EQ(QueryUniformiv(program, pv, GL_UNIFORM_OFFSET), 0);
|
||||
EXPECT_EQ(QueryUniformiv(program, sf, GL_UNIFORM_OFFSET), 0);
|
||||
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_OFFSET), 16);
|
||||
// The remaining qualifiers in the rewritten layout() list survive.
|
||||
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_IS_ROW_MAJOR), 1);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, UniformBlockReflectsUnusedMembersWithGLNamesAndPaddedSize) {
|
||||
const char* fsSource = R"(#version 330
|
||||
layout(std140) uniform Blk {
|
||||
float used;
|
||||
vec4 unusedArr[3];
|
||||
ivec3 tail;
|
||||
};
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(used);
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
|
||||
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk");
|
||||
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||
|
||||
// All three members are active (unusedArr and tail are never read), the array is
|
||||
// reported under its GL name "unusedArr[0]", and both spellings resolve.
|
||||
const GLuint used = UniformIndexByName(program, "used");
|
||||
const GLuint unusedSuffixed = UniformIndexByName(program, "unusedArr[0]");
|
||||
const GLuint unusedBare = UniformIndexByName(program, "unusedArr");
|
||||
const GLuint tail = UniformIndexByName(program, "tail");
|
||||
ASSERT_NE(used, GL_INVALID_INDEX);
|
||||
ASSERT_NE(unusedSuffixed, GL_INVALID_INDEX);
|
||||
ASSERT_NE(tail, GL_INVALID_INDEX);
|
||||
EXPECT_EQ(unusedSuffixed, unusedBare);
|
||||
|
||||
char nameBuf[64] = "";
|
||||
GLsizei nameLen = 0;
|
||||
GLint arraySize = 0;
|
||||
GLenum type = 0;
|
||||
GetActiveUniform(program, unusedSuffixed, sizeof(nameBuf), &nameLen, &arraySize, &type, nameBuf);
|
||||
EXPECT_STREQ(nameBuf, "unusedArr[0]");
|
||||
EXPECT_EQ(arraySize, 3);
|
||||
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
||||
|
||||
// std140 layout of the unused members.
|
||||
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_OFFSET), 16);
|
||||
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_ARRAY_STRIDE), 16);
|
||||
EXPECT_EQ(QueryUniformiv(program, tail, GL_UNIFORM_OFFSET), 64);
|
||||
|
||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS agrees with the INDICES list and counts all members.
|
||||
GLint activeInBlock = 0;
|
||||
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &activeInBlock);
|
||||
ASSERT_EQ(activeInBlock, 3);
|
||||
GLint indices[3] = {-1, -1, -1};
|
||||
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices);
|
||||
for (GLint index : indices) {
|
||||
EXPECT_TRUE(index == static_cast<GLint>(used) || index == static_cast<GLint>(unusedSuffixed) ||
|
||||
index == static_cast<GLint>(tail));
|
||||
}
|
||||
|
||||
// The block ends with an ivec3 at offset 64 (unpadded end 76); the backend compiles
|
||||
// the std140 block at its vec4-padded size, and the reported size must cover it or
|
||||
// buffers sized from this query are too small to draw with.
|
||||
GLint dataSize = 0;
|
||||
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
|
||||
EXPECT_EQ(dataSize, 80);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, UniformBlockStructArrayExpandsPerElementWithStd140Strides) {
|
||||
const char* fsSource = R"(#version 330
|
||||
struct S {
|
||||
ivec2 v[2];
|
||||
float f;
|
||||
};
|
||||
layout(std140) uniform Blk2 {
|
||||
S s[2];
|
||||
} inst;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(inst.s[0].f);
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
|
||||
// ARB_program_interface_query naming: one entry per struct array element, prefixed
|
||||
// with the BLOCK name (not the instance name), basic arrays suffixed with "[0]".
|
||||
const GLuint v0 = UniformIndexByName(program, "Blk2.s[0].v[0]");
|
||||
const GLuint f0 = UniformIndexByName(program, "Blk2.s[0].f");
|
||||
const GLuint v1 = UniformIndexByName(program, "Blk2.s[1].v[0]");
|
||||
const GLuint f1 = UniformIndexByName(program, "Blk2.s[1].f");
|
||||
ASSERT_NE(v0, GL_INVALID_INDEX);
|
||||
ASSERT_NE(f0, GL_INVALID_INDEX);
|
||||
ASSERT_NE(v1, GL_INVALID_INDEX);
|
||||
ASSERT_NE(f1, GL_INVALID_INDEX);
|
||||
|
||||
// std140: ivec2 v[2] rounds each element up to a vec4 (stride 16, NOT the tight 8
|
||||
// glslang reflects for arrays nested inside a struct member); struct size rounds to
|
||||
// 48, giving s[1] members a 48-byte bias.
|
||||
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_OFFSET), 0);
|
||||
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_ARRAY_STRIDE), 16);
|
||||
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_SIZE), 2);
|
||||
EXPECT_EQ(QueryUniformiv(program, f0, GL_UNIFORM_OFFSET), 32);
|
||||
EXPECT_EQ(QueryUniformiv(program, v1, GL_UNIFORM_OFFSET), 48);
|
||||
EXPECT_EQ(QueryUniformiv(program, f1, GL_UNIFORM_OFFSET), 80);
|
||||
|
||||
GLint dataSize = 0;
|
||||
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk2");
|
||||
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
|
||||
EXPECT_EQ(dataSize, 96);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, UniformBlockInstanceArrayReportsPerInstanceBlocks) {
|
||||
const char* fsSource = R"(#version 330
|
||||
layout(std140) uniform ArrBlk {
|
||||
vec4 av;
|
||||
} insts[2];
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = insts[0].av + insts[1].av;
|
||||
})";
|
||||
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
|
||||
|
||||
const GLuint inst0 = GetUniformBlockIndex(program, "ArrBlk[0]");
|
||||
const GLuint inst1 = GetUniformBlockIndex(program, "ArrBlk[1]");
|
||||
ASSERT_NE(inst0, GL_INVALID_INDEX);
|
||||
ASSERT_NE(inst1, GL_INVALID_INDEX);
|
||||
EXPECT_NE(inst0, inst1);
|
||||
// A bare block name resolves to the first instance.
|
||||
EXPECT_EQ(GetUniformBlockIndex(program, "ArrBlk"), inst0);
|
||||
|
||||
// Every instance of the array shares the single reflected member set.
|
||||
GLint count0 = 0;
|
||||
GLint count1 = 0;
|
||||
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count0);
|
||||
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count1);
|
||||
EXPECT_EQ(count0, 1);
|
||||
EXPECT_EQ(count1, 1);
|
||||
GLint index0 = -1;
|
||||
GLint index1 = -1;
|
||||
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index0);
|
||||
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index1);
|
||||
EXPECT_EQ(index0, index1);
|
||||
EXPECT_EQ(static_cast<GLuint>(index0), UniformIndexByName(program, "ArrBlk.av"));
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) {
|
||||
// GL CTS compiles through exactly this sequence (create, attach, DELETE, source,
|
||||
// compile): glDeleteShader on an attached shader only flags it, and the name must
|
||||
// keep working until the last detach.
|
||||
const char* vsSource = R"(#version 330
|
||||
void main() { gl_Position = vec4(0.0); }
|
||||
)";
|
||||
const char* fsSource = R"(#version 330
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
|
||||
GLuint program = CreateProgram();
|
||||
GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
AttachShader(program, vs);
|
||||
DeleteShader(vs);
|
||||
EXPECT_EQ(IsShader(vs), GL_TRUE); // still alive: attached
|
||||
|
||||
ShaderSource(vs, 1, &vsSource, nullptr);
|
||||
CompileShader(vs);
|
||||
GLint status = GL_FALSE;
|
||||
GetShaderiv(vs, GL_COMPILE_STATUS, &status);
|
||||
EXPECT_EQ(status, GL_TRUE);
|
||||
status = GL_FALSE;
|
||||
GetShaderiv(vs, GL_DELETE_STATUS, &status);
|
||||
EXPECT_EQ(status, GL_TRUE);
|
||||
|
||||
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
AttachShader(program, fs);
|
||||
DeleteShader(fs);
|
||||
ShaderSource(fs, 1, &fsSource, nullptr);
|
||||
CompileShader(fs);
|
||||
|
||||
LinkProgram(program);
|
||||
GLint linkStatus = GL_FALSE;
|
||||
char infoLog[1024] = "";
|
||||
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
||||
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
|
||||
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
|
||||
|
||||
// The last GL-visible detach releases the flagged shader's name.
|
||||
DetachShader(program, vs);
|
||||
EXPECT_EQ(IsShader(vs), GL_FALSE);
|
||||
|
||||
// Deleting the program releases the other flagged shader.
|
||||
DeleteProgram(program);
|
||||
EXPECT_EQ(IsShader(fs), GL_FALSE);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -556,81 +556,6 @@ TEST_F(ProgramUtilTest, CompileSimpleVertexShader) {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy desktop sources are normalized to "#version 330 core", which is stricter than the 460 they
|
||||
// used to be forced to. A shader declaring 330 while using 420-era syntax without the matching
|
||||
// #extension line is accepted by real drivers, so CompileShader retries it at 460 instead of failing.
|
||||
TEST_F(ProgramUtilTest, CompileShaderRetriesAt460WhenLegacyVersionRejects420Syntax) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
String source = R"(#version 330
|
||||
layout(binding = 0) uniform sampler2D InSampler;
|
||||
in vec2 texCoord;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(InSampler, texCoord);
|
||||
})";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
// The normal path still emits 330 - the retry must not become the default.
|
||||
ASSERT_EQ(source.find("#version 330 core"), 0u);
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||
}
|
||||
|
||||
// Same source compiled for the OpenGL environment must take the retry too.
|
||||
ShaderAttrib glAttrib{
|
||||
.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source, .flags = ShaderCompileBits::CompileForOpenGL};
|
||||
auto glRes = ShaderCompiler::CompileShader(glAttrib);
|
||||
if (!glRes) {
|
||||
FAIL() << "errc: " << glRes.error().errc << "\nlog: " << glRes.error().log;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, CompileShaderStillFailsWithOriginalDiagnosticsWhenRetryCannotHelp) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
String source = R"(#version 330
|
||||
in vec2 texCoord;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = thisFunctionDoesNotExist(texCoord);
|
||||
})";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(res.error().errc, -2);
|
||||
EXPECT_NE(res.error().log.find("thisFunctionDoesNotExist"), String::npos) << res.error().log;
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, RetargetLegacyVersionDirectiveOnlyTouchesNormalizedDesktopCore) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String normalized = "#version 330 core\nvoid main() {}\n";
|
||||
EXPECT_TRUE(RetargetLegacyVersionDirectiveTo460(normalized));
|
||||
EXPECT_EQ(normalized.find("#version 460 core"), 0u);
|
||||
|
||||
// Already modern: nothing to retarget.
|
||||
String modern = "#version 460 core\nvoid main() {}\n";
|
||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(modern));
|
||||
EXPECT_EQ(modern.find("#version 460 core"), 0u);
|
||||
|
||||
// ES and compatibility sources keep what they declared.
|
||||
String es = "#version 300 es\nvoid main() {}\n";
|
||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(es));
|
||||
EXPECT_EQ(es.find("#version 300 es"), 0u);
|
||||
|
||||
String compat = "#version 330 compatibility\nvoid main() {}\n";
|
||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(compat));
|
||||
EXPECT_EQ(compat.find("#version 330 compatibility"), 0u);
|
||||
|
||||
// A commented-out directive is not the real one.
|
||||
String commented = "// #version 330 core\nvoid main() {}\n";
|
||||
EXPECT_FALSE(RetargetLegacyVersionDirectiveTo460(commented));
|
||||
EXPECT_EQ(commented.find("#version 460"), String::npos);
|
||||
}
|
||||
|
||||
const char* fs = R"(#version 150
|
||||
|
||||
uniform sampler2D InSampler;
|
||||
@@ -1329,57 +1254,3 @@ void main() {
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(binRes->at(0), optimized));
|
||||
}
|
||||
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessCoercesBlockPackingQualifiersToStd140) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// glslang rejects `packed`/`shared` outright when generating SPIR-V, and MobileGL's
|
||||
// UBO layout is always std140 anyway; the preprocessor rewrites the qualifiers so the
|
||||
// validation compile, reflection, and generated SPIR-V all agree on std140 (GL CTS
|
||||
// KHR-GL33.shaders.uniform_block.*.packed/shared).
|
||||
String source = R"(#version 330
|
||||
layout(packed) uniform PackedBlock { vec4 pv; };
|
||||
layout(shared, row_major) uniform SharedBlock { mat4 sm; };
|
||||
layout ( shared ) uniform SpacedBlock { float sx; };
|
||||
layout(std140) uniform KeptBlock { float kx; };
|
||||
// A non-layout use of the identifier stays untouched (compute storage qualifier).
|
||||
void main() {
|
||||
gl_Position = pv + vec4(sm[0][0]) + vec4(sx) + vec4(kx);
|
||||
})";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||
|
||||
EXPECT_EQ(source.find("packed"), String::npos);
|
||||
EXPECT_EQ(source.find("layout(shared"), String::npos);
|
||||
EXPECT_NE(source.find("layout(std140) uniform PackedBlock"), String::npos);
|
||||
EXPECT_NE(source.find("layout(std140, row_major) uniform SharedBlock"), String::npos);
|
||||
EXPECT_NE(source.find("layout ( std140 ) uniform SpacedBlock"), String::npos);
|
||||
EXPECT_NE(source.find("layout(std140) uniform KeptBlock"), String::npos);
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER,
|
||||
.sourceStr = source,
|
||||
.flags = ShaderCompileBits::CompileForOpenGL};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessLeavesComputeSharedStorageQualifierAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// `shared` is only a packing qualifier inside layout(...); the compute-shader storage
|
||||
// qualifier of the same spelling must survive.
|
||||
String source = R"(#version 430
|
||||
layout(local_size_x = 8) in;
|
||||
shared float sharedScratch[8];
|
||||
layout(shared) uniform Blk { float bx; };
|
||||
void main() {
|
||||
sharedScratch[gl_LocalInvocationIndex] = bx;
|
||||
})";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Compute, source);
|
||||
|
||||
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
|
||||
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
@@ -104,79 +103,6 @@ namespace {
|
||||
private:
|
||||
MobileGL::MG_External::GLESCapabilities m_snapshot;
|
||||
};
|
||||
|
||||
struct TextureBindCall {
|
||||
GLenum target;
|
||||
GLuint texture;
|
||||
};
|
||||
|
||||
MobileGL::Vector<TextureBindCall>* g_textureBindCalls = nullptr;
|
||||
GLuint g_nextBackendTextureId = 73;
|
||||
|
||||
void RecordTextureBind(GLenum target, GLuint texture) {
|
||||
if (g_textureBindCalls) {
|
||||
g_textureBindCalls->push_back({target, texture});
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateBackendTextures(GLsizei count, GLuint* textures) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
textures[i] = g_nextBackendTextureId++;
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteBackendTextures(GLsizei, const GLuint*) {}
|
||||
|
||||
GLenum NoBackendError() {
|
||||
return GL_NO_ERROR;
|
||||
}
|
||||
|
||||
// Isolates the DirectGLES globals touched by the binding-cache regression test. The test
|
||||
// installs only the native ES entry points needed to construct/bind a backend texture and
|
||||
// restores the process-wide state even when a gtest assertion unwinds the test body.
|
||||
struct ScopedDirectGLESTextureBindings {
|
||||
ScopedDirectGLESTextureBindings():
|
||||
previousContext(MobileGL::Move(MobileGL::MG_State::pGLContext)),
|
||||
previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs),
|
||||
previousActiveUnit(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit),
|
||||
previousCache(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache),
|
||||
previousRegistry(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) {
|
||||
MobileGL::MG_State::pGLContext = MobileGL::MakeUnique<MobileGL::MG_State::GLState::GLContext>();
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = 0;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = {};
|
||||
|
||||
MobileGL::MG_External::GLESFunctionsTable functions{};
|
||||
functions.glBindTexture = RecordTextureBind;
|
||||
functions.glDeleteTextures = DeleteBackendTextures;
|
||||
functions.glGenTextures = GenerateBackendTextures;
|
||||
functions.glGetError = NoBackendError;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||
g_textureBindCalls = &bindCalls;
|
||||
}
|
||||
|
||||
~ScopedDirectGLESTextureBindings() {
|
||||
g_textureBindCalls = nullptr;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = previousRegistry;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = previousActiveUnit;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = previousCache;
|
||||
MobileGL::MG_State::pGLContext = MobileGL::Move(previousContext);
|
||||
}
|
||||
|
||||
ScopedDirectGLESTextureBindings(const ScopedDirectGLESTextureBindings&) = delete;
|
||||
ScopedDirectGLESTextureBindings& operator=(const ScopedDirectGLESTextureBindings&) = delete;
|
||||
|
||||
MobileGL::Vector<TextureBindCall> bindCalls;
|
||||
|
||||
private:
|
||||
MobileGL::UniquePtr<MobileGL::MG_State::GLState::GLContext> previousContext;
|
||||
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||
MobileGL::Uint previousActiveUnit;
|
||||
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
|
||||
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) previousRegistry;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST(Sanity, BasicAssertions) {
|
||||
@@ -222,52 +148,6 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGL
|
||||
extensions.end());
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
|
||||
ScopedDirectGLESTextureBindings state;
|
||||
|
||||
GLuint frontendTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||
ASSERT_NE(frontendTexture, 0u);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, frontendTexture);
|
||||
const auto& frontendTextureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject();
|
||||
ASSERT_NE(frontendTextureObject, nullptr);
|
||||
ASSERT_EQ(frontendTextureObject->GetExternalIndex(), frontendTexture);
|
||||
|
||||
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(frontendTextureObject);
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u);
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||
|
||||
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
|
||||
// redundant rebind while the real 2D frontend object remains current.
|
||||
DirectGLES::BindCurrentTextures();
|
||||
EXPECT_EQ(state.bindCalls.size(), 1u);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(
|
||||
MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject()
|
||||
.get()));
|
||||
|
||||
DirectGLES::BindCurrentTextures();
|
||||
|
||||
ASSERT_EQ(state.bindCalls.size(), 2u);
|
||||
EXPECT_EQ(state.bindCalls[1].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[1].texture, 0u);
|
||||
EXPECT_EQ(DirectGLES::TextureImpl::g_boundTexturesCache[0][static_cast<SizeT>(TextureTarget::Texture2D)],
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
|
||||
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
const auto& funcs = backend.GetBackendFunctions().GL;
|
||||
|
||||
@@ -17,7 +17,7 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
add_executable(
|
||||
VkClearManagerTest
|
||||
@@ -35,4 +35,4 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
gtest_discover_tests(VkClearManagerTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(VkClearManagerTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,4 +17,4 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30)
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
@@ -37,31 +35,9 @@ protected:
|
||||
|
||||
return vbo;
|
||||
}
|
||||
// GL error flags are sticky per error code and the context outlives an individual test in this
|
||||
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
|
||||
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
|
||||
static void DrainPendingGlErrors() {
|
||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
|
||||
// The call under test must raise exactly the expected error and nothing more: a second pending
|
||||
// error means one entry point queued several, which GetError() would hand out at an unrelated
|
||||
// call site later on.
|
||||
static void ExpectSingleGlError(GLenum expected) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
||||
}
|
||||
void TearDown() override {}
|
||||
};
|
||||
|
||||
TEST_F(VertexArrayTest, GenerateAndBindVAO) {
|
||||
@@ -81,46 +57,6 @@ TEST_F(VertexArrayTest, GenerateAndBindVAO) {
|
||||
// Do not detect if it supports default VAO
|
||||
}
|
||||
|
||||
// GL 3.3 core 2.10 name lifecycle - the same three rules the other object families assert:
|
||||
// deleting an unknown name is silent, a released reservation is recycled, and binding a dead
|
||||
// name is INVALID_OPERATION.
|
||||
TEST_F(VertexArrayTest, DeleteOfUnknownOrAlreadyDeletedVertexArrayNameIsSilent) {
|
||||
GLuint vao = 0;
|
||||
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
|
||||
ASSERT_NE(vao, 0u);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Not a small literal: other tests in this binary share the context and generate names in
|
||||
// bulk, so a low number may well be a legitimately reserved name here.
|
||||
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
|
||||
MG_Impl::GLImpl::DeleteVertexArrays(2, unknownNames);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(VertexArrayTest, DeleteGeneratedButUnboundVertexArrayNameReleasesReservationAndBindFails) {
|
||||
GLuint vao = 0;
|
||||
MG_Impl::GLImpl::GenVertexArrays(1, &vao);
|
||||
ASSERT_NE(vao, 0u);
|
||||
ASSERT_TRUE(MG_State::pGLContext->ValidateVertexArrayName(vao));
|
||||
|
||||
MG_Impl::GLImpl::DeleteVertexArrays(1, &vao);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateVertexArrayName(vao));
|
||||
|
||||
MG_Impl::GLImpl::BindVertexArray(vao);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenVertexArrays(1, &recycled);
|
||||
EXPECT_EQ(recycled, vao);
|
||||
}
|
||||
|
||||
TEST_F(VertexArrayTest, VertexAttributeSetup) {
|
||||
Vector<Uint> vaoNames;
|
||||
MobileGL::MG_State::pGLContext->GenVertexArrayNames(1, vaoNames);
|
||||
@@ -328,9 +264,7 @@ TEST_F(VertexArrayTest, VertexBindingIndexIsBoundedByTheAdvertisedAttribLimit) {
|
||||
|
||||
const GLuint outOfRange = MG_Impl::GLImpl::VertexArrayImpl::GetMaxVertexAttribs();
|
||||
MG_Impl::GLImpl::VertexAttribBinding(0, outOfRange);
|
||||
// Asserting the exact code (rather than just "some error") also consumes it, so the next test
|
||||
// does not inherit it - GL error flags are sticky and this context is shared.
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
EXPECT_TRUE(MG_State::pGLContext->HasGLError());
|
||||
}
|
||||
|
||||
// The default attribute -> binding-point mapping is the identity. It used to be a 16-element literal
|
||||
@@ -1173,17 +1107,9 @@ TEST_F(GeneralVertexArrayTest, CurrentAttrib_PackedValidation) {
|
||||
GetVertexAttribfv(1, GL_CURRENT_VERTEX_ATTRIB, out);
|
||||
EXPECT_FLOAT_EQ(out[0], 42.0f); // unchanged by the failed call
|
||||
|
||||
// GL 3.3 core 2.7: attribute 0's current value is writable like any other generic
|
||||
// attribute - no error. (An earlier MobileGL policy rejected index 0 with
|
||||
// GL_INVALID_OPERATION, which broke GL CTS's per-case state reset: gluStateReset writes
|
||||
// vertexAttrib4f(0, 0,0,0,1) for every attribute after every case.)
|
||||
// Attribute 0 is rejected by MobileGL policy (GL_INVALID_OPERATION).
|
||||
VertexAttribP4ui(0, GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
GetVertexAttribfv(0, GL_CURRENT_VERTEX_ATTRIB, out);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_FLOAT_EQ(out[0], 1.0f); // x field of the packed word
|
||||
VertexAttrib4f(0, 0.0f, 0.0f, 0.0f, 1.0f); // restore the initial current value
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// Out-of-range index -> GL_INVALID_VALUE.
|
||||
VertexAttribP4ui(VertexArrayImpl::GetMaxVertexAttribs(), GL_UNSIGNED_INT_2_10_10_10_REV, GL_FALSE, 1u);
|
||||
|
||||
@@ -911,13 +911,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||
// Only legal to query once the extension has been seen in the loop above, hence not batched
|
||||
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
|
||||
if (caps.SupportsTextureFilterAnisotropy) {
|
||||
GLfloat maxTextureMaxAnisotropy = 1.0f;
|
||||
glesFuncs.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxTextureMaxAnisotropy);
|
||||
caps.MaxTextureMaxAnisotropy = std::max(maxTextureMaxAnisotropy, 1.0f);
|
||||
}
|
||||
caps.AliasedLineWidthRangeMin = aliasedLineWidthRange[0];
|
||||
caps.AliasedLineWidthRangeMax = aliasedLineWidthRange[1];
|
||||
caps.SmoothLineWidthRangeMin = smoothLineWidthRange[0];
|
||||
|
||||
@@ -1034,9 +1034,6 @@ namespace MobileGL {
|
||||
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
|
||||
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
|
||||
Bool SupportsTextureFilterAnisotropy = false;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT of the host driver; only queried when the
|
||||
// extension above is present, and left at 1.0 (no anisotropy) otherwise.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
Bool SupportsBaseInstance = false;
|
||||
// GL_EXT_disjoint_timer_query is present in the extension string.
|
||||
Bool SupportsDisjointTimerQuery = false;
|
||||
|
||||
@@ -124,7 +124,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.UniformBufferOffsetAlignment = static_cast<int>(p.limits.minUniformBufferOffsetAlignment);
|
||||
caps.AliasedLineWidthRangeMin = p.limits.lineWidthRange[0];
|
||||
caps.AliasedLineWidthRangeMax = p.limits.lineWidthRange[1];
|
||||
caps.MaxSamplerAnisotropy = p.limits.maxSamplerAnisotropy;
|
||||
caps.SmoothLineWidthRangeMin = p.limits.lineWidthRange[0];
|
||||
caps.SmoothLineWidthRangeMax = p.limits.lineWidthRange[1];
|
||||
caps.SmoothLineWidthGranularity = p.limits.lineWidthGranularity;
|
||||
@@ -209,7 +208,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.UniformBufferOffsetAlignment = static_cast<int>(properties.limits.minUniformBufferOffsetAlignment);
|
||||
caps.AliasedLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
||||
caps.AliasedLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
||||
caps.MaxSamplerAnisotropy = properties.limits.maxSamplerAnisotropy;
|
||||
caps.SmoothLineWidthRangeMin = properties.limits.lineWidthRange[0];
|
||||
caps.SmoothLineWidthRangeMax = properties.limits.lineWidthRange[1];
|
||||
caps.SmoothLineWidthGranularity = properties.limits.lineWidthGranularity;
|
||||
|
||||
@@ -18,9 +18,6 @@ namespace MobileGL {
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. Whether it can be used at all depends on
|
||||
// the samplerAnisotropy feature, which the renderer decides at device creation.
|
||||
Float MaxSamplerAnisotropy = 1.0f;
|
||||
Float SmoothLineWidthRangeMin = 1.0f;
|
||||
Float SmoothLineWidthRangeMax = 1.0f;
|
||||
Float SmoothLineWidthGranularity = 1.0f;
|
||||
|
||||
@@ -58,17 +58,9 @@ namespace MobileGL {
|
||||
|
||||
TextureInputFormat ConvertGLEnumToTextureInputFormat(GLenum format) {
|
||||
switch (format) {
|
||||
// Legacy carve-out: GL_ALPHA stays mapped to Red so alpha-texture uploads keep landing in
|
||||
// the R channel of the R8-backed storage (TexImage*_State pairs this with a 0,0,0,R
|
||||
// swizzle). Readback of GL_ALPHA is corrected at the backend, which maps the raw enum to
|
||||
// source channel 3 (DirectGLES GetReadbackChannelMapping).
|
||||
case GL_ALPHA:
|
||||
case GL_RED:
|
||||
return TextureInputFormat::Red;
|
||||
case GL_GREEN:
|
||||
return TextureInputFormat::Green;
|
||||
case GL_BLUE:
|
||||
return TextureInputFormat::Blue;
|
||||
case GL_RG:
|
||||
return TextureInputFormat::RG;
|
||||
case GL_RGB:
|
||||
@@ -81,12 +73,6 @@ namespace MobileGL {
|
||||
return TextureInputFormat::BGRA;
|
||||
case GL_RED_INTEGER:
|
||||
return TextureInputFormat::RInteger;
|
||||
case GL_GREEN_INTEGER:
|
||||
return TextureInputFormat::GreenInteger;
|
||||
case GL_BLUE_INTEGER:
|
||||
return TextureInputFormat::BlueInteger;
|
||||
case GL_ALPHA_INTEGER:
|
||||
return TextureInputFormat::AlphaInteger;
|
||||
case GL_RG_INTEGER:
|
||||
return TextureInputFormat::RGInteger;
|
||||
case GL_RGB_INTEGER:
|
||||
@@ -131,9 +117,6 @@ namespace MobileGL {
|
||||
case GL_RGB4:
|
||||
return TextureInternalFormat::RGB4;
|
||||
case GL_RGB5:
|
||||
// GL_RGB565 (GL 4.1 / ARB_ES2_compatibility, used directly by the GL CTS) is the
|
||||
// ES-facing rendition of the legacy RGB5 resolution.
|
||||
case GL_RGB565:
|
||||
return TextureInternalFormat::RGB5;
|
||||
case GL_RGB8:
|
||||
return TextureInternalFormat::RGB8;
|
||||
@@ -311,8 +294,6 @@ namespace MobileGL {
|
||||
return TexturePixelDataType::UnsignedInt8888;
|
||||
case GL_UNSIGNED_INT_8_8_8_8_REV:
|
||||
return TexturePixelDataType::UnsignedInt8888Rev;
|
||||
case GL_UNSIGNED_INT_10_10_10_2:
|
||||
return TexturePixelDataType::UnsignedInt1010102;
|
||||
case GL_UNSIGNED_INT_10F_11F_11F_REV:
|
||||
return TexturePixelDataType::UnsignedInt101111Rev;
|
||||
case GL_UNSIGNED_INT_2_10_10_10_REV:
|
||||
|
||||
@@ -535,7 +535,6 @@ namespace MobileGL {
|
||||
CASE(GL_RED_INTEGER)
|
||||
CASE(GL_GREEN_INTEGER)
|
||||
CASE(GL_BLUE_INTEGER)
|
||||
CASE(GL_ALPHA_INTEGER)
|
||||
CASE(GL_RGB_INTEGER)
|
||||
CASE(GL_RGBA_INTEGER)
|
||||
CASE(GL_BGR_INTEGER)
|
||||
|
||||
@@ -67,18 +67,6 @@ namespace MobileGL {
|
||||
return GL_RGBA_INTEGER;
|
||||
case TextureInputFormat::BGRAInteger:
|
||||
return GL_BGRA_INTEGER;
|
||||
case TextureInputFormat::Green:
|
||||
return GL_GREEN;
|
||||
case TextureInputFormat::Blue:
|
||||
return GL_BLUE;
|
||||
case TextureInputFormat::Alpha:
|
||||
return GL_ALPHA;
|
||||
case TextureInputFormat::GreenInteger:
|
||||
return GL_GREEN_INTEGER;
|
||||
case TextureInputFormat::BlueInteger:
|
||||
return GL_BLUE_INTEGER;
|
||||
case TextureInputFormat::AlphaInteger:
|
||||
return GL_ALPHA_INTEGER;
|
||||
case TextureInputFormat::StencilIndex:
|
||||
return GL_STENCIL_INDEX;
|
||||
case TextureInputFormat::DepthComponent:
|
||||
@@ -113,10 +101,7 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RGB4:
|
||||
return GL_RGB4;
|
||||
case TextureInternalFormat::RGB5:
|
||||
// Emit the ES-compatible GL_RGB565 rendition: desktop GL_RGB5 is not a legal
|
||||
// sized internalformat on OpenGL ES backends, GL_RGB565 is (and GL 4.1+
|
||||
// accepts it too via ARB_ES2_compatibility).
|
||||
return GL_RGB565;
|
||||
return GL_RGB5;
|
||||
case TextureInternalFormat::RGB8:
|
||||
return GL_RGB8;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
|
||||
@@ -67,18 +67,6 @@ namespace MobileGL {
|
||||
return "RGBAInteger";
|
||||
case TextureInputFormat::BGRAInteger:
|
||||
return "BGRAInteger";
|
||||
case TextureInputFormat::Green:
|
||||
return "Green";
|
||||
case TextureInputFormat::Blue:
|
||||
return "Blue";
|
||||
case TextureInputFormat::Alpha:
|
||||
return "Alpha";
|
||||
case TextureInputFormat::GreenInteger:
|
||||
return "GreenInteger";
|
||||
case TextureInputFormat::BlueInteger:
|
||||
return "BlueInteger";
|
||||
case TextureInputFormat::AlphaInteger:
|
||||
return "AlphaInteger";
|
||||
case TextureInputFormat::StencilIndex:
|
||||
return "StencilIndex";
|
||||
case TextureInputFormat::DepthComponent:
|
||||
|
||||
@@ -65,16 +65,6 @@ namespace MobileGL {
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInputFormat::BGRAInteger:
|
||||
return VK_FORMAT_B8G8R8A8_UINT;
|
||||
// Single-channel desktop client formats: the client memory holds one component per pixel,
|
||||
// matching the R8 layouts (the channel it feeds is a pixel-transfer concern, not a layout one).
|
||||
case TextureInputFormat::Green:
|
||||
case TextureInputFormat::Blue:
|
||||
case TextureInputFormat::Alpha:
|
||||
return VK_FORMAT_R8_UNORM;
|
||||
case TextureInputFormat::GreenInteger:
|
||||
case TextureInputFormat::BlueInteger:
|
||||
case TextureInputFormat::AlphaInteger:
|
||||
return VK_FORMAT_R8_UINT;
|
||||
case TextureInputFormat::StencilIndex:
|
||||
return VK_FORMAT_S8_UINT;
|
||||
case TextureInputFormat::DepthComponent:
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/Math/HalfFloat.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Util {
|
||||
inline Float DecodeHalfBitsToFloat(Uint16 half) {
|
||||
const Uint32 sign = static_cast<Uint32>(half & 0x8000u) << 16;
|
||||
const Uint32 exponent = (half >> 10) & 0x1Fu;
|
||||
const Uint32 mantissa = half & 0x3FFu;
|
||||
Uint32 bits;
|
||||
if (exponent == 0) {
|
||||
if (mantissa == 0) {
|
||||
bits = sign; // signed zero
|
||||
} else {
|
||||
// Subnormal half: renormalize into a float exponent.
|
||||
Uint32 e = 127 - 15 + 1;
|
||||
Uint32 m = mantissa;
|
||||
while ((m & 0x400u) == 0) {
|
||||
m <<= 1;
|
||||
--e;
|
||||
}
|
||||
bits = sign | (e << 23) | ((m & 0x3FFu) << 13);
|
||||
}
|
||||
} else if (exponent == 31) {
|
||||
bits = sign | 0x7F800000u | (mantissa << 13); // Inf / NaN
|
||||
} else {
|
||||
bits = sign | ((exponent + 112) << 23) | (mantissa << 13);
|
||||
}
|
||||
return std::bit_cast<Float>(bits);
|
||||
}
|
||||
|
||||
inline Uint16 EncodeFloatToHalfBits(Float value) {
|
||||
const Uint32 bits = std::bit_cast<Uint32>(value);
|
||||
const auto sign = static_cast<Uint16>((bits >> 16) & 0x8000u);
|
||||
const Uint32 exponent = (bits >> 23) & 0xFFu;
|
||||
const Uint32 mantissa = bits & 0x7FFFFFu;
|
||||
if (exponent == 0xFF) { // Inf / NaN
|
||||
return static_cast<Uint16>(sign | 0x7C00u | (mantissa != 0 ? 0x200u : 0u));
|
||||
}
|
||||
const Int32 halfExponent = static_cast<Int32>(exponent) - 127 + 15;
|
||||
if (halfExponent >= 31) {
|
||||
return static_cast<Uint16>(sign | 0x7C00u); // overflow -> Inf
|
||||
}
|
||||
if (halfExponent <= 0) {
|
||||
if (halfExponent < -10) {
|
||||
return sign; // underflow -> signed zero
|
||||
}
|
||||
const Uint32 m = mantissa | 0x800000u;
|
||||
const Uint32 shift = static_cast<Uint32>(14 - halfExponent);
|
||||
Uint32 half = m >> shift;
|
||||
if ((m >> (shift - 1)) & 1u) {
|
||||
++half; // round to nearest
|
||||
}
|
||||
return static_cast<Uint16>(sign | half);
|
||||
}
|
||||
Uint32 half = (static_cast<Uint32>(halfExponent) << 10) | (mantissa >> 13);
|
||||
if (mantissa & 0x1000u) {
|
||||
++half; // round to nearest; a carry into the exponent is the correct result
|
||||
}
|
||||
return static_cast<Uint16>(sign | half);
|
||||
}
|
||||
} // namespace MobileGL::MG_Util
|
||||
@@ -1,116 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/Math/SmallFloat.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace MobileGL::MG_Util {
|
||||
// Encodes an unsigned small float with a 5-bit exponent (bias 15) and mantissaBits mantissa
|
||||
// bits, per the EXT_packed_float conversion rules: negatives (including -Inf) go to zero,
|
||||
// +Inf stays +Inf, NaN stays NaN, and finite values above the largest representable value
|
||||
// clamp to it. The mantissa is truncated (rounding mode is implementation-defined).
|
||||
inline Uint32 EncodeFloatToUnsignedSmallFloat(Float value, Int mantissaBits) {
|
||||
const Uint32 bits = std::bit_cast<Uint32>(value);
|
||||
const Bool negative = (bits & 0x80000000u) != 0;
|
||||
const Uint32 exponent = (bits >> 23) & 0xFFu;
|
||||
const Uint32 mantissa = bits & 0x7FFFFFu;
|
||||
const Uint32 exponentMask = 0x1Fu << mantissaBits;
|
||||
if (exponent == 0xFFu) {
|
||||
if (mantissa != 0) {
|
||||
return exponentMask | 1u; // NaN keeps NaN
|
||||
}
|
||||
return negative ? 0u : exponentMask; // -Inf -> 0, +Inf -> +Inf
|
||||
}
|
||||
if (negative) {
|
||||
return 0u;
|
||||
}
|
||||
const Int32 smallExponent = static_cast<Int32>(exponent) - 127 + 15;
|
||||
if (smallExponent >= 31) { // above the largest finite value -> clamp to it
|
||||
return ((31u - 1u) << mantissaBits) | ((1u << mantissaBits) - 1u);
|
||||
}
|
||||
if (smallExponent <= 0) { // subnormal range: renormalize, flushing tiny values to zero
|
||||
const Uint32 fullMantissa = mantissa | 0x800000u;
|
||||
const Int32 shift = (23 - mantissaBits) + 1 - smallExponent;
|
||||
return shift > 23 ? 0u : fullMantissa >> shift;
|
||||
}
|
||||
return (static_cast<Uint32>(smallExponent) << mantissaBits) |
|
||||
(mantissa >> (23u - static_cast<Uint32>(mantissaBits)));
|
||||
}
|
||||
|
||||
inline Uint32 EncodeFloatToUnsignedF11(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 6); }
|
||||
inline Uint32 EncodeFloatToUnsignedF10(Float value) { return EncodeFloatToUnsignedSmallFloat(value, 5); }
|
||||
|
||||
// Decodes an unsigned small float (5-bit exponent, bias 15, mantissaBits mantissa bits).
|
||||
inline Float DecodeUnsignedSmallFloatToFloat(Uint32 field, Int mantissaBits) {
|
||||
const Uint32 exponent = (field >> mantissaBits) & 0x1Fu;
|
||||
const Uint32 mantissa = field & ((1u << mantissaBits) - 1u);
|
||||
const Float mantissaScale = 1.0f / static_cast<Float>(1u << mantissaBits);
|
||||
if (exponent == 0) {
|
||||
return std::exp2(-14.0f) * static_cast<Float>(mantissa) * mantissaScale;
|
||||
}
|
||||
if (exponent == 31) {
|
||||
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
|
||||
: std::numeric_limits<Float>::quiet_NaN();
|
||||
}
|
||||
return std::exp2(static_cast<Float>(exponent) - 15.0f) *
|
||||
(1.0f + static_cast<Float>(mantissa) * mantissaScale);
|
||||
}
|
||||
|
||||
inline Float DecodeUnsignedF11ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 6); }
|
||||
inline Float DecodeUnsignedF10ToFloat(Uint32 field) { return DecodeUnsignedSmallFloatToFloat(field, 5); }
|
||||
|
||||
// RGB9E5 shared-exponent encode, following the EXT_texture_shared_exponent spec algorithm
|
||||
// (N = 9 mantissa bits, B = 15 exponent bias, Emax = 31).
|
||||
inline Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) {
|
||||
constexpr Int kMantissaBits = 9;
|
||||
constexpr Int kExponentBias = 15;
|
||||
constexpr Float kSharedExpMax = 511.0f / 512.0f * 65536.0f; // (2^N-1)/2^N * 2^(Emax-B)
|
||||
|
||||
Float clamped[3];
|
||||
for (Int i = 0; i < 3; ++i) {
|
||||
const Float v = rgb[i];
|
||||
clamped[i] = (std::isnan(v) || v < 0.0f) ? 0.0f : std::min(v, kSharedExpMax);
|
||||
}
|
||||
const Float maxComponent = std::max(clamped[0], std::max(clamped[1], clamped[2]));
|
||||
|
||||
Int sharedExponent = 0; // all-zero input keeps the all-zero word
|
||||
if (maxComponent > 0.0f) {
|
||||
sharedExponent = std::max(-kExponentBias - 1, static_cast<Int>(std::floor(std::log2(maxComponent)))) +
|
||||
1 + kExponentBias;
|
||||
const Float maxScaled = std::floor(
|
||||
maxComponent / std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits)) +
|
||||
0.5f);
|
||||
if (maxScaled >= 512.0f) { // rounded up to 2^N: bump the shared exponent instead
|
||||
++sharedExponent;
|
||||
}
|
||||
}
|
||||
|
||||
const Float scale = std::exp2(static_cast<Float>(sharedExponent - kExponentBias - kMantissaBits));
|
||||
Uint32 word = static_cast<Uint32>(sharedExponent) << 27;
|
||||
for (Int i = 0; i < 3; ++i) {
|
||||
const auto field = static_cast<Uint32>(std::floor(clamped[i] / scale + 0.5f));
|
||||
word |= std::min(field, 511u) << (i * kMantissaBits);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
// RGB9E5 shared-exponent decode.
|
||||
inline void DecodeSharedExponentRGB9E5(Uint32 word, Float outRgb[3]) {
|
||||
constexpr Int kMantissaBits = 9;
|
||||
constexpr Int kExponentBias = 15;
|
||||
const Int exponent = static_cast<Int>(word >> 27) - kExponentBias - kMantissaBits;
|
||||
const Float scale = std::exp2(static_cast<Float>(exponent));
|
||||
for (Int i = 0; i < 3; ++i) {
|
||||
outRgb[i] = static_cast<Float>((word >> (i * kMantissaBits)) & 0x1FFu) * scale;
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Util
|
||||
@@ -15,10 +15,10 @@ namespace MobileGL {
|
||||
SizeT GetSizedInternalFormatSizeInBytes(TextureInternalFormat internal) {
|
||||
switch (internal) {
|
||||
case TextureInternalFormat::R8:
|
||||
case TextureInternalFormat::Red: // UNorm8 shadow layout
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
return 1;
|
||||
|
||||
case TextureInternalFormat::R16:
|
||||
@@ -27,17 +27,14 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG8:
|
||||
case TextureInternalFormat::RG: // UNorm8x2 shadow layout
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::DepthComponent16:
|
||||
return 2;
|
||||
|
||||
case TextureInternalFormat::R3G3B2: // UNorm8x3 shadow layout
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
case TextureInternalFormat::RGB: // UNorm8x3 shadow layout
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::SRGB8:
|
||||
@@ -46,10 +43,11 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::DepthComponent24:
|
||||
return 3;
|
||||
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
case TextureInternalFormat::RGBA: // UNorm8x4 shadow layout
|
||||
case TextureInternalFormat::RGBA8:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
@@ -74,8 +72,6 @@ namespace MobileGL {
|
||||
return 4;
|
||||
|
||||
case TextureInternalFormat::RGB16:
|
||||
case TextureInternalFormat::RGB10: // UNorm16x3 shadow layout
|
||||
case TextureInternalFormat::RGB12: // UNorm16x3 shadow layout
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
@@ -117,12 +113,6 @@ namespace MobileGL {
|
||||
switch (format) {
|
||||
case TextureInputFormat::Red:
|
||||
case TextureInputFormat::RInteger:
|
||||
case TextureInputFormat::Green:
|
||||
case TextureInputFormat::GreenInteger:
|
||||
case TextureInputFormat::Blue:
|
||||
case TextureInputFormat::BlueInteger:
|
||||
case TextureInputFormat::Alpha:
|
||||
case TextureInputFormat::AlphaInteger:
|
||||
return 1;
|
||||
case TextureInputFormat::RG:
|
||||
case TextureInputFormat::RGInteger:
|
||||
|
||||
@@ -548,8 +548,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (summary.capsValid) {
|
||||
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
|
||||
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
|
||||
advertisedExtensions = JoinAdvertisedExtensions(
|
||||
MG_Backend::DirectGLES::BuildAdvertisedExtensions(summary.caps.SupportsDisjointTimerQuery));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
@@ -841,7 +841,6 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
String driverVersionString; // raw hex, vendor-encoded (see RunVulkanDriverPost)
|
||||
Bool shaderSubgroupUsable = false;
|
||||
Bool timerQueriesSupported = false;
|
||||
Bool samplerAnisotropySupported = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1108,7 +1107,6 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
VkPhysicalDeviceFeatures features{};
|
||||
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
|
||||
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
|
||||
if (features.multiDrawIndirect == VK_TRUE) {
|
||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||
} else {
|
||||
@@ -1281,7 +1279,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
|
||||
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include "ShaderSourceProcessor.h"
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
@@ -135,23 +132,27 @@ namespace MobileGL {
|
||||
return Resources;
|
||||
}
|
||||
|
||||
// One parse attempt. A glslang::TShader cannot be re-parsed, so a retry has to build a
|
||||
// fresh one with byte-identical setup - hence a single factored body rather than two
|
||||
// copies that could drift apart.
|
||||
static Result<SharedPtr<glslang::TShader>> ParseShaderSource(EShLanguage lang, GLenum shaderType,
|
||||
const String& source,
|
||||
Flags<ShaderCompileBits> flags) {
|
||||
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
|
||||
auto shaderType = attrib.shaderType;
|
||||
auto& sourceStr = attrib.sourceStr;
|
||||
|
||||
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
|
||||
if (lang == EShLanguage::EShLangCount) {
|
||||
ResultInfo r;
|
||||
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
|
||||
r.errc = -1;
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
SharedPtr<glslang::TShader> res;
|
||||
auto& tshader = res;
|
||||
tshader = MakeShared<glslang::TShader>(lang);
|
||||
// setStrings gets no length array, so it relies on NUL termination: source must be an
|
||||
// owning buffer that outlives parse(), never a StringView's substring.
|
||||
const char* src[] = {source.c_str()};
|
||||
const char* src[] = {sourceStr.data()};
|
||||
tshader->setStrings(src, 1);
|
||||
tshader->setNanMinMaxClamp(true);
|
||||
tshader->setInvertY(true);
|
||||
tshader->setPreamble("#undef VULKAN\n");
|
||||
if (flags & ShaderCompileBits::CompileForOpenGL) {
|
||||
if (attrib.flags & ShaderCompileBits::CompileForOpenGL) {
|
||||
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
|
||||
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
|
||||
tshader->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3);
|
||||
@@ -180,39 +181,6 @@ namespace MobileGL {
|
||||
return res;
|
||||
}
|
||||
|
||||
Result<SharedPtr<glslang::TShader>> ShaderCompiler::CompileShader(const ShaderAttrib& attrib) {
|
||||
auto shaderType = attrib.shaderType;
|
||||
|
||||
auto lang = MG_Util::ConvertGLEnumToEShLanguage(shaderType);
|
||||
if (lang == EShLanguage::EShLangCount) {
|
||||
ResultInfo r;
|
||||
r.log += "Error: [Preprocess] Unsupported shader type: " + ConvertGLEnumToString(shaderType);
|
||||
r.errc = -1;
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
const String source(attrib.sourceStr);
|
||||
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
|
||||
if (result) return result;
|
||||
|
||||
// Legacy desktop sources are normalized to "#version 330 core", which parses under
|
||||
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
|
||||
// using e.g. layout(binding=...) without the matching #extension line compiles on real
|
||||
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
|
||||
// broken shader fails both attempts and keeps its original diagnostics.
|
||||
String retrySource = source;
|
||||
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags);
|
||||
if (!retryResult) return result;
|
||||
|
||||
MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460",
|
||||
ConvertGLEnumToString(shaderType).c_str());
|
||||
return retryResult;
|
||||
}
|
||||
|
||||
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
|
||||
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
|
||||
for (auto& s : attrib.shaders) {
|
||||
@@ -298,19 +266,6 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
@@ -323,75 +278,6 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
static constexpr Uint32 kHeaderWords = 5;
|
||||
static constexpr Uint32 kOpDecorate = 71;
|
||||
static constexpr Uint32 kOpMemberDecorate = 72;
|
||||
static constexpr Uint32 kDecorationInvariant = 18;
|
||||
static constexpr Uint32 kDecorationBuiltIn = 11;
|
||||
static constexpr Uint32 kBuiltInPosition = 0;
|
||||
if (inputBinary.size() < kHeaderWords) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First pass: find targets that already carry Invariant so we never duplicate.
|
||||
struct MemberKey {
|
||||
Uint32 id;
|
||||
Uint32 member;
|
||||
bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; }
|
||||
};
|
||||
Vector<Uint32> invariantIds;
|
||||
Vector<MemberKey> invariantMembers;
|
||||
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
|
||||
const Uint32 word0 = inputBinary[i];
|
||||
const Uint32 opcode = word0 & 0xFFFFu;
|
||||
const Uint32 length = word0 >> 16;
|
||||
if (length == 0 || i + length > inputBinary.size()) {
|
||||
return false;
|
||||
}
|
||||
if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) {
|
||||
invariantIds.push_back(inputBinary[i + 1]);
|
||||
} else if (opcode == kOpMemberDecorate && length >= 4 &&
|
||||
inputBinary[i + 3] == kDecorationInvariant) {
|
||||
invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]});
|
||||
}
|
||||
i += length;
|
||||
}
|
||||
|
||||
outputBinary.clear();
|
||||
outputBinary.reserve(inputBinary.size() + 8);
|
||||
outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords);
|
||||
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
|
||||
const Uint32 word0 = inputBinary[i];
|
||||
const Uint32 opcode = word0 & 0xFFFFu;
|
||||
const Uint32 length = word0 >> 16;
|
||||
outputBinary.insert(outputBinary.end(), inputBinary.begin() + i,
|
||||
inputBinary.begin() + i + length);
|
||||
if (opcode == kOpDecorate && length == 4 &&
|
||||
inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) {
|
||||
const Uint32 target = inputBinary[i + 1];
|
||||
if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) {
|
||||
outputBinary.push_back((3u << 16) | kOpDecorate);
|
||||
outputBinary.push_back(target);
|
||||
outputBinary.push_back(kDecorationInvariant);
|
||||
}
|
||||
} else if (opcode == kOpMemberDecorate && length == 5 &&
|
||||
inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) {
|
||||
const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]};
|
||||
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) ==
|
||||
invariantMembers.end()) {
|
||||
outputBinary.push_back((4u << 16) | kOpMemberDecorate);
|
||||
outputBinary.push_back(key.id);
|
||||
outputBinary.push_back(key.member);
|
||||
outputBinary.push_back(kDecorationInvariant);
|
||||
}
|
||||
}
|
||||
i += length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
||||
spvc_compiler_options options;
|
||||
session.CreateOptions(&options);
|
||||
|
||||
@@ -27,25 +27,12 @@ namespace MobileGL {
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
// Only for the DirectGLES transpile path.
|
||||
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
// which wrongly includes baseInstance).
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Adds the Invariant decoration to every Position builtin output. GL apps
|
||||
// routinely rely on cross-program position invariance for multi-pass
|
||||
// equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
|
||||
// mobile drivers that optimize per-pipeline break that without the
|
||||
// decoration. DirectVulkan only.
|
||||
static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
static Result<String> DecompileShader(SpvcSession& session);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
|
||||
@@ -471,65 +471,6 @@ namespace {
|
||||
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
||||
}
|
||||
|
||||
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
|
||||
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
|
||||
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
|
||||
// std140 (glslang under a SPIR-V target rejects `packed`/`shared` outright and SPIRV-Cross has
|
||||
// no other packing for UBOs), so std140 IS this implementation's chosen layout. Rewriting at
|
||||
// the source level keeps the validation compile, the reflection the app queries, and the
|
||||
// generated SPIR-V all agreeing on that choice. Both replacement tokens are 6 characters, so
|
||||
// the rewrite is done in place.
|
||||
void CoerceUniformBlockPackingToStd140(MobileGL::String& source) {
|
||||
constexpr const char* layoutToken = "layout";
|
||||
constexpr SizeT layoutLen = 6;
|
||||
|
||||
SizeT pos = 0;
|
||||
while ((pos = source.find(layoutToken, pos)) != MobileGL::String::npos) {
|
||||
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]);
|
||||
SizeT probe = pos + layoutLen;
|
||||
const bool hasRightBoundary = probe >= source.size() || !IsIdentifierChar(source[probe]);
|
||||
if (!hasLeftBoundary || !hasRightBoundary) {
|
||||
pos = probe;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (probe < source.size() && std::isspace(static_cast<unsigned char>(source[probe]))) {
|
||||
probe++;
|
||||
}
|
||||
if (probe >= source.size() || source[probe] != '(') {
|
||||
pos = probe;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan the qualifier list; layout qualifier values may contain parenthesized
|
||||
// constant expressions, so track nesting until the matching ')'.
|
||||
SizeT cursor = probe + 1;
|
||||
int depth = 1;
|
||||
while (cursor < source.size() && depth > 0) {
|
||||
const char ch = source[cursor];
|
||||
if (ch == '(') {
|
||||
depth++;
|
||||
} else if (ch == ')') {
|
||||
depth--;
|
||||
} else if (IsIdentifierChar(ch) && (cursor == 0 || !IsIdentifierChar(source[cursor - 1]))) {
|
||||
SizeT identifierEnd = cursor;
|
||||
while (identifierEnd < source.size() && IsIdentifierChar(source[identifierEnd])) {
|
||||
identifierEnd++;
|
||||
}
|
||||
const SizeT identifierLen = identifierEnd - cursor;
|
||||
if (identifierLen == 6 && (source.compare(cursor, 6, "packed") == 0 ||
|
||||
source.compare(cursor, 6, "shared") == 0)) {
|
||||
source.replace(cursor, 6, "std140");
|
||||
}
|
||||
cursor = identifierEnd;
|
||||
continue;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
pos = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) {
|
||||
// Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and
|
||||
// ignored in the normalized desktop core profiles, so glslang handles them natively.
|
||||
@@ -620,7 +561,6 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
FilterUnsupportedGpuShaderInt64(source);
|
||||
CoerceUniformBlockPackingToStd140(source);
|
||||
|
||||
// Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma().
|
||||
// These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation.
|
||||
@@ -633,21 +573,6 @@ namespace MobileGL {
|
||||
InjectDepthRangeBuiltinShim(stage, source);
|
||||
}
|
||||
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
||||
// Re-inspect rather than searching for the literal directive: it is not necessarily at
|
||||
// offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere
|
||||
// must not be mistaken for the real one.
|
||||
const ShaderLanguageInfo info = InspectShaderLanguage(source);
|
||||
if (!info.HasVersionDirective()) return false;
|
||||
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
|
||||
// compatibility shaders keep whatever they declared.
|
||||
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
|
||||
|
||||
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
|
||||
"#version 460 core\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -20,14 +20,6 @@ namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
void PreprocessShaderSource(ShaderStage stage, String& source);
|
||||
|
||||
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
|
||||
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving
|
||||
// the source untouched) for anything else: ES, compatibility, or an already-modern
|
||||
// declaration. Exists so a shader that only parses under the laxer 460 rules - e.g. it
|
||||
// uses 420-era syntax without the matching #extension line, which real drivers tend to
|
||||
// accept - can be retried instead of failing to compile.
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,119 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 "StripUboMemberRelaxedPrecisionPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// Marks `typeId` and every struct type reachable through its members
|
||||
// (following arrays) for decoration stripping.
|
||||
void CollectStructTypes(IRContext* context, uint32_t typeId,
|
||||
std::unordered_set<uint32_t>& structTypeIds) {
|
||||
Instruction* typeInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (typeInst == nullptr) return;
|
||||
|
||||
switch (typeInst->opcode()) {
|
||||
case spv::Op::OpTypeStruct: {
|
||||
if (!structTypeIds.insert(typeId).second) return; // already visited
|
||||
for (uint32_t member = 0; member < typeInst->NumInOperands(); ++member) {
|
||||
CollectStructTypes(context, typeInst->GetSingleWordInOperand(member), structTypeIds);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
CollectStructTypes(context, typeInst->GetSingleWordInOperand(0), structTypeIds);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status StripUboMemberRelaxedPrecisionPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// Uniform blocks: StorageClass Uniform variables whose pointee struct carries
|
||||
// the Block decoration (BufferBlock/StorageBuffer SSBOs are left alone - they
|
||||
// are not stage-matched by member precision in this pipeline's ESSL output).
|
||||
std::unordered_set<uint32_t> blockStructIds;
|
||||
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) != spv::Decoration::Block) {
|
||||
continue;
|
||||
}
|
||||
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
std::unordered_set<uint32_t> structTypeIds;
|
||||
for (Instruction& variable : irContext->module()->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Uniform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue;
|
||||
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
|
||||
|
||||
// Instance-arrayed blocks: unwrap the array around the block struct.
|
||||
Instruction* pointee = defUseMgr->GetDef(pointeeId);
|
||||
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
|
||||
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||
pointeeId = pointee->GetSingleWordInOperand(0);
|
||||
pointee = defUseMgr->GetDef(pointeeId);
|
||||
}
|
||||
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) continue;
|
||||
if (blockStructIds.find(pointeeId) == blockStructIds.end()) continue;
|
||||
|
||||
CollectStructTypes(irContext, pointeeId, structTypeIds);
|
||||
}
|
||||
if (structTypeIds.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
std::vector<Instruction*> decorationsToRemove;
|
||||
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) !=
|
||||
spv::Decoration::RelaxedPrecision) {
|
||||
continue;
|
||||
}
|
||||
if (structTypeIds.find(annotation.GetSingleWordInOperand(0)) == structTypeIds.end()) continue;
|
||||
decorationsToRemove.push_back(&annotation);
|
||||
}
|
||||
if (decorationsToRemove.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
for (Instruction* decoration : decorationsToRemove) {
|
||||
irContext->KillInst(decoration);
|
||||
}
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<StripUboMemberRelaxedPrecisionPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,44 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Removes RelaxedPrecision member decorations from every struct type reachable
|
||||
// from a uniform-block variable (the block struct itself and any structs nested
|
||||
// in it through members or arrays).
|
||||
//
|
||||
// Rationale: ESSL requires matched uniform blocks to declare members with
|
||||
// identical precision in every stage, but SPIRV-Cross prints a member's
|
||||
// qualifier relative to the stage's DEFAULT precision (highp in the vertex
|
||||
// stage, mediump in the fragment stage). A RelaxedPrecision member therefore
|
||||
// comes out as an explicit "mediump" in the vertex shader but UNQUALIFIED in
|
||||
// the fragment shader - and once ForceSupporterOutput swaps the fragment
|
||||
// header to "precision highp float;", that unqualified member reads back as
|
||||
// highp and the ES driver refuses to link ("definitions of uniform block ...
|
||||
// do not match", GL CTS KHR-GL33.shaders.uniform_block struct sub-groups).
|
||||
// Dropping the hint promotes the member to highp in BOTH stages, which is
|
||||
// always conformant and matches the std140 data layout either way. Only meant
|
||||
// for the DirectGLES transpile path - block member precision is a per-member
|
||||
// hint with no layout effect, and no other emission behavior changes.
|
||||
class StripUboMemberRelaxedPrecisionPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "strip-ubo-member-relaxed-precision"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripUboMemberRelaxedPrecisionPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -48,69 +48,6 @@ namespace MobileGL {
|
||||
return SPVC_BASETYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
// Record one flattened leaf uniform of the global UBO into the metadata maps.
|
||||
static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name,
|
||||
Uint32 offsetInUBO, SpvcMetadata& metadata) {
|
||||
metadata.plainUniformOffsetsInUBO[name] = offsetInUBO;
|
||||
metadata.plainUniformMemberSizesInBytes[name] = member.size;
|
||||
metadata.plainUniformArrayStridesInUBO[name] =
|
||||
member.array.dims_count > 0 ? member.array.stride : 0;
|
||||
|
||||
Uint32 vectorSize = member.numeric.vector.component_count;
|
||||
if (vectorSize == 0) vectorSize = 1;
|
||||
Uint32 matCol = member.numeric.matrix.column_count;
|
||||
if (matCol == 0) matCol = 1;
|
||||
metadata.plainUniformMemberTypes[name] = {
|
||||
.basetype = MapReflectToSpvcBasetype(member),
|
||||
.vectorSize = vectorSize,
|
||||
.matCol = matCol,
|
||||
};
|
||||
}
|
||||
|
||||
// Flatten a (possibly nested struct / struct array) member of the global UBO
|
||||
// into leaf entries named the way glslang reflection names plain uniforms:
|
||||
// "s[0].b[1].b" for `uniform S s[2]` with `struct T { vec2 b[2]; }` members.
|
||||
// glUniform* writes are routed per leaf location, so the state layer needs a
|
||||
// byte offset for every leaf, not just for the top-level block members.
|
||||
// `baseOffset` accumulates parent offsets; member.offset is relative to the
|
||||
// enclosing struct (top-level members: relative to the block start).
|
||||
static void FlattenGlobalUboMember(const SpvReflectBlockVariable& member, const String& prefix,
|
||||
Uint32 baseOffset, SpvcMetadata& metadata) {
|
||||
const String name = prefix + (member.name != nullptr ? member.name : "");
|
||||
const Uint32 selfOffset = baseOffset + member.offset;
|
||||
|
||||
if (member.member_count == 0 || member.members == nullptr) {
|
||||
RecordGlobalUboLeaf(member, name, selfOffset, metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.array.dims_count == 0) {
|
||||
// Plain nested struct.
|
||||
for (Uint32 j = 0; j < member.member_count; ++j) {
|
||||
FlattenGlobalUboMember(member.members[j], name + ".", selfOffset, metadata);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.array.dims_count > 1) {
|
||||
// Arrays of arrays of structs cannot be declared in the GL 3.3-era GLSL
|
||||
// MobileGL ingests; record the base so at least element 0 resolves.
|
||||
MGLOG_W("FlattenGlobalUboMember: multi-dimensional struct array '%s' is not supported, "
|
||||
"flattening element 0 only",
|
||||
name.c_str());
|
||||
}
|
||||
|
||||
const Uint32 elementCount = member.array.dims[0] > 0 ? member.array.dims[0] : 1;
|
||||
const Uint32 elementStride = member.array.stride;
|
||||
for (Uint32 element = 0; element < elementCount; ++element) {
|
||||
const String elementPrefix = name + "[" + std::to_string(element) + "].";
|
||||
const Uint32 elementOffset = selfOffset + element * elementStride;
|
||||
for (Uint32 j = 0; j < member.member_count; ++j) {
|
||||
FlattenGlobalUboMember(member.members[j], elementPrefix, elementOffset, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpvcSession::SpvcSession(const Vector<unsigned int>& spirv, Flags<SessionUsageBit> usage)
|
||||
: usage(usage) {
|
||||
if (usage & SessionUsageBit::Transpile) {
|
||||
@@ -362,10 +299,20 @@ namespace MobileGL {
|
||||
metadata.globalUboSize = block.size;
|
||||
|
||||
for (uint32_t j = 0; j < block.member_count; ++j) {
|
||||
// Recurse into nested structs / struct arrays so every leaf
|
||||
// uniform ("s[0].b[1].b") gets its real byte offset; top-level
|
||||
// scalars/vectors/matrices flatten to themselves.
|
||||
FlattenGlobalUboMember(block.members[j], "", 0, metadata);
|
||||
auto& member = block.members[j];
|
||||
metadata.plainUniformOffsetsInUBO[member.name] = member.offset;
|
||||
metadata.plainUniformMemberSizesInBytes[member.name] = member.size;
|
||||
|
||||
Uint32 vectorSize = member.numeric.vector.component_count;
|
||||
if (vectorSize == 0) vectorSize = 1;
|
||||
Uint32 matCol = member.numeric.matrix.column_count;
|
||||
if (matCol == 0) matCol = 1;
|
||||
|
||||
metadata.plainUniformMemberTypes[member.name] = {
|
||||
.basetype = MapReflectToSpvcBasetype(member),
|
||||
.vectorSize = vectorSize,
|
||||
.matCol = matCol,
|
||||
};
|
||||
}
|
||||
return SPVC_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -65,11 +65,6 @@ namespace MobileGL {
|
||||
UnorderedMap<String, unsigned> plainUniformOffsetsInUBO;
|
||||
UnorderedMap<String, SizeT> plainUniformMemberSizesInBytes;
|
||||
UnorderedMap<String, SpvcType> plainUniformMemberTypes;
|
||||
// Byte stride between consecutive array elements of an arrayed plain
|
||||
// uniform (0 for non-arrays). Keyed like the offset map: names are the
|
||||
// flattened leaf names glslang reflection uses ("s[0].b[1].b"), without
|
||||
// a trailing "[0]".
|
||||
UnorderedMap<String, Uint32> plainUniformArrayStridesInUBO;
|
||||
SizeT globalUboSize = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "PixelStoreProcessor.h"
|
||||
#include "MG_Util/Math/HalfFloat.h"
|
||||
#include "MG_Util/Math/SmallFloat.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
static SizeT CalculateRowStride(Int width, SizeT pixelSize, Int alignment) {
|
||||
@@ -73,674 +70,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Unpack channel expansion / type conversion ------------------------------------------------------------
|
||||
// The shadow mip buffer stores every level in the internal format's canonical layout: its channels in
|
||||
// R,G,B(,A) order, encoded with the component type the backends upload with (see
|
||||
// TextureFormatProcessor::NormalizePixelFormat; channelCount * componentSize matches
|
||||
// GetSizedInternalFormatSizeInBytes for every format listed below). When the client's (format, type)
|
||||
// does not already produce that byte layout, each texel is decoded to RGBA (float for normalized/float
|
||||
// formats, integer for *_INTEGER formats, missing G/B = 0 and missing A = 1) and re-encoded.
|
||||
|
||||
namespace {
|
||||
enum class ShadowComponent {
|
||||
UNorm8,
|
||||
SNorm8,
|
||||
UNorm16,
|
||||
SNorm16,
|
||||
UInt8,
|
||||
Int8,
|
||||
UInt16,
|
||||
Int16,
|
||||
UInt32,
|
||||
Int32,
|
||||
Half,
|
||||
Float32,
|
||||
};
|
||||
|
||||
struct InternalShadowLayout {
|
||||
Int channelCount;
|
||||
ShadowComponent component;
|
||||
Bool isInteger;
|
||||
};
|
||||
|
||||
SizeT GetShadowComponentSize(ShadowComponent component) {
|
||||
switch (component) {
|
||||
case ShadowComponent::UNorm8:
|
||||
case ShadowComponent::SNorm8:
|
||||
case ShadowComponent::UInt8:
|
||||
case ShadowComponent::Int8:
|
||||
return 1;
|
||||
case ShadowComponent::UNorm16:
|
||||
case ShadowComponent::SNorm16:
|
||||
case ShadowComponent::UInt16:
|
||||
case ShadowComponent::Int16:
|
||||
case ShadowComponent::Half:
|
||||
return 2;
|
||||
default:
|
||||
return 4;
|
||||
static Bool GetRgba8ByteSwizzleForUnpack(TextureInputFormat inputFormat, TexturePixelDataType inputDataType,
|
||||
Vector<TextureSwizzleParam>& swizzle) {
|
||||
if (inputFormat == TextureInputFormat::RGBA) {
|
||||
if (inputDataType == TexturePixelDataType::UnsignedInt8888) {
|
||||
swizzle = {TextureSwizzleParam::Alpha, TextureSwizzleParam::Blue, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Red};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
|
||||
switch (internal) {
|
||||
case TextureInternalFormat::R8:
|
||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RG8:
|
||||
case TextureInternalFormat::RG: out = {2, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::SRGB8: out = {3, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGBA8:
|
||||
case TextureInternalFormat::RGBA:
|
||||
case TextureInternalFormat::SRGB8Alpha8: out = {4, ShadowComponent::UNorm8, false}; return true;
|
||||
|
||||
// Legacy desktop-GL sized normalized formats are stored in the closest ES-legal layout
|
||||
// (see TextureFormatProcessor::NormalizePixelFormat): 8-bit unorm for <=8-bit channels,
|
||||
// 16-bit unorm for 10/12-bit channels.
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5: out = {3, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGBA2:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
case TextureInternalFormat::RGB5A1: out = {4, ShadowComponent::UNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12: out = {3, ShadowComponent::UNorm16, false}; return true;
|
||||
case TextureInternalFormat::RGBA12: out = {4, ShadowComponent::UNorm16, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R8Snorm: out = {1, ShadowComponent::SNorm8, false}; return true;
|
||||
case TextureInternalFormat::RG8Snorm: out = {2, ShadowComponent::SNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGB8Snorm: out = {3, ShadowComponent::SNorm8, false}; return true;
|
||||
case TextureInternalFormat::RGBA8Snorm: out = {4, ShadowComponent::SNorm8, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R16: out = {1, ShadowComponent::UNorm16, false}; return true;
|
||||
case TextureInternalFormat::RG16: out = {2, ShadowComponent::UNorm16, false}; return true;
|
||||
case TextureInternalFormat::RGB16: out = {3, ShadowComponent::UNorm16, false}; return true;
|
||||
case TextureInternalFormat::RGBA16: out = {4, ShadowComponent::UNorm16, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R16Snorm: out = {1, ShadowComponent::SNorm16, false}; return true;
|
||||
case TextureInternalFormat::RG16Snorm: out = {2, ShadowComponent::SNorm16, false}; return true;
|
||||
case TextureInternalFormat::RGB16Snorm: out = {3, ShadowComponent::SNorm16, false}; return true;
|
||||
case TextureInternalFormat::RGBA16Snorm: out = {4, ShadowComponent::SNorm16, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R16F: out = {1, ShadowComponent::Half, false}; return true;
|
||||
case TextureInternalFormat::RG16F: out = {2, ShadowComponent::Half, false}; return true;
|
||||
case TextureInternalFormat::RGB16F: out = {3, ShadowComponent::Half, false}; return true;
|
||||
case TextureInternalFormat::RGBA16F: out = {4, ShadowComponent::Half, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R32F: out = {1, ShadowComponent::Float32, false}; return true;
|
||||
case TextureInternalFormat::RG32F: out = {2, ShadowComponent::Float32, false}; return true;
|
||||
case TextureInternalFormat::RGB32F: out = {3, ShadowComponent::Float32, false}; return true;
|
||||
case TextureInternalFormat::RGBA32F: out = {4, ShadowComponent::Float32, false}; return true;
|
||||
|
||||
case TextureInternalFormat::R8UI: out = {1, ShadowComponent::UInt8, true}; return true;
|
||||
case TextureInternalFormat::RG8UI: out = {2, ShadowComponent::UInt8, true}; return true;
|
||||
case TextureInternalFormat::RGB8UI: out = {3, ShadowComponent::UInt8, true}; return true;
|
||||
case TextureInternalFormat::RGBA8UI: out = {4, ShadowComponent::UInt8, true}; return true;
|
||||
|
||||
case TextureInternalFormat::R8I: out = {1, ShadowComponent::Int8, true}; return true;
|
||||
case TextureInternalFormat::RG8I: out = {2, ShadowComponent::Int8, true}; return true;
|
||||
case TextureInternalFormat::RGB8I: out = {3, ShadowComponent::Int8, true}; return true;
|
||||
case TextureInternalFormat::RGBA8I: out = {4, ShadowComponent::Int8, true}; return true;
|
||||
|
||||
case TextureInternalFormat::R16UI: out = {1, ShadowComponent::UInt16, true}; return true;
|
||||
case TextureInternalFormat::RG16UI: out = {2, ShadowComponent::UInt16, true}; return true;
|
||||
case TextureInternalFormat::RGB16UI: out = {3, ShadowComponent::UInt16, true}; return true;
|
||||
case TextureInternalFormat::RGBA16UI: out = {4, ShadowComponent::UInt16, true}; return true;
|
||||
|
||||
case TextureInternalFormat::R16I: out = {1, ShadowComponent::Int16, true}; return true;
|
||||
case TextureInternalFormat::RG16I: out = {2, ShadowComponent::Int16, true}; return true;
|
||||
case TextureInternalFormat::RGB16I: out = {3, ShadowComponent::Int16, true}; return true;
|
||||
case TextureInternalFormat::RGBA16I: out = {4, ShadowComponent::Int16, true}; return true;
|
||||
|
||||
case TextureInternalFormat::R32UI: out = {1, ShadowComponent::UInt32, true}; return true;
|
||||
case TextureInternalFormat::RG32UI: out = {2, ShadowComponent::UInt32, true}; return true;
|
||||
case TextureInternalFormat::RGB32UI: out = {3, ShadowComponent::UInt32, true}; return true;
|
||||
case TextureInternalFormat::RGBA32UI: out = {4, ShadowComponent::UInt32, true}; return true;
|
||||
|
||||
case TextureInternalFormat::R32I: out = {1, ShadowComponent::Int32, true}; return true;
|
||||
case TextureInternalFormat::RG32I: out = {2, ShadowComponent::Int32, true}; return true;
|
||||
case TextureInternalFormat::RGB32I: out = {3, ShadowComponent::Int32, true}; return true;
|
||||
case TextureInternalFormat::RGBA32I: out = {4, ShadowComponent::Int32, true}; return true;
|
||||
|
||||
default:
|
||||
// Packed internal layouts (RGB10A2, RGB9E5, ...), depth/stencil and unsized formats
|
||||
// have no component-array shadow layout (packed ones are handled below).
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Packed internal formats whose shadow bytes hold the ES upload word directly
|
||||
// (GL_UNSIGNED_INT_2_10_10_10_REV / 5_9_9_9_REV / 10F_11F_11F_REV encoding, 4 bytes/texel).
|
||||
enum class PackedInternalKind {
|
||||
UNorm2101010Rev, // GL_RGB10_A2
|
||||
UInt2101010Rev, // GL_RGB10_A2UI
|
||||
FloatR11G11B10, // GL_R11F_G11F_B10F
|
||||
FloatRGB9E5, // GL_RGB9_E5
|
||||
};
|
||||
|
||||
struct InternalPackedLayout {
|
||||
PackedInternalKind kind;
|
||||
Int channelCount;
|
||||
Bool isInteger;
|
||||
};
|
||||
|
||||
Bool GetInternalPackedLayout(TextureInternalFormat internal, InternalPackedLayout& out) {
|
||||
switch (internal) {
|
||||
case TextureInternalFormat::RGB10A2:
|
||||
out = {PackedInternalKind::UNorm2101010Rev, 4, false};
|
||||
return true;
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
out = {PackedInternalKind::UInt2101010Rev, 4, true};
|
||||
return true;
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
out = {PackedInternalKind::FloatR11G11B10, 3, false};
|
||||
return true;
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
out = {PackedInternalKind::FloatRGB9E5, 3, false};
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 EncodePackedInternalWordFloat(PackedInternalKind kind, const Float rgba[4]) {
|
||||
switch (kind) {
|
||||
case PackedInternalKind::UNorm2101010Rev: {
|
||||
const auto field = [](Float v, Uint32 maxValue) {
|
||||
return static_cast<Uint32>(std::llround(std::clamp(v, 0.0f, 1.0f) * static_cast<Float>(maxValue)));
|
||||
};
|
||||
return field(rgba[0], 1023u) | (field(rgba[1], 1023u) << 10) | (field(rgba[2], 1023u) << 20) |
|
||||
(field(rgba[3], 3u) << 30);
|
||||
}
|
||||
case PackedInternalKind::FloatR11G11B10:
|
||||
return EncodeFloatToUnsignedF11(rgba[0]) | (EncodeFloatToUnsignedF11(rgba[1]) << 11) |
|
||||
(EncodeFloatToUnsignedF10(rgba[2]) << 22);
|
||||
case PackedInternalKind::FloatRGB9E5:
|
||||
return EncodeSharedExponentRGB9E5(rgba);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 EncodePackedInternalWordInt(PackedInternalKind kind, const Int64 rgba[4]) {
|
||||
if (kind != PackedInternalKind::UInt2101010Rev) {
|
||||
return 0;
|
||||
}
|
||||
const auto field = [](Int64 v, Int64 maxValue) {
|
||||
return static_cast<Uint32>(std::clamp<Int64>(v, 0, maxValue));
|
||||
};
|
||||
return field(rgba[0], 1023) | (field(rgba[1], 1023) << 10) | (field(rgba[2], 1023) << 20) |
|
||||
(field(rgba[3], 3) << 30);
|
||||
}
|
||||
|
||||
struct UnpackChannelMapping {
|
||||
Int formatPosition[4]; // position of R,G,B,A within the input format's component list; -1 = missing
|
||||
Int channelCount;
|
||||
Bool isInteger;
|
||||
};
|
||||
|
||||
Bool GetUnpackChannelMapping(TextureInputFormat format, UnpackChannelMapping& out) {
|
||||
switch (format) {
|
||||
case TextureInputFormat::Red: out = {{0, -1, -1, -1}, 1, false}; return true;
|
||||
case TextureInputFormat::RInteger: out = {{0, -1, -1, -1}, 1, true}; return true;
|
||||
case TextureInputFormat::Green: out = {{-1, 0, -1, -1}, 1, false}; return true;
|
||||
case TextureInputFormat::GreenInteger: out = {{-1, 0, -1, -1}, 1, true}; return true;
|
||||
case TextureInputFormat::Blue: out = {{-1, -1, 0, -1}, 1, false}; return true;
|
||||
case TextureInputFormat::BlueInteger: out = {{-1, -1, 0, -1}, 1, true}; return true;
|
||||
case TextureInputFormat::Alpha: out = {{-1, -1, -1, 0}, 1, false}; return true;
|
||||
case TextureInputFormat::AlphaInteger: out = {{-1, -1, -1, 0}, 1, true}; return true;
|
||||
case TextureInputFormat::RG: out = {{0, 1, -1, -1}, 2, false}; return true;
|
||||
case TextureInputFormat::RGInteger: out = {{0, 1, -1, -1}, 2, true}; return true;
|
||||
case TextureInputFormat::RGB: out = {{0, 1, 2, -1}, 3, false}; return true;
|
||||
case TextureInputFormat::RGBInteger: out = {{0, 1, 2, -1}, 3, true}; return true;
|
||||
case TextureInputFormat::BGR: out = {{2, 1, 0, -1}, 3, false}; return true;
|
||||
case TextureInputFormat::BGRInteger: out = {{2, 1, 0, -1}, 3, true}; return true;
|
||||
case TextureInputFormat::RGBA: out = {{0, 1, 2, 3}, 4, false}; return true;
|
||||
case TextureInputFormat::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true;
|
||||
case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; return true;
|
||||
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
||||
default:
|
||||
return false; // depth / stencil / unknown
|
||||
}
|
||||
}
|
||||
|
||||
struct PackedTypeLayout {
|
||||
Int fieldCount;
|
||||
Int width[4]; // bit width of each format component, in component order
|
||||
Int totalBits;
|
||||
Bool reversed; // *_REV: the first format component sits in the least significant bits
|
||||
};
|
||||
|
||||
Bool GetPackedTypeLayout(TexturePixelDataType type, PackedTypeLayout& out) {
|
||||
switch (type) {
|
||||
case TexturePixelDataType::UnsignedByte332: out = {3, {3, 3, 2, 0}, 8, false}; return true;
|
||||
case TexturePixelDataType::UnsignedByte233Rev: out = {3, {3, 3, 2, 0}, 8, true}; return true;
|
||||
case TexturePixelDataType::UnsignedShort565: out = {3, {5, 6, 5, 0}, 16, false}; return true;
|
||||
case TexturePixelDataType::UnsignedShort565Rev: out = {3, {5, 6, 5, 0}, 16, true}; return true;
|
||||
case TexturePixelDataType::UnsignedShort4444: out = {4, {4, 4, 4, 4}, 16, false}; return true;
|
||||
case TexturePixelDataType::UnsignedShort4444Rev: out = {4, {4, 4, 4, 4}, 16, true}; return true;
|
||||
case TexturePixelDataType::UnsignedShort5551: out = {4, {5, 5, 5, 1}, 16, false}; return true;
|
||||
case TexturePixelDataType::UnsignedShort1555Rev: out = {4, {5, 5, 5, 1}, 16, true}; return true;
|
||||
case TexturePixelDataType::UnsignedInt8888: out = {4, {8, 8, 8, 8}, 32, false}; return true;
|
||||
case TexturePixelDataType::UnsignedInt8888Rev: out = {4, {8, 8, 8, 8}, 32, true}; return true;
|
||||
case TexturePixelDataType::UnsignedInt1010102: out = {4, {10, 10, 10, 2}, 32, false}; return true;
|
||||
case TexturePixelDataType::UnsignedInt2101010Rev: out = {4, {10, 10, 10, 2}, 32, true}; return true;
|
||||
default:
|
||||
return false; // shared-exponent / packed-float / depth-stencil types stay on the legacy path
|
||||
}
|
||||
}
|
||||
|
||||
// Base data types whose in-memory encoding equals a shadow component encoding (fast-path check).
|
||||
Bool GetDirectShadowComponentForType(TexturePixelDataType type, Bool isInteger, ShadowComponent& out) {
|
||||
switch (type) {
|
||||
case TexturePixelDataType::UnsignedByte:
|
||||
out = isInteger ? ShadowComponent::UInt8 : ShadowComponent::UNorm8;
|
||||
return true;
|
||||
case TexturePixelDataType::Byte:
|
||||
out = isInteger ? ShadowComponent::Int8 : ShadowComponent::SNorm8;
|
||||
return true;
|
||||
case TexturePixelDataType::UnsignedShort:
|
||||
out = isInteger ? ShadowComponent::UInt16 : ShadowComponent::UNorm16;
|
||||
return true;
|
||||
case TexturePixelDataType::Short:
|
||||
out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16;
|
||||
return true;
|
||||
case TexturePixelDataType::UnsignedInt:
|
||||
if (!isInteger) return false; // no 32-bit normalized shadow layout
|
||||
out = ShadowComponent::UInt32;
|
||||
return true;
|
||||
case TexturePixelDataType::Int:
|
||||
if (!isInteger) return false;
|
||||
out = ShadowComponent::Int32;
|
||||
return true;
|
||||
case TexturePixelDataType::HalfFloat:
|
||||
if (isInteger) return false;
|
||||
out = ShadowComponent::Half;
|
||||
return true;
|
||||
case TexturePixelDataType::Float:
|
||||
if (isInteger) return false;
|
||||
out = ShadowComponent::Float32;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsIdentityChannelOrder(const UnpackChannelMapping& mapping) {
|
||||
for (Int i = 0; i < 4; ++i) {
|
||||
const Int expected = i < mapping.channelCount ? i : -1;
|
||||
if (mapping.formatPosition[i] != expected) return false;
|
||||
if (inputFormat == TextureInputFormat::BGRA) {
|
||||
if (inputDataType == TexturePixelDataType::UnsignedInt8888) {
|
||||
swizzle = {TextureSwizzleParam::Green, TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha,
|
||||
TextureSwizzleParam::Red};
|
||||
} else {
|
||||
swizzle = {TextureSwizzleParam::Blue, TextureSwizzleParam::Green, TextureSwizzleParam::Red,
|
||||
TextureSwizzleParam::Alpha};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct UnpackConversionSpec {
|
||||
UnpackChannelMapping mapping;
|
||||
InternalShadowLayout internal;
|
||||
PackedTypeLayout packed;
|
||||
Bool isPacked;
|
||||
TexturePixelDataType type;
|
||||
SizeT inputPixelSize;
|
||||
SizeT swapGroupSize; // UNPACK_SWAP_BYTES group: packed word size, or the component size
|
||||
SizeT internalPixelSize;
|
||||
Bool internalIsPacked;
|
||||
InternalPackedLayout internalPacked;
|
||||
};
|
||||
|
||||
// Returns true when the (format, type) -> internal-format upload needs a per-texel conversion;
|
||||
// returns false both for layouts that already match the shadow bytes (memcpy fast path) and for
|
||||
// combinations the converter does not support (legacy copy behavior).
|
||||
Bool GetUnpackConversionSpec(TextureInternalFormat internal, TextureInputFormat format,
|
||||
TexturePixelDataType type, UnpackConversionSpec& out) {
|
||||
InternalShadowLayout layout{};
|
||||
InternalPackedLayout packedInternal{};
|
||||
const Bool hasComponentLayout = GetInternalShadowLayout(internal, layout);
|
||||
const Bool hasPackedInternal = !hasComponentLayout && GetInternalPackedLayout(internal, packedInternal);
|
||||
if (!hasComponentLayout && !hasPackedInternal) return false;
|
||||
const Bool internalIsInteger = hasComponentLayout ? layout.isInteger : packedInternal.isInteger;
|
||||
const Int internalChannelCount = hasComponentLayout ? layout.channelCount : packedInternal.channelCount;
|
||||
|
||||
UnpackChannelMapping mapping{};
|
||||
if (!GetUnpackChannelMapping(format, mapping)) return false;
|
||||
if (mapping.isInteger != internalIsInteger) return false; // rejected upstream; stay safe
|
||||
|
||||
PackedTypeLayout packed{};
|
||||
const Bool isPacked = GetPackedTypeLayout(type, packed);
|
||||
if (isPacked) {
|
||||
if (packed.fieldCount != mapping.channelCount) return false;
|
||||
// Byte layout already equals the RGBA8 shadow layout on little-endian.
|
||||
if (internal == TextureInternalFormat::RGBA8 && format == TextureInputFormat::RGBA &&
|
||||
type == TexturePixelDataType::UnsignedInt8888Rev) {
|
||||
return false;
|
||||
}
|
||||
// The client word already equals the packed internal word (memcpy fast path).
|
||||
if (hasPackedInternal && type == TexturePixelDataType::UnsignedInt2101010Rev &&
|
||||
(format == TextureInputFormat::RGBA || format == TextureInputFormat::RGBAInteger) &&
|
||||
(packedInternal.kind == PackedInternalKind::UNorm2101010Rev ||
|
||||
packedInternal.kind == PackedInternalKind::UInt2101010Rev)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
ShadowComponent direct{};
|
||||
const Bool hasDirect = GetDirectShadowComponentForType(type, mapping.isInteger, direct);
|
||||
switch (type) {
|
||||
case TexturePixelDataType::UnsignedByte:
|
||||
case TexturePixelDataType::Byte:
|
||||
case TexturePixelDataType::UnsignedShort:
|
||||
case TexturePixelDataType::Short:
|
||||
case TexturePixelDataType::UnsignedInt:
|
||||
case TexturePixelDataType::Int:
|
||||
break;
|
||||
case TexturePixelDataType::Float:
|
||||
case TexturePixelDataType::HalfFloat:
|
||||
if (mapping.isInteger) return false; // rejected upstream
|
||||
break;
|
||||
case TexturePixelDataType::UnsignedInt5999Rev:
|
||||
case TexturePixelDataType::UnsignedInt101111Rev:
|
||||
// Packed-float RGB source words (decoded in ConvertUnpackRow); only pair with
|
||||
// GL_RGB, which the state layer already enforces.
|
||||
if (mapping.isInteger || mapping.channelCount != 3) return false;
|
||||
// The client word already equals the packed internal word.
|
||||
if (hasPackedInternal &&
|
||||
((packedInternal.kind == PackedInternalKind::FloatRGB9E5 &&
|
||||
type == TexturePixelDataType::UnsignedInt5999Rev) ||
|
||||
(packedInternal.kind == PackedInternalKind::FloatR11G11B10 &&
|
||||
type == TexturePixelDataType::UnsignedInt101111Rev))) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (hasComponentLayout && hasDirect && direct == layout.component &&
|
||||
mapping.channelCount == layout.channelCount && IsIdentityChannelOrder(mapping)) {
|
||||
return false; // input already matches the shadow layout
|
||||
}
|
||||
}
|
||||
|
||||
out.mapping = mapping;
|
||||
out.internal = hasComponentLayout
|
||||
? layout
|
||||
: InternalShadowLayout{internalChannelCount, ShadowComponent::UNorm8, internalIsInteger};
|
||||
out.packed = packed;
|
||||
out.isPacked = isPacked;
|
||||
out.type = type;
|
||||
out.inputPixelSize = GetInputBytesPerPixel(format, type);
|
||||
const Bool isPackedFloatWord = type == TexturePixelDataType::UnsignedInt5999Rev ||
|
||||
type == TexturePixelDataType::UnsignedInt101111Rev;
|
||||
out.swapGroupSize = isPacked ? static_cast<SizeT>(packed.totalBits / 8)
|
||||
: (isPackedFloatWord ? 4 : GetBaseTexturePixelDataTypeSize(type));
|
||||
out.internalIsPacked = hasPackedInternal;
|
||||
out.internalPacked = packedInternal;
|
||||
out.internalPixelSize =
|
||||
hasPackedInternal ? 4
|
||||
: static_cast<SizeT>(layout.channelCount) * GetShadowComponentSize(layout.component);
|
||||
return true;
|
||||
}
|
||||
|
||||
Float DecodeComponentToFloat(const Uint8* p, TexturePixelDataType type) {
|
||||
switch (type) {
|
||||
case TexturePixelDataType::UnsignedByte:
|
||||
return static_cast<Float>(*p) / 255.0f;
|
||||
case TexturePixelDataType::Byte: {
|
||||
Int8 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return std::max(static_cast<Float>(v) / 127.0f, -1.0f);
|
||||
}
|
||||
case TexturePixelDataType::UnsignedShort: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(v) / 65535.0f;
|
||||
}
|
||||
case TexturePixelDataType::Short: {
|
||||
Int16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return std::max(static_cast<Float>(v) / 32767.0f, -1.0f);
|
||||
}
|
||||
case TexturePixelDataType::UnsignedInt: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(static_cast<Double>(v) / 4294967295.0);
|
||||
}
|
||||
case TexturePixelDataType::Int: {
|
||||
Int32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(std::max(static_cast<Double>(v) / 2147483647.0, -1.0));
|
||||
}
|
||||
case TexturePixelDataType::HalfFloat: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return DecodeHalfBitsToFloat(v);
|
||||
}
|
||||
case TexturePixelDataType::Float: {
|
||||
Float v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
Int64 DecodeComponentToInt(const Uint8* p, TexturePixelDataType type) {
|
||||
switch (type) {
|
||||
case TexturePixelDataType::UnsignedByte:
|
||||
return *p;
|
||||
case TexturePixelDataType::Byte: {
|
||||
Int8 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case TexturePixelDataType::UnsignedShort: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case TexturePixelDataType::Short: {
|
||||
Int16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case TexturePixelDataType::UnsignedInt: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case TexturePixelDataType::Int: {
|
||||
Int32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 ReadPackedWord(const Uint8* p, Int totalBits) {
|
||||
switch (totalBits) {
|
||||
case 8:
|
||||
return *p;
|
||||
case 16: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
default: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 ExtractPackedField(Uint32 word, const PackedTypeLayout& packed, Int position, Int& outWidth) {
|
||||
Int shift;
|
||||
if (packed.reversed) {
|
||||
shift = 0;
|
||||
for (Int i = 0; i < position; ++i) shift += packed.width[i];
|
||||
} else {
|
||||
shift = packed.totalBits;
|
||||
for (Int i = 0; i <= position; ++i) shift -= packed.width[i];
|
||||
}
|
||||
outWidth = packed.width[position];
|
||||
const Uint32 mask = (1u << outWidth) - 1u;
|
||||
return (word >> shift) & mask;
|
||||
}
|
||||
|
||||
void EncodeShadowComponentFloat(Uint8* dst, ShadowComponent component, Float v) {
|
||||
switch (component) {
|
||||
case ShadowComponent::UNorm8: {
|
||||
const auto out = static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::SNorm8: {
|
||||
const auto out = static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::UNorm16: {
|
||||
const auto out = static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::SNorm16: {
|
||||
const auto out = static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::Half: {
|
||||
const Uint16 out = EncodeFloatToHalfBits(v);
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::Float32:
|
||||
Memcpy(dst, &v, sizeof(v));
|
||||
break;
|
||||
default:
|
||||
break; // integer components never reach the float encoder
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeShadowComponentInt(Uint8* dst, ShadowComponent component, Int64 v) {
|
||||
switch (component) {
|
||||
case ShadowComponent::UInt8: {
|
||||
const auto out = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::Int8: {
|
||||
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::UInt16: {
|
||||
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::Int16: {
|
||||
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::UInt32: {
|
||||
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
case ShadowComponent::Int32: {
|
||||
const auto out = static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
|
||||
Memcpy(dst, &out, sizeof(out));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break; // float components never reach the integer encoder
|
||||
}
|
||||
}
|
||||
|
||||
void ConvertUnpackRow(const Uint8* src, Uint8* dst, SizeT pixelCount, const UnpackConversionSpec& conv) {
|
||||
const SizeT dstComponentSize = GetShadowComponentSize(conv.internal.component);
|
||||
const SizeT srcComponentSize = conv.isPacked ? 0 : GetBaseTexturePixelDataTypeSize(conv.type);
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
const Uint8* s = src + i * conv.inputPixelSize;
|
||||
Uint8* d = dst + i * conv.internalPixelSize;
|
||||
if (conv.internal.isInteger) {
|
||||
Int64 rgba[4] = {0, 0, 0, 1};
|
||||
if (conv.isPacked) {
|
||||
const Uint32 word = ReadPackedWord(s, conv.packed.totalBits);
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
const Int pos = conv.mapping.formatPosition[ch];
|
||||
if (pos < 0) continue;
|
||||
Int width = 0;
|
||||
rgba[ch] = ExtractPackedField(word, conv.packed, pos, width);
|
||||
}
|
||||
} else {
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
const Int pos = conv.mapping.formatPosition[ch];
|
||||
if (pos < 0) continue;
|
||||
rgba[ch] = DecodeComponentToInt(s + static_cast<SizeT>(pos) * srcComponentSize, conv.type);
|
||||
}
|
||||
}
|
||||
if (conv.internalIsPacked) {
|
||||
const Uint32 word = EncodePackedInternalWordInt(conv.internalPacked.kind, rgba);
|
||||
Memcpy(d, &word, sizeof(word));
|
||||
continue;
|
||||
}
|
||||
for (Int ch = 0; ch < conv.internal.channelCount; ++ch) {
|
||||
EncodeShadowComponentInt(d + static_cast<SizeT>(ch) * dstComponentSize,
|
||||
conv.internal.component, rgba[ch]);
|
||||
}
|
||||
} else {
|
||||
Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
if (conv.type == TexturePixelDataType::UnsignedInt5999Rev ||
|
||||
conv.type == TexturePixelDataType::UnsignedInt101111Rev) {
|
||||
// Packed-float RGB source word: decode the shared-exponent / small-float fields.
|
||||
Uint32 word;
|
||||
Memcpy(&word, s, sizeof(word));
|
||||
Float comps[3];
|
||||
if (conv.type == TexturePixelDataType::UnsignedInt5999Rev) {
|
||||
DecodeSharedExponentRGB9E5(word, comps);
|
||||
} else {
|
||||
comps[0] = DecodeUnsignedF11ToFloat(word & 0x7FFu);
|
||||
comps[1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu);
|
||||
comps[2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu);
|
||||
}
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
const Int pos = conv.mapping.formatPosition[ch];
|
||||
if (pos < 0 || pos >= 3) continue;
|
||||
rgba[ch] = comps[pos];
|
||||
}
|
||||
} else if (conv.isPacked) {
|
||||
const Uint32 word = ReadPackedWord(s, conv.packed.totalBits);
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
const Int pos = conv.mapping.formatPosition[ch];
|
||||
if (pos < 0) continue;
|
||||
Int width = 0;
|
||||
const Uint32 field = ExtractPackedField(word, conv.packed, pos, width);
|
||||
rgba[ch] = static_cast<Float>(field) / static_cast<Float>((1u << width) - 1u);
|
||||
}
|
||||
} else {
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
const Int pos = conv.mapping.formatPosition[ch];
|
||||
if (pos < 0) continue;
|
||||
rgba[ch] =
|
||||
DecodeComponentToFloat(s + static_cast<SizeT>(pos) * srcComponentSize, conv.type);
|
||||
}
|
||||
}
|
||||
if (conv.internalIsPacked) {
|
||||
const Uint32 word = EncodePackedInternalWordFloat(conv.internalPacked.kind, rgba);
|
||||
Memcpy(d, &word, sizeof(word));
|
||||
continue;
|
||||
}
|
||||
for (Int ch = 0; ch < conv.internal.channelCount; ++ch) {
|
||||
EncodeShadowComponentFloat(d + static_cast<SizeT>(ch) * dstComponentSize,
|
||||
conv.internal.component, rgba[ch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
return false;
|
||||
}
|
||||
|
||||
// assume 8 bit per channel
|
||||
// swizzle.size() == channel count
|
||||
@@ -770,12 +123,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
const Int effectiveWidth = (params.RowLength > 0) ? params.RowLength : width;
|
||||
const Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height;
|
||||
const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment);
|
||||
|
||||
UnpackConversionSpec conversion{};
|
||||
const Bool needConversion =
|
||||
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
|
||||
const SizeT outputPixelSize = needConversion ? conversion.internalPixelSize : pixelSize;
|
||||
const SizeT outputRowStride = static_cast<SizeT>(width) * outputPixelSize;
|
||||
const SizeT outputRowStride = static_cast<SizeT>(width) * pixelSize;
|
||||
|
||||
const Int startX = params.SkipPixels;
|
||||
const Int startY = params.SkipRows;
|
||||
@@ -785,16 +133,15 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
const Int copyHeight = height;
|
||||
const Int copyDepth = depth;
|
||||
|
||||
MGLOG_D("%s: start at: (%d, %d, %d), copy size: (%d, %d, %d), i/o row stride: (%d, %dx%d), convert: %d",
|
||||
__func__, startX, startY, startZ, copyWidth, copyHeight, copyDepth, inputRowStride, width,
|
||||
outputPixelSize, needConversion ? 1 : 0);
|
||||
MGLOG_D("%s: start at: (%d, %d, %d), copy size: (%d, %d, %d), i/o row stride: (%d, %dx%d)", __func__, startX,
|
||||
startY, startZ, copyWidth, copyHeight, copyDepth, inputRowStride, width, pixelSize);
|
||||
|
||||
if (copyWidth <= 0 || copyHeight <= 0 || copyDepth <= 0) {
|
||||
outSize = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
outSize = static_cast<SizeT>(copyWidth) * copyHeight * copyDepth * outputPixelSize;
|
||||
outSize = static_cast<SizeT>(copyWidth) * copyHeight * copyDepth * pixelSize;
|
||||
void* outputPixels = malloc(outSize);
|
||||
if (!outputPixels) {
|
||||
outSize = 0;
|
||||
@@ -808,49 +155,38 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
src += static_cast<SizeT>(startY) * inputRowStride;
|
||||
src += static_cast<SizeT>(startX) * pixelSize;
|
||||
|
||||
const Bool isByteType =
|
||||
Bool isByteType =
|
||||
(inputDataType == TexturePixelDataType::UnsignedByte || inputDataType == TexturePixelDataType::Byte);
|
||||
// UNPACK_SWAP_BYTES applies to the input elements (packed word / component) before conversion.
|
||||
const Bool conversionSwapsBytes = needConversion && params.SwapBytes && conversion.swapGroupSize > 1;
|
||||
Vector<Uint8> swapScratch;
|
||||
if (conversionSwapsBytes) {
|
||||
swapScratch.resize(static_cast<SizeT>(copyWidth) * pixelSize);
|
||||
}
|
||||
Vector<TextureSwizzleParam> colorSwizzle;
|
||||
const Bool needColorSwizzle =
|
||||
targetInternalFormat == TextureInternalFormat::RGBA8 &&
|
||||
GetRgba8ByteSwizzleForUnpack(textureInputFormat, inputDataType, colorSwizzle);
|
||||
for (Int z = 0; z < copyDepth; ++z) {
|
||||
const Uint8* layerSrc = src;
|
||||
Uint8* layerDst = dst;
|
||||
|
||||
for (Int y = 0; y < copyHeight; ++y) {
|
||||
if (needConversion) {
|
||||
const Uint8* rowSrc = layerSrc;
|
||||
if (conversionSwapsBytes) {
|
||||
Memcpy(swapScratch.data(), layerSrc, static_cast<SizeT>(copyWidth) * pixelSize);
|
||||
const SizeT groupCount = static_cast<SizeT>(copyWidth) * pixelSize / conversion.swapGroupSize;
|
||||
SwapBytes(swapScratch.data(), conversion.swapGroupSize, groupCount);
|
||||
rowSrc = swapScratch.data();
|
||||
}
|
||||
ConvertUnpackRow(rowSrc, layerDst, static_cast<SizeT>(copyWidth), conversion);
|
||||
} else {
|
||||
Memcpy(layerDst, layerSrc, static_cast<SizeT>(copyWidth) * pixelSize);
|
||||
Memcpy(layerDst, layerSrc, static_cast<SizeT>(copyWidth) * pixelSize);
|
||||
|
||||
if (params.SwapBytes && !isByteType) {
|
||||
// GL_UNPACK_SWAP_BYTES swaps within each element (component or packed
|
||||
// word), never across a whole multi-component pixel.
|
||||
SizeT swapGroup = GetSizedTexturePixelDataTypeSize(inputDataType);
|
||||
if (swapGroup == 0) swapGroup = GetBaseTexturePixelDataTypeSize(inputDataType);
|
||||
if (swapGroup > 1) {
|
||||
MGLOG_D("%s: SwapBytes (group %d)", __func__, static_cast<Int>(swapGroup));
|
||||
SwapBytes(layerDst, swapGroup,
|
||||
static_cast<SizeT>(copyWidth) * pixelSize / swapGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.LSBFirst && isBitmap) {
|
||||
MGLOG_D("%s: LSBFirst", __func__);
|
||||
ProcessLSBFirst(layerDst, static_cast<SizeT>(copyWidth), 1);
|
||||
}
|
||||
if (params.SwapBytes && pixelSize > 1 && !isByteType) {
|
||||
MGLOG_D("%s: SwapBytes", __func__);
|
||||
SwapBytes(layerDst, pixelSize, static_cast<SizeT>(copyWidth));
|
||||
}
|
||||
|
||||
if (params.LSBFirst && isBitmap) {
|
||||
MGLOG_D("%s: LSBFirst", __func__);
|
||||
ProcessLSBFirst(layerDst, static_cast<SizeT>(copyWidth), 1);
|
||||
}
|
||||
|
||||
if (needColorSwizzle) {
|
||||
MGLOG_D("%s: Swizzle RGBA8 unpack", __func__);
|
||||
// MGLOG_D("%s: pixel0 before = %x", __func__, *((Uint32*)layerDst));
|
||||
ProcessColorSwizzle(layerDst, static_cast<SizeT>(copyWidth), colorSwizzle);
|
||||
// MGLOG_D("%s: pixel0 after = %x", __func__, *((Uint32*)layerDst));
|
||||
}
|
||||
// else
|
||||
// MGLOG_D("%s: pixel0 = %x", __func__, *((Uint32*)layerDst));
|
||||
|
||||
layerSrc += inputRowStride;
|
||||
layerDst += outputRowStride;
|
||||
}
|
||||
@@ -941,177 +277,4 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
|
||||
return outputPixels;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Float DecodeShadowComponentToFloat(const Uint8* p, ShadowComponent component) {
|
||||
switch (component) {
|
||||
case ShadowComponent::UNorm8:
|
||||
return static_cast<Float>(*p) / 255.0f;
|
||||
case ShadowComponent::SNorm8: {
|
||||
Int8 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return std::max(static_cast<Float>(v) / 127.0f, -1.0f);
|
||||
}
|
||||
case ShadowComponent::UNorm16: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return static_cast<Float>(v) / 65535.0f;
|
||||
}
|
||||
case ShadowComponent::SNorm16: {
|
||||
Int16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return std::max(static_cast<Float>(v) / 32767.0f, -1.0f);
|
||||
}
|
||||
case ShadowComponent::Half: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return DecodeHalfBitsToFloat(v);
|
||||
}
|
||||
case ShadowComponent::Float32: {
|
||||
Float v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
Int64 DecodeShadowComponentToInt(const Uint8* p, ShadowComponent component) {
|
||||
switch (component) {
|
||||
case ShadowComponent::UInt8:
|
||||
return *p;
|
||||
case ShadowComponent::Int8: {
|
||||
Int8 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::UInt16: {
|
||||
Uint16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::Int16: {
|
||||
Int16 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::UInt32: {
|
||||
Uint32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
case ShadowComponent::Int32: {
|
||||
Int32 v;
|
||||
Memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount,
|
||||
Vector<Uint8>& outWide, Bool& outIsInteger, Bool& outIsSigned) {
|
||||
if (!src) return false;
|
||||
const Uint8* srcBytes = static_cast<const Uint8*>(src);
|
||||
|
||||
InternalShadowLayout layout{};
|
||||
if (GetInternalShadowLayout(internalFormat, layout)) {
|
||||
const SizeT componentSize = GetShadowComponentSize(layout.component);
|
||||
const SizeT srcPixelSize = static_cast<SizeT>(layout.channelCount) * componentSize;
|
||||
outIsInteger = layout.isInteger;
|
||||
outIsSigned = layout.component == ShadowComponent::Int8 || layout.component == ShadowComponent::Int16 ||
|
||||
layout.component == ShadowComponent::Int32;
|
||||
outWide.resize(pixelCount * 16);
|
||||
if (layout.isInteger) {
|
||||
auto* dst = reinterpret_cast<Uint32*>(outWide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
const Uint8* s = srcBytes + i * srcPixelSize;
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
Int64 v = ch == 3 ? 1 : 0;
|
||||
if (ch < layout.channelCount) {
|
||||
v = DecodeShadowComponentToInt(s + static_cast<SizeT>(ch) * componentSize,
|
||||
layout.component);
|
||||
}
|
||||
if (outIsSigned) {
|
||||
const auto out = static_cast<Int32>(v);
|
||||
Memcpy(&dst[i * 4 + ch], &out, sizeof(out));
|
||||
} else {
|
||||
dst[i * 4 + ch] = static_cast<Uint32>(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto* dst = reinterpret_cast<Float*>(outWide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
const Uint8* s = srcBytes + i * srcPixelSize;
|
||||
for (Int ch = 0; ch < 4; ++ch) {
|
||||
Float v = ch == 3 ? 1.0f : 0.0f;
|
||||
if (ch < layout.channelCount) {
|
||||
v = DecodeShadowComponentToFloat(s + static_cast<SizeT>(ch) * componentSize,
|
||||
layout.component);
|
||||
}
|
||||
dst[i * 4 + ch] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
InternalPackedLayout packedInternal{};
|
||||
if (GetInternalPackedLayout(internalFormat, packedInternal)) {
|
||||
outIsInteger = packedInternal.isInteger;
|
||||
outIsSigned = false;
|
||||
outWide.resize(pixelCount * 16);
|
||||
if (packedInternal.isInteger) {
|
||||
auto* dst = reinterpret_cast<Uint32*>(outWide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
Uint32 word;
|
||||
Memcpy(&word, srcBytes + i * 4, sizeof(word));
|
||||
dst[i * 4 + 0] = word & 0x3FFu;
|
||||
dst[i * 4 + 1] = (word >> 10) & 0x3FFu;
|
||||
dst[i * 4 + 2] = (word >> 20) & 0x3FFu;
|
||||
dst[i * 4 + 3] = (word >> 30) & 0x3u;
|
||||
}
|
||||
} else {
|
||||
auto* dst = reinterpret_cast<Float*>(outWide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
Uint32 word;
|
||||
Memcpy(&word, srcBytes + i * 4, sizeof(word));
|
||||
switch (packedInternal.kind) {
|
||||
case PackedInternalKind::UNorm2101010Rev:
|
||||
dst[i * 4 + 0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
||||
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
||||
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
||||
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
||||
break;
|
||||
case PackedInternalKind::FloatR11G11B10:
|
||||
dst[i * 4 + 0] = DecodeUnsignedF11ToFloat(word & 0x7FFu);
|
||||
dst[i * 4 + 1] = DecodeUnsignedF11ToFloat((word >> 11) & 0x7FFu);
|
||||
dst[i * 4 + 2] = DecodeUnsignedF10ToFloat((word >> 22) & 0x3FFu);
|
||||
dst[i * 4 + 3] = 1.0f;
|
||||
break;
|
||||
case PackedInternalKind::FloatRGB9E5: {
|
||||
Float rgb[3];
|
||||
DecodeSharedExponentRGB9E5(word, rgb);
|
||||
dst[i * 4 + 0] = rgb[0];
|
||||
dst[i * 4 + 1] = rgb[1];
|
||||
dst[i * 4 + 2] = rgb[2];
|
||||
dst[i * 4 + 3] = 1.0f;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
dst[i * 4 + 0] = dst[i * 4 + 1] = dst[i * 4 + 2] = 0.0f;
|
||||
dst[i * 4 + 3] = 1.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::PixelStoreProcessor
|
||||
|
||||
@@ -22,12 +22,4 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
TextureInputFormat dstInputFormat, TexturePixelDataType dstDataType,
|
||||
IntVec3 dimension, Bool isBitmap, SizeT& outSize);
|
||||
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle);
|
||||
|
||||
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
|
||||
// readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with
|
||||
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set
|
||||
// outIsInteger (outIsSigned tells signed from unsigned). Missing channels read 0 (G/B) and
|
||||
// 1 / 1.0f (A). Returns false when the format has no canonical shadow layout.
|
||||
Bool DecodeShadowDataToWideRGBA(TextureInternalFormat internalFormat, const void* src, SizeT pixelCount,
|
||||
Vector<Uint8>& outWide, Bool& outIsInteger, Bool& outIsSigned);
|
||||
} // namespace MobileGL::MG_Util::PixelStoreProcessor
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user