mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3fac5e9fa | ||
|
|
3823fe528c | ||
|
|
08124c99f1 | ||
|
|
f973008c4d | ||
|
|
7030d307be | ||
|
|
853c5f1264 | ||
|
|
c14283ee2d | ||
|
|
9dccabf03c | ||
|
|
53fa16c2c2 | ||
|
|
c2de6576ad | ||
|
|
9c32d9fa68 | ||
|
|
4d3d677a7f | ||
|
|
d20c2d01ae | ||
|
|
21c7e8cd63 | ||
|
|
9524c688ca | ||
|
|
c60639db1d | ||
|
|
b1d60523e4 | ||
|
|
8966d5c294 | ||
|
|
568090f09f |
+27
-157
@@ -6,24 +6,8 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
- codex/fix-github-ci
|
||||
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
|
||||
# workflow keeps running for docs: it hosts the doc citation lint.
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
|
||||
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -37,7 +21,6 @@ jobs:
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -55,9 +38,6 @@ jobs:
|
||||
distribution: zulu
|
||||
java-version: '17'
|
||||
|
||||
- name: Android replay lifecycle regression
|
||||
run: python3 tools/trace_replay/test_android_lifecycle.py
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
@@ -81,8 +61,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -97,13 +75,7 @@ jobs:
|
||||
run: python update_glslang_sources.py
|
||||
|
||||
- name: Build plugin APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -145,14 +117,7 @@ jobs:
|
||||
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
|
||||
|
||||
- name: Build retrace APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
|
||||
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -234,8 +199,6 @@ jobs:
|
||||
name: trace case matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
outputs:
|
||||
android: ${{ steps.trace-cases.outputs.android }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
@@ -246,12 +209,7 @@ jobs:
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: |
|
||||
python3 tools/trace_replay/trace_cases.py --self-test-transport-matrices
|
||||
matrix_format=github-apk-matrix
|
||||
if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then
|
||||
matrix_format=github-apk-transport-matrix
|
||||
fi
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format "${matrix_format}")" >> "$GITHUB_OUTPUT"
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
@@ -335,8 +293,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -364,7 +320,7 @@ jobs:
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
retrace:
|
||||
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
|
||||
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
@@ -374,8 +330,6 @@ jobs:
|
||||
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||
timeout-minutes: 75
|
||||
env:
|
||||
MOBILEGL_CI_TRANSPORT: ${{ matrix.transport || 'monolith' }}
|
||||
MOBILEGL_CI_LANE: ${{ matrix.lane || 'monolith-control' }}
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
@@ -385,7 +339,7 @@ jobs:
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
steps:
|
||||
- name: Set swap space
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 8
|
||||
@@ -414,8 +368,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -454,7 +406,7 @@ jobs:
|
||||
--profile pixel_6 \
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
- name: Launch emulator
|
||||
- name: Launch Emulator
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh start \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
@@ -475,31 +427,6 @@ jobs:
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
extra_retrace_args=()
|
||||
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--require-inproc
|
||||
)
|
||||
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
|
||||
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
|
||||
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
|
||||
# environment by the launcher and read by the client, which is where they belong.
|
||||
#
|
||||
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
|
||||
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
|
||||
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
|
||||
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
|
||||
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
|
||||
# into the CLIENT's process and SetWindowHandle is refused on the way across with
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--use-pbuffer
|
||||
--require-spawn
|
||||
)
|
||||
else
|
||||
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
|
||||
fi
|
||||
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
||||
# min-filter downgrading on ANGLE llvmpipe.
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
|
||||
@@ -581,12 +508,6 @@ jobs:
|
||||
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
|
||||
mkdir -p "${result_dir}"
|
||||
python3 - "${result_dir}/lane.json" <<'PY'
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
Path(sys.argv[1]).write_text(json.dumps({"lane": os.environ["MOBILEGL_CI_LANE"],
|
||||
"transport": os.environ["MOBILEGL_CI_TRANSPORT"]}, indent=2))
|
||||
PY
|
||||
if [ -s "${{ matrix.case.golden }}" ]; then
|
||||
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
|
||||
fi
|
||||
@@ -611,7 +532,7 @@ jobs:
|
||||
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
|
||||
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
|
||||
|
||||
- name: Stop emulator
|
||||
- name: Stop Emulator
|
||||
if: always()
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
@@ -623,7 +544,7 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
path: android-retrace-result/**
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -632,8 +553,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: retrace
|
||||
if: always()
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -648,73 +567,25 @@ jobs:
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Download Android monolith control results
|
||||
- name: Download Android retrace results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
|
||||
path: retrace-artifacts/monolith-control
|
||||
pattern: MobileGL-android-retrace-result-*
|
||||
path: retrace-artifacts
|
||||
|
||||
- name: Download Android inproc acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-*
|
||||
path: retrace-artifacts/inproc-acceptance
|
||||
|
||||
- name: Download Android spawn acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
|
||||
path: retrace-artifacts/spawn-acceptance
|
||||
|
||||
- name: Render monolith control summary
|
||||
- name: Render retrace summary
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/monolith-control \
|
||||
--output-dir android-retrace-summary/monolith-control \
|
||||
--title "MobileGL Android monolith control" \
|
||||
--group-label "Android Emulator / monolith control" \
|
||||
--html mobilegl-android-monolith-control.html
|
||||
--input retrace-artifacts \
|
||||
--output-dir android-retrace-summary \
|
||||
--title "MobileGL Android retrace overview" \
|
||||
--group-label "Android Emulator" \
|
||||
--html mobilegl-android-retrace-overview.html
|
||||
|
||||
- name: Render inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/inproc-acceptance \
|
||||
--output-dir android-retrace-summary/inproc-acceptance \
|
||||
--title "MobileGL Android inproc acceptance" \
|
||||
--group-label "Android Emulator / inproc acceptance" \
|
||||
--html mobilegl-android-inproc-acceptance.html
|
||||
|
||||
- name: Render spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
|
||||
|
||||
- name: Upload Android monolith control summary
|
||||
- name: Upload Android retrace summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-monolith-control-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
|
||||
path: android-retrace-summary/mobilegl-android-retrace-overview.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -730,19 +601,15 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fail before deletion if either paginated inventory cannot be read.
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
|
||||
> "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv' \
|
||||
> "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
declare -A failed_cases=()
|
||||
while IFS= read -r job_name; do
|
||||
case_name="${job_name#retrace (*, }"
|
||||
case_name="${case_name%)}"
|
||||
failed_cases["${case_name}"]=1
|
||||
done < "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
|
||||
)
|
||||
|
||||
if ((${#failed_cases[@]})); then
|
||||
echo "Retaining fixtures and results for failed retrace case(s):"
|
||||
@@ -785,6 +652,9 @@ jobs:
|
||||
echo "Deleting ${artifact_name} (${artifact_id})"
|
||||
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
||||
((deleted += 1))
|
||||
done < "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
|
||||
)
|
||||
|
||||
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
|
||||
|
||||
+39
-2969
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,3 @@
|
||||
[submodule "include/ska"]
|
||||
path = include/ska
|
||||
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||
[submodule "3rdparty/flatbuffers"]
|
||||
path = 3rdparty/flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
|
||||
Vendored
-1
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
-391
@@ -14,33 +14,6 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
|
||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
|
||||
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
|
||||
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
|
||||
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
|
||||
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
|
||||
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
|
||||
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
|
||||
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
|
||||
# only make sense when the two roles share an address space. It is a SUPERSET, never a
|
||||
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
|
||||
# only decides whether the shims are compiled in.
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
|
||||
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||
# the whole migration window; it retires with the pull path itself at P13.
|
||||
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||
|
||||
@@ -185,61 +158,15 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
# Generate MGGitHash.h
|
||||
#
|
||||
# RESULT_VARIABLE, WHICH THIS CALL DID NOT HAVE (CONTRACT-P6 4.2). Without it a failed
|
||||
# `git rev-parse` yields "" and nothing says so. Verified live: building this worktree from WSL
|
||||
# produces an EMPTY hash, because the worktree's `.git` is a file holding a WINDOWS path that
|
||||
# WSL's git cannot resolve - so two builds from different commits got IDENTICAL ABI
|
||||
# fingerprints and the stamp silently stopped doing its documented job.
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE MOBILEGL_GIT_STAMP_RESULT
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT)
|
||||
|
||||
# --- the BUILD STAMP, and why it is a second pair rather than a fix to the first -------------
|
||||
#
|
||||
# GIT_COMMIT_HASH_SHORT MAY NOT MOVE. It is baked into glGetString(GL_VERSION)
|
||||
# (MG_Impl/GLImpl/Getter/GL_Getter.cpp), so redefining it changes an APPLICATION-VISIBLE GL
|
||||
# string and the pull build's .rodata - a behaviour leak and a G1 break at once. The handshake
|
||||
# gets its own pair instead, consumed only by the ABI fingerprint, which no pull build compiles.
|
||||
#
|
||||
# PRESENT IS A FIRST-CLASS INPUT, not the emptiness of VALUE. "this build has no stamp" and
|
||||
# "this build's stamp is the empty string" are different claims, and a fingerprint that could
|
||||
# not tell them apart is how two unrelated builds agreed in the first place.
|
||||
#
|
||||
# -DMOBILEGL_BUILD_STAMP=<anything> (or the environment variable) overrides, because a tree git
|
||||
# cannot read is not always a tree whose commit is unknown - CI has ${GITHUB_SHA} in hand.
|
||||
set(MOBILEGL_BUILD_STAMP "" CACHE STRING "Explicit ABI-fingerprint build stamp when git cannot be read")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "")
|
||||
if(NOT "${MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${MOBILEGL_BUILD_STAMP}")
|
||||
elseif(NOT "$ENV{MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "$ENV{MOBILEGL_BUILD_STAMP}")
|
||||
elseif(MOBILEGL_GIT_STAMP_RESULT EQUAL 0 AND NOT "${GIT_COMMIT_HASH_FULL}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${GIT_COMMIT_HASH_FULL}")
|
||||
endif()
|
||||
|
||||
if("${MOBILEGL_BUILD_STAMP_VALUE}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 0)
|
||||
# A WARNING AND NOT A FATAL: a build with no stamp is still a usable build, it just cannot
|
||||
# prove that a peer was built from the same tree. Saying so once at configure time is the
|
||||
# difference between a known limitation and the silent one 4.2 found.
|
||||
message(WARNING
|
||||
"MobileGL: no build stamp. `git rev-parse HEAD` failed (result "
|
||||
"'${MOBILEGL_GIT_STAMP_RESULT}') and MOBILEGL_BUILD_STAMP was not set, so the ABI "
|
||||
"fingerprint cannot distinguish two builds of the same sizes from different commits - "
|
||||
"which is exactly what a stale MOBILEGL_IPC_SERVER_PATH looks like. Pass "
|
||||
"-DMOBILEGL_BUILD_STAMP=<commit> to restore it.")
|
||||
else()
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 1)
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Util/Miscellany/MGGitHash.h.in
|
||||
${CMAKE_BINARY_DIR}/generated/MGGitHash.h
|
||||
@@ -311,8 +238,6 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/PipeStats.cpp
|
||||
|
||||
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
|
||||
@@ -493,180 +418,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MG_Remote (disaggregated transport). Everything below is gated: with the
|
||||
# option OFF not one file here is compiled and no include path is added.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
|
||||
# variable, not a forced cache write, for the reason the two implications below give: an
|
||||
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
|
||||
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
|
||||
# with the transport shadowed off would compile a role-isolation shim over no roles.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
|
||||
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED ON)
|
||||
endif()
|
||||
|
||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||
# a checkout without the submodule must configure and build, just without the
|
||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||
# lines later. Note this only checks for the RUNTIME headers - flatc is never
|
||||
# built here (see scripts/gen_protocol.py).
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
|
||||
message(WARNING
|
||||
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
|
||||
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
|
||||
"disaggregated shape for this configure; the cached ON takes effect once the "
|
||||
"submodule is present.")
|
||||
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
|
||||
# made the plain re-configure after `git submodule update` stay OFF with no message at
|
||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||
# was, so the next configure - with the submodule there - honours it.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
|
||||
# would still be defined over a build with no MG_Remote in it at all.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
|
||||
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
|
||||
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
|
||||
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
|
||||
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
|
||||
# at the CMake level from a legitimate transport-only build, which is why it is stated here
|
||||
# rather than left to whoever hits the link error. Same normal-variable form as the two
|
||||
# implications above.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
|
||||
"this configure: the split path decodes into the MGPipe applier, and the "
|
||||
"applier is what MOBILEGL_PIPE_PUSH compiles")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
|
||||
# cache write, for the same reason as the disaggregated fallback above.
|
||||
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||
"configure: with nothing pushed it is the only arm there is")
|
||||
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Transport/ShmLink.cpp
|
||||
MobileGL/MG_Remote/Transport/LinkMetrics.cpp
|
||||
MobileGL/MG_Remote/Transport/StreamLink.cpp
|
||||
MobileGL/MG_Remote/Transport/Ring.cpp
|
||||
MobileGL/MG_Remote/Transport/Doorbell.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegment.cpp
|
||||
# Both platform halves are listed unconditionally and each is empty on
|
||||
# the other OS, so neither can rot behind an `if (WIN32)` nobody
|
||||
# configures.
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
|
||||
MobileGL/MG_Remote/Transport/FdPassing.cpp
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/SocketTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/ControlInbox.cpp
|
||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||
# ---- P5: the three new directories ------------------------------
|
||||
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
|
||||
# var-tails in and out of SEG_STAGE. [w1]
|
||||
# Client/ the emitting role: session, the 69-slot emit table, the
|
||||
# caps mirror. [c1]
|
||||
# Server/ the applying role: session, the applier bridge onto the
|
||||
# existing MGPipeApply* free functions, the apply thread. [v1]
|
||||
#
|
||||
# Every file below lands in P5 as a HEADER plus a .cpp of named
|
||||
# Fatal stubs, so that all seven P5 packages compile and link on day
|
||||
# one against signatures that cannot then move under them. A stub is
|
||||
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
|
||||
# emitter that returns quietly is how a split lane runs monolith and
|
||||
# goes green (ARCHITECTURE.md 10.3).
|
||||
MobileGL/MG_Remote/CapsCodec.cpp
|
||||
# P6 dl: the one place a session dies - Session::Fail's funnel (Fatal-family aware,
|
||||
# publishes SessionFault, one telemetry point).
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
|
||||
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
|
||||
# P5 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
|
||||
# emitters installed over the two generated tables, which is what stops every
|
||||
# resource/CSO/texture/program record executing synchronously on the GL thread under
|
||||
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
|
||||
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
|
||||
MobileGL/MG_Remote/Client/WireTables.cpp
|
||||
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
MobileGL/MG_Remote/Client/GpuWritePending.cpp
|
||||
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSpawn.cpp
|
||||
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
|
||||
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
|
||||
MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp
|
||||
MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp
|
||||
# P12 (on-screen server window): the process-wide holder of the server's own window
|
||||
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
|
||||
# ServerOwned request is refused by name there).
|
||||
MobileGL/MG_Remote/Server/ServerDisplay.cpp
|
||||
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
|
||||
# arms and the probe that measures the first against the second, shared by the
|
||||
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
|
||||
# so it stays out of the pull build (gate G1).
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -681,14 +432,6 @@ if (ANDROID)
|
||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
|
||||
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
|
||||
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Server/DisplayServerJni.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
@@ -725,29 +468,11 @@ set(MOBILEGL_COMPILE_DEF
|
||||
-DASIO_NO_DEPRECATED
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/MobileGL
|
||||
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
|
||||
# line above too; this entry lets the client, the backends and MG_Remote spell them
|
||||
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
|
||||
${spirv-tools_SOURCE_DIR}
|
||||
${spirv-tools_SOURCE_DIR}/include
|
||||
${spirv-tools_BINARY_DIR}
|
||||
@@ -758,13 +483,6 @@ set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# Header-only runtime: an include path, no add_subdirectory, no link
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
@@ -981,112 +699,3 @@ endif()
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
|
||||
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
|
||||
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
|
||||
# so a second executable has to be built with an .so name and exec'd out of
|
||||
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
|
||||
# chain end to end: it is packaged like a library, exec'd from the app's own
|
||||
# untrusted_app process, and writes a marker the parent reads back.
|
||||
#
|
||||
# Off by default and ANDROID-only, so no shipping configuration builds it. The
|
||||
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
# P6 `sm`: the REAL server image. Unlike MOBILEGL_BUILD_SERVER_SPIKE below - the
|
||||
# P0 spike, which links no MobileGL library and exists only to prove the Android
|
||||
# delivery chain - this one links the library and runs mobilegl_server_main.
|
||||
#
|
||||
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
|
||||
# dlsym's the entry point, so one shared library serves both roles. On a desktop
|
||||
# configure we link it directly instead: there is no APK packaging rule to satisfy
|
||||
# and a direct link is what makes an undefined symbol a BUILD error rather than a
|
||||
# runtime dlsym failure in a child whose stderr is /dev/null.
|
||||
#
|
||||
# Android keeps the lib*.so naming and the nativeLibraryDir output the spike
|
||||
# proved is the only exec-able location in an APK.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT WIN32)
|
||||
add_executable(MobileGLServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Remote/Server/ServerEntry.cpp)
|
||||
|
||||
target_include_directories(MobileGLServer PRIVATE
|
||||
${MOBILEGL_INCLUDE_DIR})
|
||||
|
||||
target_link_libraries(MobileGLServer PRIVATE ${CMAKE_PROJECT_NAME})
|
||||
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServer PRIVATE -pie)
|
||||
|
||||
# $ORIGIN, OR THE SERVER CANNOT START AT ALL ON ANDROID.
|
||||
#
|
||||
# This executable links libMobileGL.so, and it is EXEC'd rather than dlopen'd - so it gets a
|
||||
# fresh linker namespace with the default search path, not the app's. An APK's lib/<abi>/ is
|
||||
# not on that path, and the failure is the bluntest one there is:
|
||||
#
|
||||
# F linker: CANNOT LINK EXECUTABLE ".../lib/arm64/libMobileGLServer.so":
|
||||
# library "libMobileGL.so" not found: needed by main executable
|
||||
#
|
||||
# It cost a device run to find because it is invisible on desktop: CMake gives build-tree
|
||||
# targets an absolute RPATH automatically, so the WSL server resolved its dependency through a
|
||||
# path that does not exist on a phone. $ORIGIN says "beside me", which is true in the APK, true
|
||||
# in the build tree, and true wherever somebody copies the pair - and it is what makes a
|
||||
# deployment that is NOT a build tree work at all, which the end state is.
|
||||
# THE PROPERTIES, NOT target_link_options. A raw `-Wl,-rpath,$ORIGIN` reaches the generator
|
||||
# unquoted and the SHELL expands $ORIGIN to nothing - measured on device, where the linker
|
||||
# said `normalize_path - invalid input: "$"` and then failed to find libMobileGL.so anyway.
|
||||
# BUILD_RPATH / INSTALL_RPATH are the supported spelling and CMake escapes them correctly.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
|
||||
if (ANDROID)
|
||||
# An APK's only exec-able install location is lib/<abi>/, and the packager
|
||||
# only puts a file there if it is named lib*.so. P0 spike A proved this
|
||||
# works from the app's own untrusted_app domain on two devices.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer")
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
else()
|
||||
# Desktop: the name the dladdr fallback looks for beside libMobileGL.so.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
# Renamed by P6 sm: the real server took the name it had been holding.
|
||||
add_executable(MobileGLServerSpike
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
|
||||
|
||||
# An executable that is named like a shared library still has to be a real
|
||||
# PIE executable: Android has refused non-PIE executables since API 21, and
|
||||
# the name alone does not change what the loader demands of the file.
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServerSpike"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServerSpike PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServerSpike PRIVATE -pie)
|
||||
|
||||
# AGP packages what the external native build drops into the per-ABI output
|
||||
# directory, and it selects by the .so extension. CMake puts executables in
|
||||
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
|
||||
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
|
||||
# the library directory when the generator gave us one.
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+13
-378
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -132,6 +132,18 @@ namespace MobileGL::MG_Config {
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
|
||||
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
|
||||
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
|
||||
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
|
||||
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
|
||||
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
|
||||
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
|
||||
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
|
||||
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
|
||||
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
|
||||
Uint32 MagmaDescriptorTrimFrames = 120;
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
@@ -316,383 +328,6 @@ namespace MobileGL::MG_Config {
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
|
||||
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
|
||||
// is the "everything before P5e" control P5e's A/B is run against - each phase's
|
||||
// constant survives as the next phase's control, which is why none of them is ever
|
||||
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
|
||||
// bits are listed here (MG_Pipe/MGPipe.h owns them):
|
||||
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
||||
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||
// set_texture_params) - requires 0x80 AND 0x800
|
||||
// (the built-in sampler CSO a set_texture_params record names is minted by
|
||||
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
|
||||
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
|
||||
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
|
||||
// classes and the dirty bits 15/16/17) - requires 0x80
|
||||
// (every MGPBufferRange::Res names a Buffer handle; P5e)
|
||||
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
|
||||
// family runs its legacy arm; it is never half-run.
|
||||
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||
// never probed. The negative control the CSO design is measured against.
|
||||
Uint64 PipePush = 0;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
// counts instead, for triage and for the lane that must survive to read its own
|
||||
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
|
||||
// off.
|
||||
Bool PipeVerifyFatal = true;
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
|
||||
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
|
||||
// green verify run goes red naming it (negative control A). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipeVerifyCorrupt;
|
||||
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
|
||||
// the value) of that field for that verb, an omission indistinguishable from a
|
||||
// forgotten FillPoints.def row, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
// per-draw staging, counted separately in the stats.
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
// the log whenever PipeStats is on, so a device run needs no writable path.
|
||||
String PipeStatsFile;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
|
||||
//
|
||||
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
|
||||
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
|
||||
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
|
||||
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
|
||||
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
|
||||
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
|
||||
// behind their own #if.
|
||||
//
|
||||
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
|
||||
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
|
||||
// parser below does not exist to complain about it, and putting a complaint in the
|
||||
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
|
||||
// shape of "the split lane ran monolith and went green", so the gate against it is a
|
||||
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
|
||||
// P5 adds beside build-linux-verify).
|
||||
enum class TransportMode : Uint8 {
|
||||
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
|
||||
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
|
||||
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
|
||||
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
|
||||
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
|
||||
// transport in is not the same as using it, and every existing lane of a build-split
|
||||
// must keep running monolith unless it is asked for one.
|
||||
extern TransportMode Transport;
|
||||
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
|
||||
extern String TransportEndpoint;
|
||||
|
||||
// P7 F1. DID **THIS PROCESS'S OWN CONFIGURATION** ASK FOR A SPLIT TRANSPORT.
|
||||
//
|
||||
// `Transport` alone cannot answer that, and the difference is exactly the one the F1 gate
|
||||
// turns on. A unit fixture assigns `Transport` by hand to put the code under test on its
|
||||
// split arm (ServerLoopTest's main does, and says why) while building no client at all; a
|
||||
// real run gets it from MG_ConfigLoader::Init(), and that run WILL bring a client half up.
|
||||
// Only the second may be held to "never decide a record family's fate from a placeholder
|
||||
// caps mirror" - in the first there is no handshake, no mirror to adopt and nothing the
|
||||
// rule could mean.
|
||||
//
|
||||
// Written once, by InitTransport(), and never cleared: a process does not stop having been
|
||||
// configured. Read by MG_Remote::Client::CapsMirror::RequireFirstSnapshot.
|
||||
extern Bool SplitTransportRequestedByConfig;
|
||||
|
||||
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
|
||||
// for the G1 reason above and because every field here is meaningless without the
|
||||
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
|
||||
//
|
||||
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
|
||||
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
|
||||
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
|
||||
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
|
||||
// to P6 or later.
|
||||
//
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE's two values. See IpcTable::Surface.
|
||||
enum class IpcSurface : Uint8 {
|
||||
Offscreen = 0, // a window surface is the client's own window, as before (the default)
|
||||
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
|
||||
};
|
||||
struct IpcTable {
|
||||
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
|
||||
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
|
||||
// SPLIT variant already carry it, and an environment variable that nothing parses is
|
||||
// indistinguishable from one that is parsed and ignored.
|
||||
String ServerPath;
|
||||
// Control endpoint and data-plane selection are independent of topology.
|
||||
String Control = "fork"; // fork | unix:<path> | tcp://host:port
|
||||
String Data = "auto"; // auto | shm | stream
|
||||
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
|
||||
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
|
||||
// codec publish a max-record-bytes counter rather than assume that is enough.
|
||||
Uint32 RingMb = 8;
|
||||
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
|
||||
// here ONE BLOB AT A TIME: a row whose content can outgrow the segment cuts it at the
|
||||
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
|
||||
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
|
||||
Uint32 StageMb = 32;
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
|
||||
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
|
||||
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
|
||||
// Stores no command names are destroyed at once and stores whose last submission has
|
||||
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
|
||||
// exceed it after a sweep, the server flushes what it has recorded and waits for it
|
||||
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
|
||||
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
|
||||
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
|
||||
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
|
||||
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
|
||||
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
|
||||
// and MagmaWireReclaimScenario's watermark cases must go red.
|
||||
Uint32 WireDeferredMb = 64;
|
||||
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
|
||||
Uint32 SpinUs = 50;
|
||||
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
|
||||
// reverse-channel event that finds SEG_EVENT full under run-ahead. The apply thread
|
||||
// publishes, rings the client and parks until the client drains enough room; a client
|
||||
// that has not made room when this runs out - it stopped draining (NotDraining) or it
|
||||
// drains a slot at a time (TooSlow) - FORFEITS the reverse channel: that event and every
|
||||
// later one is dropped and counted (eventDropped), and the session stops by the ordinary
|
||||
// stop path rather than by Fatal{EventRingOverflow}. A peer that goes away (PeerGone) or
|
||||
// a session that is stopped (Stopped) ends the wait at once instead, so the knob may
|
||||
// exceed ServerLoop::Stop()'s 5000 ms join. It bounds the whole reservation, not one
|
||||
// park, so a trickling peer cannot stretch it - nor can a shm peer that writes the
|
||||
// (peer-writable) eventRingFull latch to 0 itself, though that one keeps the apply
|
||||
// thread spinning rather than parked until the budget runs out. Was a 30000 ms constant
|
||||
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
|
||||
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
|
||||
Uint32 EventWaitMs = 2000;
|
||||
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||
Uint32 PersistentBlockKb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
|
||||
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
|
||||
// range every verb. 0 restores the whole-range push (A/B control).
|
||||
Uint32 PersistentHashSuppress = 1;
|
||||
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
|
||||
// with no reply slot) are published without waiting for their own apply; the barrier
|
||||
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
|
||||
// every reply-slot row), which is the only place BARRIER-PULLED fields are read. 0
|
||||
// restores the per-record barrier of R-1.
|
||||
Uint32 BatchWaits = 1;
|
||||
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
|
||||
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
|
||||
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
|
||||
// answer is taken when the window fills.
|
||||
//
|
||||
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
|
||||
// measured reasons and one structural one, in the order they were found:
|
||||
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
|
||||
// client emits on the order of a thousand records - so the answer was always gone
|
||||
// (`want=310` on every miss while the ring spanned [1175..1504]);
|
||||
// 2. a residency budget that tried to take answers back early could not help, because
|
||||
// what displaces an answer is what the SERVER sends, not what the client emits;
|
||||
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
|
||||
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
|
||||
//
|
||||
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
|
||||
// frame-sent identical per op throughout):
|
||||
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
|
||||
// window 2 2,500 17.27 s
|
||||
// window 4 1,644 11.02 s
|
||||
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
|
||||
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
|
||||
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
|
||||
//
|
||||
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
|
||||
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
|
||||
// now; the old 4 was derived from a reply pool the window does not run on). Three
|
||||
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
|
||||
//
|
||||
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
|
||||
// window 5 1,277 11.31 s (10.61-12.12)
|
||||
// window 6 1,155 10.79 s ( 9.69-10.81)
|
||||
// window 8 859 10.96 s ( 8.92-11.03)
|
||||
//
|
||||
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
|
||||
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
|
||||
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
|
||||
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
|
||||
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
|
||||
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
|
||||
// the constant ~350-400 creates that block for a reason depth does not reach, and the
|
||||
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
|
||||
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
|
||||
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
|
||||
// flag keyed on wantReply would disable the very mechanism it exists to enable
|
||||
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
|
||||
Uint32 CreateWindow = 10;
|
||||
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||
// negative control has a spelling the day P11 writes it.
|
||||
Uint32 AdoptTier = 2;
|
||||
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
|
||||
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
|
||||
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
|
||||
// GLContext by the client's residual fill and a free-running queue lets the server
|
||||
// read a FUTURE value of them.
|
||||
Uint32 VerbBarrier = 1;
|
||||
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
|
||||
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
|
||||
// It is one half of a conjunction and never a switch on its own - the client arms
|
||||
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
|
||||
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
|
||||
// saying so.
|
||||
//
|
||||
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
|
||||
// every record are identical on both arms, and the only difference is whether the
|
||||
// client waits. That is what makes "is the picture the same" a question about the wait
|
||||
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
|
||||
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
|
||||
// the first barriered row's stale pull.
|
||||
//
|
||||
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
|
||||
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
|
||||
// arm that stops filling it.
|
||||
Uint32 RunAhead = 1;
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
|
||||
// have in flight before it waits for a swap to come back. 1 = the client publishes
|
||||
// frame N+1's records while the server applies and swaps frame N - one frame of
|
||||
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
|
||||
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
|
||||
// client that is already CPU-bound and costs a frame of latency, which is why the
|
||||
// default is 1 and not "as deep as the ring".
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
|
||||
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
|
||||
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
|
||||
// latch decides dead (device lost) from alive-but-silent (a named diagnostic). The
|
||||
// contract named this knob from P6 on; the client hard-coded its default until P7.
|
||||
Uint32 ControlTimeoutMs = 5000;
|
||||
// MOBILEGL_IPC_COLD_START_MS (P7): the same wait for the three ops a server may bring its
|
||||
// NATIVE backend up inside - CreatePbufferSurface, CreateWindowSurface, MakeCurrent -
|
||||
// until the session's first MakeCurrent is answered ok. The bring-up is lazy (Espryt's
|
||||
// eglInitialize, Magma's Vulkan instance and device), ~100 ms on a workstation and more
|
||||
// than the steady 5 s on a loaded CI runner (retrace-split spawn legs, runs 35671704873
|
||||
// and 35706183230). Never shorter than CONTROL_TIMEOUT_MS; its expiry is the same
|
||||
// non-fatal named answer. The default is the spawn connect budget, for the same reason:
|
||||
// "the server has to create a backend, and a cold software rasteriser is not fast".
|
||||
Uint32 ColdStartMs = 20000;
|
||||
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
|
||||
// build, the seven sticky forwards that are otherwise exempt - from "count it in
|
||||
// rsp" to Fatal (R-7.3).
|
||||
Bool StrictErrors = false;
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
|
||||
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
|
||||
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
|
||||
// only because the two roles share one object turns into a named
|
||||
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
|
||||
// Meaningless under monolith transport (the two roles are one thread there, so the
|
||||
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
|
||||
// (the comparator owns the one fill block it compares against). 0 is not merely the
|
||||
// default, it is the negative control: the lane's distinctness case must go red
|
||||
// without it (P5F §6).
|
||||
Bool RoleSplitState = false;
|
||||
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||
Bool Audit = false;
|
||||
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
|
||||
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
|
||||
// because the resolved mask is logged by whoever starts the apply thread, and the
|
||||
// string is what an operator typed.
|
||||
String ServerAffinity = "auto";
|
||||
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
|
||||
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
|
||||
// any native window including NULL, send ONE CreateWindowSurface naming
|
||||
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
|
||||
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
|
||||
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
|
||||
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
|
||||
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
|
||||
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
|
||||
IpcSurface Surface = IpcSurface::Offscreen;
|
||||
};
|
||||
extern IpcTable Ipc;
|
||||
|
||||
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
|
||||
// surface is the server's when the knob says so AND there is a remote server to own it.
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+3
-329
@@ -7,20 +7,9 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Config.h"
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
|
||||
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||
// pull build's translation unit is unchanged.
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
|
||||
// unchanged (G1).
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
@@ -30,16 +19,6 @@ namespace MobileGL::MG_Config {
|
||||
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
|
||||
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
|
||||
FeaturesTable Features;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
|
||||
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
|
||||
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
|
||||
// and there is nothing to define.
|
||||
TransportMode Transport = TransportMode::Monolith;
|
||||
String TransportEndpoint;
|
||||
Bool SplitTransportRequestedByConfig = false;
|
||||
IpcTable Ipc;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
namespace MobileGL::MG_ConfigLoader {
|
||||
@@ -180,38 +159,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return static_cast<Uint32>(parsedValue);
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
|
||||
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
|
||||
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
|
||||
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
|
||||
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
|
||||
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const String& value = it->second;
|
||||
const char* text = value.c_str();
|
||||
int base = 10;
|
||||
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
text += 2;
|
||||
base = 16;
|
||||
}
|
||||
char* parseEnd = nullptr;
|
||||
errno = 0;
|
||||
const bool negative = value.find('-') != String::npos;
|
||||
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
|
||||
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
|
||||
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
|
||||
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return static_cast<Uint64>(parsedValue);
|
||||
}
|
||||
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
@@ -230,6 +177,9 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.MagmaMaxDrawsPerCommandBuffer =
|
||||
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
|
||||
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
@@ -260,40 +210,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
|
||||
#else
|
||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||
// "pull everything" and that stays literally true.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||
#endif
|
||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||
features.PipeVerifyFatal =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
@@ -312,254 +228,12 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
#undef ENTRY
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
|
||||
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
|
||||
// one fallback that names what it did instead. The two prefixed forms are the only
|
||||
// reason this is not literally that function's ENTRY macro.
|
||||
//
|
||||
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
|
||||
// rather than silent, because the failure this avoids is a P6 lane that set
|
||||
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
|
||||
// The mode is left at Monolith so nothing half-initializes.
|
||||
inline void InitTransport() {
|
||||
String value;
|
||||
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||
String lowered = value;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
MG_Config::TransportEndpoint.clear();
|
||||
if (lowered.empty() || lowered == "monolith") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
if (lowered == "inproc") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
|
||||
// one, and NOT on the two that name a transport and then stay monolith - a process
|
||||
// that was refused its transport brings no client half up either.
|
||||
MG_Config::SplitTransportRequestedByConfig = true;
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread");
|
||||
return;
|
||||
}
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||
// rather than "unknown", which are different bugs on the operator's side.
|
||||
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
|
||||
// rendezvous of its own and connects to it - the two processes are
|
||||
// independent, so this is `Dial = Connect` with a launcher attached
|
||||
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
|
||||
if (lowered == "spawn") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Spawn;
|
||||
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
|
||||
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
|
||||
// run_trace_case.cmake asserts on a distinctive sentence from this
|
||||
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
|
||||
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
|
||||
// every MOBILEGL_* in EVERY build including the pull one - so the
|
||||
// bare KEY=VALUE is satisfied by a monolith library that parsed
|
||||
// nothing (review M-5). A spawn arm with no line of its own could
|
||||
// only have been asserted with the inproc marker, which spawn never
|
||||
// emits: the arm would have been red for a missing sentence rather
|
||||
// than green for a working transport.
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread in ANOTHER PROCESS");
|
||||
return;
|
||||
}
|
||||
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
|
||||
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
|
||||
// ELSE is listening on needs the server lifecycle to be somebody
|
||||
// else's too, which is P12's. Named, never a silent degrade.
|
||||
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
|
||||
"staying on monolith. This run is NOT a split run.",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
|
||||
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
|
||||
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
|
||||
// how an A/B stops being one, and the ranges below are the diagnostics.
|
||||
inline void InitIpc() {
|
||||
auto& ipc = MG_Config::Ipc;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
|
||||
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
|
||||
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
|
||||
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
|
||||
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
|
||||
// create_shader_state archive already needs far more than a toy ring. The ceilings
|
||||
// are sanity, not policy.
|
||||
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
|
||||
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
|
||||
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
|
||||
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
|
||||
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
|
||||
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
|
||||
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
|
||||
// patience at all would forfeit a healthy run-ahead client the first time its ring
|
||||
// filled between two of the client's drains, which is an ordinary backlog.
|
||||
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
|
||||
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||
// Whole-range push is the 0 arm; with it on (default) only blocks whose
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
|
||||
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
|
||||
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
|
||||
// was inert before, the device numbers, and the sweep that put the default at 6 rather
|
||||
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
|
||||
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
|
||||
// declared answers, and the device sweep showed the wall clock still falling at the old
|
||||
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
|
||||
// StreamLink's crossing log are the checks on this number.
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
// shadow comparer is armed.
|
||||
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
|
||||
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
|
||||
// point of use, which is where the "P11" in the message belongs.
|
||||
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
|
||||
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
|
||||
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
|
||||
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
|
||||
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
|
||||
// run-ahead's whole point is that the client stops filling gPipeInputs for an
|
||||
// unbarriered record, and the comparator has nothing left to compare when it does.
|
||||
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
|
||||
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
|
||||
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
|
||||
// admitted: a credit of zero would mean "publish no present at all".
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
|
||||
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
|
||||
// Both are floored at 100 ms: a zero bound would declare every live server silent.
|
||||
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
|
||||
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
|
||||
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
|
||||
// verify harness: the comparator's entry compare and compare-at-read hook are built on
|
||||
// there being ONE filled block (the hook pins itself to &gPipeInputs,
|
||||
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
|
||||
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
|
||||
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
|
||||
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
|
||||
"the dual-block rehearsal is OFF for this run");
|
||||
ipc.RoleSplitState = false;
|
||||
}
|
||||
|
||||
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
|
||||
//
|
||||
// The device-lost latch is deliberately one-way - a session whose server died has
|
||||
// nothing to recover into, because every handle the client minted names an object in a
|
||||
// process that no longer exists. Re-pushing the world onto a fresh server is the work
|
||||
// this knob would turn on, and no stage has written it.
|
||||
//
|
||||
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
|
||||
// the whole IPC family: an environment variable nothing consumes is indistinguishable
|
||||
// from one that is consumed and does nothing, and an operator who set this one would
|
||||
// otherwise conclude that recovery had been tried and had not helped.
|
||||
{
|
||||
String respawn;
|
||||
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
|
||||
if (!respawn.empty() && respawn != "0") {
|
||||
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
|
||||
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
|
||||
"this client minted names an object inside the server process, so a new "
|
||||
"server would have to be re-pushed the entire world before a single verb "
|
||||
"could land. This run will latch device-lost and stay there.",
|
||||
respawn.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
|
||||
// the semantics). An unknown value is named and read as the default, like every knob here.
|
||||
{
|
||||
String surface;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
|
||||
std::transform(surface.begin(), surface.end(), surface.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (surface.empty() || surface == "offscreen") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
// (Not said by a server process: a spawned server inherits the client's environment,
|
||||
// parses this before RunSession sets its own transport, and has no client half anyway.)
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
|
||||
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// Said, not silently honoured: there is no remote server here to own a window.
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
|
||||
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
|
||||
"and this run has none. Window surfaces stay the client's own");
|
||||
}
|
||||
}
|
||||
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// One line, on the arm where these numbers decide behaviour, because every one of
|
||||
// them is a number a bug report has to quote.
|
||||
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
|
||||
// missing while the value was a number nobody varied, and the moment it became one that
|
||||
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
|
||||
// log that does not name it makes every run ambiguous about its own configuration.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
|
||||
"persistent-block=%uKiB create-window=%u "
|
||||
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
|
||||
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.AdoptTier,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
|
||||
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
|
||||
if (ipc.VerbBarrier == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
|
||||
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
|
||||
"live GLContext, so an unbarriered queue lets the server read future values");
|
||||
}
|
||||
if (ipc.WireDeferredMb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
|
||||
"server never forces a sync for orphaned wire buffer stores, so a long frame "
|
||||
"that respecifies and draws holds every one of them until the frame ends");
|
||||
}
|
||||
if (ipc.PersistentBlockKb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
|
||||
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Loading configuration from environment variables...");
|
||||
InitializeAcceptedEnvVariables();
|
||||
|
||||
InitBackendType();
|
||||
InitFeatures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
|
||||
// configuration, and before the accepted-env map is destroyed just below.
|
||||
InitTransport();
|
||||
InitIpc();
|
||||
#endif
|
||||
|
||||
// Destroy the map since we won't need it anymore
|
||||
acceptedEnvVariablesMap.reset();
|
||||
|
||||
+1
-72
@@ -17,13 +17,9 @@
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
@@ -46,20 +42,6 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
|
||||
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
|
||||
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
|
||||
// symbol and no resized one (the first version called it unconditionally and moved
|
||||
// DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than
|
||||
// the call is the load-bearing part.
|
||||
MG_Backend::ShutdownSplitRoles();
|
||||
#endif
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
// off, and idempotent.
|
||||
MG_Util::PipeStats::Shutdown();
|
||||
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||
@@ -102,14 +84,6 @@ namespace MobileGL {
|
||||
MG_State::GLState::ClearProgramTranslationCache();
|
||||
MG_Backend::gBackendFunctionsTable = {};
|
||||
g_isInitialized = false;
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
|
||||
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
|
||||
// FATAL=0 summary the comparator used to write from __run_exit_handlers truncated the
|
||||
// client half of every such run to that one line. Verify builds only - the guard is
|
||||
// the compile definition, not a runtime knob, and the pull build gains no byte (G1).
|
||||
MG_Pipe::MGPipeVerifyFlushSummary();
|
||||
#endif
|
||||
if (logLifecycle) {
|
||||
MG_Util::Debug::Close();
|
||||
}
|
||||
@@ -128,54 +102,9 @@ namespace MobileGL {
|
||||
MGLOG_I("Initializing MobileGL...");
|
||||
MG_ConfigLoader::Init();
|
||||
MGLOG_I("Config loaded");
|
||||
// Immediately after the config load and before anything can count: the MGPipe
|
||||
// boundary counters latch their enable flag here, so every counting site in the
|
||||
// two backends is a load of an already-settled global for the rest of the run.
|
||||
MG_Util::PipeStats::Init();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// F1 (P7 wave 2). THE WIRE COMES UP BEFORE THE FIRST FRONTEND OBJECT IS BORN, and the
|
||||
// ORDER is the fix - not a wait, and not a friendlier default mask.
|
||||
//
|
||||
// MG_State::Init() constructs GLContext, and GLContext's TextureState constructor
|
||||
// materialises one default texture object per target (GL 3.3 core 3.8,
|
||||
// TextureState.cpp:62-71). TextureObjectBase's constructor emits resource_create for
|
||||
// every one of them (TextureState/TextureObject.cpp:168-169), and that emission is
|
||||
// gated, through PipeFill's FamilyIsLive, on
|
||||
// CapsMirror::ServerConsumes(kMGPipeSubsystemTextureResources). In the old order the
|
||||
// mirror was still GENERATION 0 when it was asked - it had no snapshot because the
|
||||
// session had not been started yet, MG_Backend::Init() being what starts it - so it
|
||||
// answered from the placeholder, every one of those creates was withheld, and the log
|
||||
// said so once and then never again:
|
||||
//
|
||||
// MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
|
||||
// family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
|
||||
// caps generation 0
|
||||
//
|
||||
// That is not a race and it never was one: the line reproduces byte-for-byte on
|
||||
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
|
||||
// thread. And "the legacy pull path runs for it" is false under split - the client has
|
||||
// no pull path to fall back to - so the records are simply lost, and only a later
|
||||
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
|
||||
//
|
||||
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
|
||||
// MG_State::Init() no handshake is in flight, and the one that will land is started
|
||||
// LATER BY THIS SAME THREAD. The only correct answer is to have already brought it up,
|
||||
// which is what this hook does. ServerConsumes now REFUSES to answer from a
|
||||
// placeholder (CapsMirror.cpp), so a regression of this order is a named Fatal rather
|
||||
// than a whole family of silently dropped records.
|
||||
//
|
||||
// In a build without MOBILEGL_BUILD_DISAGGREGATED this statement does not exist, so
|
||||
// the pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
const Bool splitBackendIsUp = MG_Backend::InitSplitRolesBeforeState();
|
||||
#endif
|
||||
MG_State::Init();
|
||||
MGLOG_D("MG_State initialized");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!splitBackendIsUp)
|
||||
#endif
|
||||
{
|
||||
MG_Backend::Init();
|
||||
}
|
||||
MG_Backend::Init();
|
||||
MGLOG_D("MG_Backend initialized");
|
||||
MG_Impl::Init();
|
||||
MGLOG_D("MG_Impl initialized");
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
@@ -36,24 +33,9 @@ namespace MobileGL {
|
||||
struct CopyImageEndpoint {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
|
||||
Bool IsRenderbuffer() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Renderbuffer != nullptr;
|
||||
}
|
||||
Bool Exists() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Texture != nullptr || Renderbuffer != nullptr;
|
||||
}
|
||||
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
|
||||
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
|
||||
};
|
||||
|
||||
enum class FormatCapability : Uint64 {
|
||||
@@ -210,23 +192,9 @@ namespace MobileGL {
|
||||
void (*MemoryBarrierByRegion)(GLbitfield barriers);
|
||||
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
|
||||
GLenum access, GLenum format);
|
||||
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
|
||||
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
|
||||
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
|
||||
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
|
||||
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
|
||||
// 64-bit and float/double widths are derived there from the same answer, which is why
|
||||
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
|
||||
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
|
||||
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
|
||||
// case for at all.
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
|
||||
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
|
||||
// size - all of which are frontend link artifacts on ProgramObject, and
|
||||
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
|
||||
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
|
||||
// one, or a SPIR-V module), in a namespace the application never sees.
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
@@ -332,13 +300,13 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
Uint64 UniformBufferOffsetAlignment = 256;
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
Uint64 ShaderStorageBufferOffsetAlignment = 256;
|
||||
SizeT ShaderStorageBufferOffsetAlignment = 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;
|
||||
@@ -396,19 +364,6 @@ namespace MobileGL {
|
||||
Int MaxFragmentShaderStorageBlocks = 8;
|
||||
Int MaxComputeUniformBlocks = 12;
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
|
||||
// axis. These six, with the invocations limit above, are the only indexed limits a
|
||||
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
|
||||
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
|
||||
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
|
||||
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
|
||||
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
|
||||
// answers, like the invocations limit: GL_Getter and the compile environment floor
|
||||
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
|
||||
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
|
||||
// MaxClipDistances' does.
|
||||
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
|
||||
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
@@ -460,8 +415,8 @@ namespace MobileGL {
|
||||
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
||||
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
||||
// of the backend.
|
||||
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Int MaxViewportWidth = 16384;
|
||||
Int MaxViewportHeight = 16384;
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
@@ -558,7 +513,7 @@ namespace MobileGL {
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
Uint32 SubgroupSupportedFeatures = 0;
|
||||
|
||||
@@ -15,45 +15,4 @@
|
||||
namespace MobileGL::MG_Backend {
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
|
||||
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
|
||||
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
|
||||
//
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
|
||||
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
|
||||
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
|
||||
// to happen at its top. Two consequences are load-bearing rather than tidy:
|
||||
//
|
||||
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
|
||||
// a counter while the final line is produced;
|
||||
// * the apply thread is joined before pActiveBackendObject.reset(), so the server's own
|
||||
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
|
||||
// owns the context, by ServerLoop::Stop, rather than on the app thread.
|
||||
//
|
||||
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
|
||||
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
|
||||
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
|
||||
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
|
||||
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
|
||||
void ShutdownSplitRoles();
|
||||
|
||||
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
|
||||
//
|
||||
// Returns true when this process took the split path - whether the bring-up succeeded or
|
||||
// was refused by name - and the caller must then NOT call Init() a second time. Returns
|
||||
// false under MG_Config::TransportMode::Monolith, where there is nothing to bring up and
|
||||
// Init() keeps its own place after MG_State::Init().
|
||||
//
|
||||
// WHY IT EXISTS AT ALL, rather than MobileGL::Initialize simply calling Init() earlier:
|
||||
// the monolith arm of Init() must keep running after MG_State::Init(), because
|
||||
// BackendObject_DirectGLES::Initialize() is the tree's own order for it and moving it is a
|
||||
// pull-build change G1 forbids. So the split arm - and only the split arm - moves.
|
||||
//
|
||||
// It exists ONLY in a split build, for the reason InitServerRoleForSpawn states: a
|
||||
// definition guarded only on the inside still emits a symbol, and G1 measured that.
|
||||
Bool InitSplitRolesBeforeState();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -32,54 +32,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / ID-67 (v1, under ID-52/ID-59's grant for this file). Which VIRTUAL (dpy, draw,
|
||||
// read, ctx) the process's one native ES context is currently bound FOR, on the apply
|
||||
// thread. DirectGLES has one native context and one native surface (g_Context, g_Surface)
|
||||
// whatever virtual handles the client uses, so "is a native eglMakeCurrent needed" is never
|
||||
// "is the native triple different" - it is "did the VIRTUAL context change":
|
||||
// DirectGLES::MakeCurrent is also where the seven caches that describe the frontend context
|
||||
// are invalidated, and a different virtual context needs them invalidated even though the
|
||||
// driver binds the same triple. ID-67: a make-current with a DIFFERENT tuple is a real
|
||||
// native bind (and a caps republish, ServerLoop's half); an IDENTICAL one is neither.
|
||||
//
|
||||
// Three states. NotBound: the next bind is native. FreshFromSurfaceCreation: the surface's
|
||||
// own creation (InitPbufferSurface / InitWindowSurface) bound natively and invalidated, and
|
||||
// no virtual tuple has claimed that bind yet - the first tuple adopts it, which is the
|
||||
// "2 -> 1 native binds per process" of round 3. BoundForTuple: bound and invalidated for
|
||||
// the recorded tuple; only that exact tuple may skip. Process-wide like the native state it
|
||||
// mirrors; under split exactly one DirectGLES object exists (the server's), and only the
|
||||
// apply thread reaches these.
|
||||
enum class NativeBindState : Uint8 { NotBound, FreshFromSurfaceCreation, BoundForTuple };
|
||||
NativeBindState g_nativeBindState = NativeBindState::NotBound;
|
||||
EGLDisplay g_nativeBoundDpy = EGL_NO_DISPLAY;
|
||||
EGLSurface g_nativeBoundDraw = EGL_NO_SURFACE;
|
||||
EGLSurface g_nativeBoundRead = EGL_NO_SURFACE;
|
||||
EGLContext g_nativeBoundCtx = EGL_NO_CONTEXT;
|
||||
|
||||
void NoteNativeContextGone() { g_nativeBindState = NativeBindState::NotBound; }
|
||||
void NoteNativeContextFreshFromSurfaceCreation() {
|
||||
g_nativeBindState = NativeBindState::FreshFromSurfaceCreation;
|
||||
}
|
||||
void NoteNativeBoundFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
g_nativeBindState = NativeBindState::BoundForTuple;
|
||||
g_nativeBoundDpy = dpy;
|
||||
g_nativeBoundDraw = draw;
|
||||
g_nativeBoundRead = read;
|
||||
g_nativeBoundCtx = ctx;
|
||||
}
|
||||
Bool NativeBindCanBeSkippedFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
switch (g_nativeBindState) {
|
||||
case NativeBindState::NotBound: return false;
|
||||
case NativeBindState::FreshFromSurfaceCreation: return true;
|
||||
case NativeBindState::BoundForTuple:
|
||||
return g_nativeBoundDpy == dpy && g_nativeBoundDraw == draw && g_nativeBoundRead == read &&
|
||||
g_nativeBoundCtx == ctx;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
|
||||
if (!gl.glGetError) return;
|
||||
while (gl.glGetError() != GL_NO_ERROR) {}
|
||||
@@ -860,19 +812,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Int maxSamples = 0;
|
||||
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52: read the ROLE's own cache. Under split that is the server's private backend
|
||||
// (ActiveBackendFormatCaps), not pActiveBackendObject, which holds the client's mirror.
|
||||
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
|
||||
if (activeCaps != nullptr && targetIndex < kFormatCapabilityTargetCount &&
|
||||
formatIndex < kFormatCapabilityFormatCount) {
|
||||
// Descending, so the head is the largest count this device actually allocated.
|
||||
const Vector<Int>& probedCounts = activeCaps->SampleCounts[targetIndex][formatIndex];
|
||||
if (!probedCounts.empty()) {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
|
||||
formatIndex < kFormatCapabilityFormatCount) {
|
||||
// Descending, so the head is the largest count this device actually allocated.
|
||||
@@ -882,7 +821,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (maxSamples <= 0) {
|
||||
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
|
||||
}
|
||||
@@ -891,19 +829,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
||||
DestroyEGLContext();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
// P12: under a transport this object is the SERVER's backend and its destruction is the
|
||||
// end of a session (ServerLoop's apply thread, after the final drain). The in-process
|
||||
// display server runs the next session in this same process, so the twins this one built
|
||||
// - naming ids of the context just destroyed - must not answer for the next client's
|
||||
// handles (Managers.h, DropEveryTwinForEndedServerSession). Monolith keeps its twins.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
DropEveryTwinForEndedServerSession();
|
||||
// Review fix: and the swap interval it asked for (DirectGLES.h).
|
||||
ForgetRequestedSwapInterval();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitWindowSurface() {
|
||||
@@ -981,20 +906,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / ID-67: the surface's creation bound natively (InitWindowSurface -> MakeCurrent);
|
||||
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
|
||||
// pull arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
}
|
||||
return created;
|
||||
#else
|
||||
return BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
@@ -1013,19 +925,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
|
||||
// arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
}
|
||||
return created;
|
||||
#else
|
||||
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
@@ -1038,9 +938,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!DirectGLES::ReleaseCurrent()) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
}
|
||||
|
||||
@@ -1061,50 +958,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ID-54 / C7, the native half of "bind once per tuple" (v1, under ID-52/ID-59's grant for
|
||||
// this file; #if-guarded so the pull build is byte-identical). Under an active transport
|
||||
// this runs on the apply thread, which is the ONLY thread that ever binds the server's
|
||||
// context, and the surface it is asked for was made natively current on this very thread
|
||||
// by its own creation (InitPbufferSurface / InitWindowSurface -> DirectGLES::MakeCurrent).
|
||||
// A second native eglMakeCurrent for the same surface is then a repeat: it rewrites the
|
||||
// owner with the same thread, re-registers the same op table and invalidates seven caches
|
||||
// that describe a context that did not change - the per-client-make-current storm the
|
||||
// v2 review measured as "2 native binds per process, unchanged". So when the requested
|
||||
// draw surface IS the active one and EGL itself says this thread holds the context
|
||||
// (IsBackendContextCurrentOnThisThread re-verifies against eglGetCurrentContext), the
|
||||
// native call is skipped and only the base class's bookkeeping below runs - which is
|
||||
// still required: it is what InitCapabilities and SwapEGLBuffers' current-thread record
|
||||
// hang off. Monolith transport in this build, and the pull build, bind exactly as before.
|
||||
//
|
||||
// AND ONLY FOR THE SAME VIRTUAL TUPLE (ID-67). The skip is keyed on NativeBindCanBeSkippedFor:
|
||||
// the surface's own creation bind is adopted by the FIRST tuple, an identical tuple skips,
|
||||
// and a DIFFERENT tuple - a second MobileGL context onto the same surface - runs the native
|
||||
// call again even though the driver's triple is the same, because MakeCurrent's seven
|
||||
// invalidations describe the frontend context that is changing. Red once, two ways:
|
||||
// make this arm unconditional (ServerLoopTest's C7 control reads 2 native binds at the EGL
|
||||
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
|
||||
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
|
||||
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
DirectGLES::IsBackendContextCurrentOnThisThread() &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
|
||||
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
|
||||
NoteNativeContextGone();
|
||||
return false;
|
||||
}
|
||||
NoteNativeBoundFor(dpy, draw, read, ctx);
|
||||
#else
|
||||
if (!DirectGLES::MakeCurrent()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
(void)DirectGLES::ReleaseCurrent();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1122,18 +981,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BackendObject_DirectGLES::ReleaseEGLResources() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
DestroyEGLContext();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
DestroyEGLContext();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
|
||||
@@ -1402,6 +1255,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.Clear = Clear;
|
||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||
@@ -1562,13 +1417,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
|
||||
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
|
||||
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
|
||||
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
|
||||
#pragma once
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Uint g_backendContextGeneration;
|
||||
|
||||
struct ContextEpoch {
|
||||
Uint Native = 0;
|
||||
Uint64 Served = 0;
|
||||
Bool Server = false;
|
||||
Bool operator==(const ContextEpoch&) const = default;
|
||||
};
|
||||
|
||||
inline ContextEpoch CurrentContextEpoch() {
|
||||
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,16 +23,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
|
||||
// Native/server-only color level readback. Returns tightly packed, owned
|
||||
// Driver-side color/depth level readback. Returns tightly packed, owned
|
||||
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
|
||||
// The caller resolves identity/extent from its resource record and supplies
|
||||
// whether the SOURCE allocation already uses an image carrier.
|
||||
// The caller supplies identity and extent, and whether the SOURCE allocation
|
||||
// already uses an image carrier.
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
|
||||
#endif
|
||||
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);
|
||||
@@ -102,6 +99,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
Bool InitWindowSurface(NativeWindowType window);
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
@@ -188,13 +187,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Applies (or defers until the window surface exists) the app-requested
|
||||
// eglSwapInterval on the native EGL surface.
|
||||
void SetSwapInterval(Int interval);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 review fix: the swap interval an ended server session asked for is not the next one's.
|
||||
// The request outlives the context on purpose (a surface re-created mid-session re-applies it),
|
||||
// so the in-process display server's session end forgets it: the next session starts at the
|
||||
// driver's default until it asks (BackendObject_DirectGLES's destructor under a transport).
|
||||
void ForgetRequestedSwapInterval();
|
||||
#endif
|
||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,6 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -44,14 +41,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MGB_CTX->GetPrimitiveRestartIndex();
|
||||
return MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -84,172 +81,19 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kMaxComputeWorkGroups = 65535;
|
||||
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
|
||||
|
||||
// BoundDrawIndirectBufferId MOVED (P5e ra2, ID-136) to sit beside BoundIndexBufferId,
|
||||
// which is the same question about the other target and now has the same two arms.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The bound index buffer, and WHICH SIDE ANSWERS FOR IT
|
||||
//
|
||||
// P5e (mv), CONTRACT-P5E §5.1 + §5.8 (ruling ID-81): this file asks the bound element
|
||||
// array buffer exactly three questions - "is one bound at all", "what GL name did
|
||||
// PrepareForDraw leave on GL_ELEMENT_ARRAY_BUFFER (and how big is its store)", and
|
||||
// "give me its bytes on the CPU" - and until now it asked all three of the FRONTEND
|
||||
// VAO, on the apply thread, once per indexed multi-draw. That was the last unguarded
|
||||
// `MGB_CTX->GetBoundVertexArray()` in the backend: package vi retired the ordinary
|
||||
// draw path's copy (DirectGLES.cpp's SyncCurrentVertexAttributeValues / PrepareForDraw)
|
||||
// and this one survived only because these batches used to die earlier, on the
|
||||
// framebuffer row fb has since retired.
|
||||
//
|
||||
// THE ARM IS STATED, NEVER INFERRED. BufferImpl::VertexInputReadsRecords() is the
|
||||
// family's own selector and reduces to `Transport != Monolith && the vertex-input bit`;
|
||||
// it is the same conjunction RefuseElementArrayBufferFromTheFrontend guards the
|
||||
// single-draw path with. Nothing below decides an arm by noticing that a handle or a
|
||||
// pointer happens to be null - conflating "a handle was noted" with "the record arm is
|
||||
// selected" is what ID-107 cost this phase 137 scenarios.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// How much of the index buffer a caller needs. DELIBERATELY not nested levels: each
|
||||
// value is exactly the work its original call site did, in its original order, so the
|
||||
// monolith arm makes the same calls in the same sequence it made before this package.
|
||||
enum class IndexBufferQuestion {
|
||||
Presence, // is an element array buffer bound at all
|
||||
DriverName, // + the GL name currently bound to GL_ELEMENT_ARRAY_BUFFER
|
||||
DriverNameAndSize,// + the store's size in bytes (the compute tier's source)
|
||||
HostBytes, // a CPU-readable copy of the whole store, and its size
|
||||
};
|
||||
|
||||
struct BoundIndexBufferView {
|
||||
Bool Present = false;
|
||||
Uint Id = 0; // DriverName*: 0 when there is no backend store yet
|
||||
SizeT Size = 0; // DriverNameAndSize / HostBytes
|
||||
const Uint8* HostBytes = nullptr; // HostBytes: null when there is no CPU copy
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A missing record on the handle arm is a NAMED refusal and never a quiet fall-back to
|
||||
// the frontend (TASK-mv requirement 2; the shape is Managers.cpp's
|
||||
// RefuseNullFrontendTextureOffTheHandleArm). The frontend element slot is not a second
|
||||
// opinion on this side: it answers for whatever the CLIENT has bound NOW, which is a
|
||||
// later draw than the one being applied. Drawing a batch from it would be a silently
|
||||
// wrong picture, which is precisely what MultiDrawScenario's batch-matches-unrolled
|
||||
// assertions exist to catch and what a fall-back would hide from them.
|
||||
[[noreturn]] void RefuseMissingIndexBufferRecord(const char* entry, MG_Pipe::MGPipeHandle res,
|
||||
const char* missing) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-index-buffer-arm\"} - %s found no %s for "
|
||||
"the index buffer {%u, %u} this batch's record named. The index buffer of this "
|
||||
"family is MGPipeApplier().IndexBuffer.Res (CONTRACT-P5E §5.1) and the arm is "
|
||||
"selected by Transport != Monolith AND the vertex-input subsystem bit (§5.8, "
|
||||
"ID-81); falling back to the frontend VAO's element slot here would draw this "
|
||||
"multi-draw from whatever the CLIENT has bound now",
|
||||
entry, missing, res.Slot, res.Gen);
|
||||
std::abort();
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
const auto& indirect =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
// ---- P5e (ra2), ID-136: THE SAME REFUSAL FOR THE INDIRECT TARGET -------------------
|
||||
//
|
||||
// A sibling of mv's rather than a second idiom, because it is the same sentence about
|
||||
// the other buffer target: a missing record on the handle arm aborts by name and never
|
||||
// falls back to the frontend binding slot, which answers for whatever the CLIENT has
|
||||
// bound NOW - a later verb than the one being applied.
|
||||
[[noreturn]] void RefuseMissingIndirectBufferRecord(const char* entry,
|
||||
MG_Pipe::MGPipeHandle res) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-indirect-buffer-arm\"} - %s found no "
|
||||
"backend resource for the indirect buffer {%u, %u} this verb's record named. "
|
||||
"The indirect buffer of this family is MGPipeApplier().VerbIndirectBuffer "
|
||||
"(CONTRACT-P5E §2.1) and the arm is selected by Transport != Monolith "
|
||||
"(ID-81); falling back to the frontend GL_DRAW_INDIRECT_BUFFER slot here "
|
||||
"would restore whatever the CLIENT has bound now",
|
||||
entry, res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Does the applier's record say the application SUPPLIED this store's content? The M-3
|
||||
// rule ScopedRestartIndexSubstitution spells: for a supplied store a coverage gap in the
|
||||
// server shadow is a missing record and Fatal by name; for one the application ORPHANED
|
||||
// the gap is its own undefined content, and reading the shadow's zero-fill is exactly
|
||||
// what the monolith arm's MappedData() hands back.
|
||||
Bool IndexBufferRecordHasDefinedContent(const MG_Pipe::MGPipeApplierState& st,
|
||||
MG_Pipe::MGPipeHandle res) {
|
||||
if (res.Slot >= st.Resources.size()) return false;
|
||||
const auto& candidate = st.Resources[res.Slot];
|
||||
if (!candidate.Live || candidate.Gen != res.Gen) return false;
|
||||
return candidate.Desc.HasDefinedContent != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
BoundIndexBufferView ResolveBoundIndexBuffer(IndexBufferQuestion question, const char* entry) {
|
||||
BoundIndexBufferView view;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (BufferImpl::VertexInputReadsRecords()) {
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
|
||||
// A null Res is "no element array buffer bound", exactly as a null frontend slot
|
||||
// is on the arm below - the batch's indices are a client array, and every caller
|
||||
// already has an arm for that. It is NOT the arm test; the arm was decided above.
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
|
||||
view.Present = true;
|
||||
if (question == IndexBufferQuestion::Presence) return view;
|
||||
|
||||
if (question == IndexBufferQuestion::HostBytes) {
|
||||
// No Sync* pair here and none is missing: on this side the applier has
|
||||
// already consumed the persistent-map blocks and the shader writebacks for
|
||||
// this resource before the draw verb replayed, so the server's staged shadow
|
||||
// IS the synced copy. That is also why this arm cannot be expressed as "find
|
||||
// the object and run the monolith body".
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.HostBytes = resource->hostBytes;
|
||||
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
|
||||
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
|
||||
"multidraw_index_rebase");
|
||||
}
|
||||
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
|
||||
// equally allowed to be null (an adopted coherent map keeps its bytes
|
||||
// elsewhere), and the one caller that asks declines the tier for it.
|
||||
return view;
|
||||
}
|
||||
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Id = resource->id;
|
||||
if (question == IndexBufferQuestion::DriverNameAndSize) {
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what each call site did before.
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao) return view;
|
||||
const auto& ibo = vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (!ibo) return view;
|
||||
view.Present = true;
|
||||
switch (question) {
|
||||
case IndexBufferQuestion::Presence:
|
||||
break;
|
||||
case IndexBufferQuestion::DriverName: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
break;
|
||||
}
|
||||
case IndexBufferQuestion::DriverNameAndSize: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
case IndexBufferQuestion::HostBytes:
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
ibo->SyncPersistentMappedRange();
|
||||
ibo->SyncGpuWrites();
|
||||
view.HostBytes = ibo->MappedData();
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
(void)entry;
|
||||
return view;
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
|
||||
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
|
||||
@@ -257,57 +101,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// matters beyond tidiness: the VAO twin memoises that it already synced this
|
||||
// index binding and will not re-issue it on the next draw.
|
||||
Uint BoundIndexBufferId() {
|
||||
return ResolveBoundIndexBuffer(IndexBufferQuestion::DriverName, "BoundIndexBufferId").Id;
|
||||
}
|
||||
|
||||
// The GL name on GL_DRAW_INDIRECT_BUFFER, i.e. what a tier that swaps in its own scratch
|
||||
// COMMAND buffer has to put back - the exact twin of BoundIndexBufferId above, and now
|
||||
// with the same two arms.
|
||||
//
|
||||
// ---- P5e (ra2), ID-136: THE SEAT THIS FILE'S OWN RULE HAD MISSED -------------------
|
||||
//
|
||||
// This was the LAST unguarded frontend read on the multi-draw apply path, and it sat
|
||||
// twenty lines above the function that retired its neighbour. It asked
|
||||
// `MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
|
||||
// `EnsureBufferResource(<frontend object>)` - a registry lookup keyed by the client's
|
||||
// identity, which is §4.4's rule and not only the allocator's. Under run-ahead that is
|
||||
// `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"}` on every batch the
|
||||
// indirect tiers execute: 18 lane entries, and the only thing standing between the flip
|
||||
// and a green lane once the fill race was fixed.
|
||||
//
|
||||
// WHAT IT IS NOT: a data dependency. The tier does not want the client's indirect
|
||||
// buffer - it never reads a byte of it. It binds its OWN scratch command buffer
|
||||
// (g_indirectCommands) and wants to put back the name that was there. So the answer is
|
||||
// not "migrate the value" (which is what P8 owes for the ordinary indirect draw path);
|
||||
// it is "ask the side that did the binding". ID-133's escalation (iii) legalised the
|
||||
// pull instead, at the price of a rendezvous on every plain glMultiDraw* on the DEFAULT
|
||||
// tier - a real cost on the shipping arm, paid to make a lane green. ID-136 withdraws
|
||||
// that escalation and retires the read, in the commit that adds this arm.
|
||||
//
|
||||
// WHY MGPipeApplier().VerbIndirectBuffer IS THE WHOLE ANSWER ON THIS ARM: with a
|
||||
// transport, the ONLY writer of this process's GL_DRAW_INDIRECT_BUFFER outside this
|
||||
// function is DirectGLES.cpp's DrawSyncBit::IndirectBuffer arm, which binds from exactly
|
||||
// that handle and nothing else. So "what was bound" IS "what the verb's record named",
|
||||
// and a null handle is "this verb bound none" - the same 0 the monolith arm returns for
|
||||
// an empty slot, and not the arm test (ID-110: the arm was decided by the transport
|
||||
// above, never inferred from a null).
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
return resource->id;
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what this function did before.
|
||||
const auto& indirect =
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
const auto& ibo = BoundIndexBuffer();
|
||||
if (!ibo) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
@@ -360,10 +156,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
// statsClass: which MGPipe byte population these bytes belong to. Counted here
|
||||
// rather than at the four call sites so a new tier cannot forget it.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass) {
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -376,9 +169,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -393,8 +183,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -418,9 +207,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
@@ -563,18 +349,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU-readable bytes of one sub-draw's indices, from the CPU copy of the bound index
|
||||
// buffer (the frontend shadow on the monolith arm, the server's staged shadow on the
|
||||
// record arm) or straight from the client array. Null when the sub-draw would read
|
||||
// outside the buffer.
|
||||
//
|
||||
// P5e (mv): `hasIndexBuffer` is a Bool and not the frontend object it used to be,
|
||||
// because presence is the only thing this function ever asked of it - and on the record
|
||||
// arm there is no such object on this side to hand it.
|
||||
const Uint8* ResolveSubDrawIndices(Bool hasIndexBuffer, const Uint8* indexBufferBytes,
|
||||
SizeT indexBufferSize, const void* indices, SizeT indexCount,
|
||||
SizeT indexSize) {
|
||||
if (!hasIndexBuffer) {
|
||||
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
|
||||
// bound index buffer or straight from the client array. Null when the sub-draw
|
||||
// would read outside the buffer.
|
||||
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
|
||||
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
|
||||
SizeT indexCount, SizeT indexSize) {
|
||||
if (!indexBuffer) {
|
||||
return static_cast<const Uint8*>(indices);
|
||||
}
|
||||
if (!indexBufferBytes) return nullptr;
|
||||
@@ -617,7 +398,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (indexSize == 0) return false;
|
||||
// Indirect commands address indices as an element offset into the bound element
|
||||
// array buffer, and an indirect draw is not defined without one.
|
||||
if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "RunIndirect").Present) return false;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return false;
|
||||
|
||||
g_commandStaging.resize(static_cast<SizeT>(drawcount));
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -635,8 +417,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -708,10 +489,17 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (total == 0) return true;
|
||||
if (total > kMaxFlattenedIndices) return false;
|
||||
|
||||
const BoundIndexBufferView indexBuffer =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::HostBytes, "RunRebasedDrawElements");
|
||||
const Uint8* const indexBufferBytes = indexBuffer.HostBytes;
|
||||
const SizeT indexBufferSize = indexBuffer.Size;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
const Uint8* indexBufferBytes = nullptr;
|
||||
SizeT indexBufferSize = 0;
|
||||
if (indexBuffer) {
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
indexBuffer->SyncPersistentMappedRange();
|
||||
indexBuffer->SyncGpuWrites();
|
||||
indexBufferBytes = indexBuffer->MappedData();
|
||||
indexBufferSize = indexBuffer->GetSize();
|
||||
}
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
@@ -730,8 +518,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer.Present, indexBufferBytes, indexBufferSize,
|
||||
indices[i], subDrawCount, indexSize);
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
@@ -744,8 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -913,18 +700,16 @@ void main() {
|
||||
|
||||
// The shader reads the source indices as a storage buffer, so there has to be
|
||||
// a real buffer to read - a client-memory index array has none.
|
||||
if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "FlattenWithCompute").Present) return;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return;
|
||||
|
||||
// A dispatch inside an open capture span is not legal, and the span would also
|
||||
// observe one merged draw rather than the batch it asked for.
|
||||
if (XfbImpl::IsCaptureSpanOpen()) return;
|
||||
|
||||
// Asked a second time, and one question later: the ensure below may do GL work, so
|
||||
// it stays BEHIND the capture-span check exactly as it was before P5e (mv).
|
||||
const BoundIndexBufferView source =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::DriverNameAndSize, "FlattenWithCompute");
|
||||
if (source.Id == 0) return;
|
||||
const SizeT sourceSize = source.Size;
|
||||
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
|
||||
if (!sourceResource || sourceResource->id == 0) return;
|
||||
const SizeT sourceSize = indexBuffer->GetSize();
|
||||
// std430 addresses the source as uint[]; a tail shorter than a word is not
|
||||
// reachable, so a narrow index type needs a word-multiple buffer.
|
||||
if (indexSize < 4 && (sourceSize % 4) != 0) return;
|
||||
@@ -952,18 +737,12 @@ void main() {
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
|
||||
return;
|
||||
}
|
||||
// data == nullptr: pure respecify, the compute pass writes the contents, so no
|
||||
// host bytes cross here and nothing is counted.
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, source.Id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
|
||||
|
||||
@@ -1107,8 +886,7 @@ void main() {
|
||||
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
|
||||
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
|
||||
|
||||
const Bool hasIndexBuffer =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "DrawElementsBatch").Present;
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
// The compute tier dispatches BEFORE the draw state is established: doing it
|
||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||
|
||||
@@ -1,785 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/SlotTables.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#endif
|
||||
|
||||
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
|
||||
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
|
||||
//
|
||||
// What changes, and why each of them is the point:
|
||||
//
|
||||
// * The KEY stops being a frontend heap address. It is MGPipeHandle{Slot, Gen}, minted by the
|
||||
// client's MGPipeSlotAllocator off the frontend object's GetLifetimeId(). A recycled heap
|
||||
// address cannot reproduce a handle, so the weak_ptr the registry carried per entry purely
|
||||
// to catch that (its Entry::stateRef, used as an IDENTITY test) stops being an identity
|
||||
// mechanism, and OwnerEquals / TwinLookupMemo x3 / UnitSamplerLookupMemo's owner compare all
|
||||
// lose their reason to exist.
|
||||
// * The lookup stops being a hash probe into an open-addressed map and becomes one bounds
|
||||
// check plus one array index, so a returned BackendPtr* is NOT invalidated by the next Find
|
||||
// on the table. That kills the hazard Managers.h documents at length, and with it the
|
||||
// by-value copy plus second Find that SyncTextureObjectToBackend paid to survive it.
|
||||
// * Slots are dense per kind, which is what lets the server side (ARCHITECTURE.md 10.1,
|
||||
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
|
||||
//
|
||||
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
|
||||
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
|
||||
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
|
||||
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
|
||||
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
|
||||
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
|
||||
// goes. So:
|
||||
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
|
||||
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
|
||||
// * a twin, and the driver storage it owns, is freed when the application lets go of the
|
||||
// object rather than up to 64 creations or 1024 draw ticks later. That is what
|
||||
// Managers.h's "dead gigabytes" note asked for.
|
||||
//
|
||||
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
|
||||
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
|
||||
// length of a test, and a context reset does the same in reverse - and the slot allocator
|
||||
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
|
||||
// resolved again by the next would find nothing to resolve. Every table therefore links itself
|
||||
// into a per-table-type list at construction and out at destruction, and one notice resolves
|
||||
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
|
||||
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
|
||||
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
|
||||
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
|
||||
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
|
||||
//
|
||||
// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the
|
||||
// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy
|
||||
// death notice walks it. It is never an identity test - that is what Gen is for - and it is
|
||||
// never read to decide whether an entry is dead: a destructor that runs after exit() has begun
|
||||
// has its notice dropped by InProcessTeardown(), and that twin is then a DELIBERATE leak (the
|
||||
// process is exiting, the driver reclaims the object, and a twin destructor must not call into
|
||||
// a driver that may already be unloaded), not something to be collected later.
|
||||
//
|
||||
// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers
|
||||
// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out
|
||||
// of server memory was the last place the server could hold one across records - and a caller
|
||||
// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside
|
||||
// the scope that names the debt, so the read is one greppable site rather than a property of
|
||||
// the iteration.
|
||||
//
|
||||
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
|
||||
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
|
||||
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
|
||||
// under a real split neither the frontend object nor its lifetime id exists on this side of
|
||||
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
|
||||
// belong on the client, and the backend should receive the handle in the verb payload. It is
|
||||
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
|
||||
// so nothing catches it automatically.
|
||||
//
|
||||
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
|
||||
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
|
||||
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
|
||||
// allocator's own three entries carry, repeated here so the refusal names this surface), and
|
||||
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
|
||||
//
|
||||
// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf,
|
||||
// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport
|
||||
// apply before touching a frontend object, with or without a barrier or named scope.
|
||||
// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef.
|
||||
// The registry guard is independent of Magma's legacy allocator debt scope.
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
// Declared in Managers.h as well; repeated here because this header is included from it
|
||||
// before that declaration, and the table below is the arming site on this arm (D13: "the
|
||||
// arming site moves to the slot table's first insertion").
|
||||
void EnsureProcessTeardownSentinel();
|
||||
|
||||
// What the two knobs add up to. Split out as a PURE function of them so a test can drive
|
||||
// every combination without needing a process per combination.
|
||||
enum class EsprytSlotArmVerdict {
|
||||
Handles, // kMGPipeSubsystemEsprytSlots is set: the {slot, gen} tables run.
|
||||
Legacy, // the bit is clear and the legacy address-keyed registry is reachable.
|
||||
NoArm, // the bit is clear AND MOBILEGL_PIPE_LEGACY_MEMOS=0 made the legacy arm
|
||||
// unreachable, so the operator asked for a configuration with no arm at all.
|
||||
};
|
||||
|
||||
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled);
|
||||
|
||||
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||
|
||||
// Says, at backend bring-up, that the knobs leave no arm - and does NOT stop.
|
||||
//
|
||||
// The stop cannot live here, and that is the whole point of the split. Backend context
|
||||
// creation runs inside eglMakeCurrent, and the integration harness pre-flights exactly that
|
||||
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
|
||||
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
|
||||
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
|
||||
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
|
||||
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
|
||||
// which happens in the test body where the harness reports it as a failure.
|
||||
//
|
||||
// The CALL SITE (InitDisplayAndContext in DirectGLES.cpp) is pinned by
|
||||
// DirectGLESSlotTable.EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs
|
||||
// the real bring-up entry point under the pair in a forked child: edit that site back to
|
||||
// ResolveEsprytSlotTablesArm() and the case fails naming both knobs.
|
||||
void DiagnoseEsprytSlotArm();
|
||||
|
||||
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
|
||||
// left no arm at all. Cold: called exactly once per process, from the latch below - i.e. at
|
||||
// the first twin lookup, which is the first moment an arm is actually needed. A process
|
||||
// that never twins anything needs no arm and is not stopped.
|
||||
Bool ResolveEsprytSlotTablesArm();
|
||||
|
||||
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
|
||||
// two arms hold their twins in different containers, so flipping mid-run would strand them.
|
||||
//
|
||||
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
|
||||
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
|
||||
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
|
||||
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
|
||||
// branch, and the arm dispatch folds into the caller.
|
||||
inline Bool EsprytSlotTablesEnabled() {
|
||||
static const Bool enabled = ResolveEsprytSlotTablesArm();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable {
|
||||
public:
|
||||
using StatePtr = SharedPtr<StateObject>;
|
||||
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||
using BackendPtr = SharedPtr<BackendObject>;
|
||||
|
||||
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
|
||||
// payload. Slots are dense and allocated per kind, so a million of one kind is already
|
||||
// far past any application's live object count; the cap is here because the alternative
|
||||
// is letting a corrupt 32-bit slot decide a vector resize. See GetOrCreate(MGPipeHandle).
|
||||
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
|
||||
|
||||
// P5e (id), CONTRACT-P5E §4.3: THE COMPOSITE BAND, and it is a P5e PREREQUISITE rather
|
||||
// than a follow-up. ShaderCso's top 1/16 of slot space (from
|
||||
// kMGPipeShaderCsoCompositeSlotBase = 983040) is the program-pipeline composites'
|
||||
// (MGPipeHandles.h); EntryAt below indexes m_slots BY SLOT and resizes to it, so a
|
||||
// single composite used to grow g_backendProgramObjects to ~983k entries of ~40 B -
|
||||
// ~40 MB for one program pipeline. Today that is reachable only through
|
||||
// GetOrCreate(StatePtr) for a composite program; after the rekey the by-handle
|
||||
// resolution IS the ordinary path, so every composite bind would pay it.
|
||||
//
|
||||
// A SECOND VECTOR RATHER THAN MORE OF THE FIRST, exactly as the allocator
|
||||
// (SlotAllocator.h's KindState::BandSlots) and the applier (PipeApply.h's
|
||||
// CompositeShaderCsos) already do, and for the same reason: both spaces stay dense
|
||||
// against their own high-water mark, which is the property that lets the server index
|
||||
// rather than hash. The band is EMPTY for every kind but ShaderCso and costs those
|
||||
// kinds one empty Vector per table.
|
||||
static constexpr Bool kHasCompositeBand = (kKind == MG_Pipe::MGPipeKind::ShaderCso);
|
||||
static constexpr SizeT kMaxBandSlots =
|
||||
MG_Pipe::kMGPipeShaderCsoSlotLimit - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
|
||||
|
||||
struct Entry {
|
||||
BackendPtr backend;
|
||||
// THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the
|
||||
// minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and
|
||||
// the legacy death notice. Never compared against another object to decide identity
|
||||
// - that is what Gen is for - never dereferenced for its address, and never read to
|
||||
// decide whether the slot is dead: death is announced, not discovered. ForEachLive
|
||||
// stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server
|
||||
// memory on every step was the last place the server could hold one across records.
|
||||
StateWeakPtr stateRef;
|
||||
// The generation this entry's twin was built for. An entry whose Gen no longer
|
||||
// matches the allocator's is a twin of the slot's PREVIOUS owner.
|
||||
Uint32 Gen = 0;
|
||||
Bool Live = false;
|
||||
};
|
||||
|
||||
// Every constructor links the table into the per-type holder list and the destructor
|
||||
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
|
||||
// registry) is a holder for exactly as long as it exists. Copy and move carry the
|
||||
// ENTRIES and the memo; the links are the table's own and are never copied.
|
||||
BackendSlotTable() { LinkHolder(); }
|
||||
BackendSlotTable(const BackendSlotTable& other):
|
||||
m_slots(other.m_slots),
|
||||
m_band(other.m_band),
|
||||
m_nullTwin(other.m_nullTwin),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||
m_slots(std::move(other.m_slots)),
|
||||
m_band(std::move(other.m_band)),
|
||||
m_nullTwin(std::move(other.m_nullTwin)),
|
||||
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||
m_memoHandle(other.m_memoHandle) {
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable& operator=(const BackendSlotTable& other) {
|
||||
if (this != &other) {
|
||||
m_slots = other.m_slots;
|
||||
m_band = other.m_band;
|
||||
m_nullTwin = other.m_nullTwin;
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
BackendSlotTable& operator=(BackendSlotTable&& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_slots = std::move(other.m_slots);
|
||||
m_band = std::move(other.m_band);
|
||||
m_nullTwin = std::move(other.m_nullTwin);
|
||||
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||
m_memoHandle = other.m_memoHandle;
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~BackendSlotTable() { UnlinkHolder(); }
|
||||
|
||||
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
|
||||
// object's lifetime id, so two calls for the same live object always land on the same
|
||||
// slot, and a successor object at the same heap address never does.
|
||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
|
||||
// must not trap where the release build quietly does the documented thing.
|
||||
if (stateObj == nullptr) {
|
||||
// The registry this replaces inserted a null KEY and handed back that entry's
|
||||
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
|
||||
// exactly that tolerance), so a release build never dereferenced null here.
|
||||
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
|
||||
// entry, so a second null call was handed the same twin the first one got.
|
||||
// Resetting here instead would have destroyed it - an arm difference in the one
|
||||
// path that documents relying on this. One per-table parking slot, never live,
|
||||
// never handed a handle, because a null object has no identity and
|
||||
// therefore cannot have a {slot, gen}.
|
||||
return m_nullTwin;
|
||||
}
|
||||
|
||||
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
|
||||
// creation is the moment a driver-owned id starts needing a guarded destructor;
|
||||
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
|
||||
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// This overload mints from frontend identity and is monolith-only. Refuse
|
||||
// before either the client object or allocator is touched, even when a
|
||||
// legacy named scope is open and the current record is barriered.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)");
|
||||
#endif
|
||||
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"MGPipe slot space of kind %u is exhausted",
|
||||
static_cast<Uint32>(kKind));
|
||||
Entry& entry = EntryAt(handle.Slot);
|
||||
if (entry.Live && entry.Gen != handle.Gen) {
|
||||
// The slot was reclaimed and handed to a new object: the twin at it describes
|
||||
// driver ids the new state object never made.
|
||||
entry.backend.reset();
|
||||
}
|
||||
entry.Gen = handle.Gen;
|
||||
entry.Live = true;
|
||||
entry.stateRef = stateObj;
|
||||
// No creation tick and no sweep here. The registry this replaces needed both,
|
||||
// because nothing told it a texture or a renderbuffer had been DELETED and object
|
||||
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
|
||||
// now announces its own death from its destructor, so a dead twin's slot is already
|
||||
// back before the next creation asks for one.
|
||||
RememberHandle(stateObj->GetLifetimeId(), handle);
|
||||
return entry.backend;
|
||||
}
|
||||
|
||||
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
|
||||
// header records against itself at the top of the file.
|
||||
//
|
||||
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
|
||||
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
|
||||
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
|
||||
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
|
||||
// the call's payload, already minted by the side that owns minting; all this does is
|
||||
// index the slot, notice a generation that no longer matches (the slot was recycled,
|
||||
// so the twin at it describes driver ids the new resource never made) and hand back
|
||||
// the twin pointer. FindByHandle beside it is the same shape and already existed.
|
||||
//
|
||||
// No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes
|
||||
// it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold
|
||||
// by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys
|
||||
// on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle,
|
||||
// asked by the one caller that needs the object, that answers null for it. Which is the
|
||||
// same set the old stateRef-locking walk produced, decided at one site instead of in
|
||||
// the loop.
|
||||
//
|
||||
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
|
||||
// announcement is the family's own destroy call, not the shared death notice, and the
|
||||
// slot is freed by the CLIENT after that call returns.
|
||||
//
|
||||
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
|
||||
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
|
||||
// it its first caller.
|
||||
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"GetOrCreate(handle) named the reserved null handle");
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
|
||||
|
||||
// A slot index that ARRIVED in a payload indexes a vector this call would RESIZE,
|
||||
// and nothing between the payload and here bounds it: the applier's blob gates sit
|
||||
// in front of the vertex-input family, not in front of the resource family, which
|
||||
// dispatches ops->Create(record.Res, ...) straight through. There is no allocator
|
||||
// constant to check against on this side - the allocator is the client's - so this
|
||||
// is a sanity cap and is documented as one: kMaxHandleSlot entries of one kind is
|
||||
// already orders of magnitude past any real GL object count, while a corrupt 32-bit
|
||||
// slot asks for a four-billion-entry resize.
|
||||
if (handle.Slot >= kMaxHandleSlot) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// PH-2: handle.Slot is peer supplied on the disaggregated arm. Release builds
|
||||
// must publish a named protocol fault instead of silently returning m_nullTwin.
|
||||
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
|
||||
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
|
||||
"GetOrCreate(handle) named slot %u, past this table's %u bound",
|
||||
handle.Slot, kMaxHandleSlot);
|
||||
#else
|
||||
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
|
||||
handle.Slot, kMaxHandleSlot);
|
||||
return m_nullTwin;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Same arming as the minting overload, and for the same reason: twin creation is
|
||||
// the moment a driver-owned id starts needing a guarded destructor.
|
||||
EnsureProcessTeardownSentinel();
|
||||
|
||||
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
|
||||
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
|
||||
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
|
||||
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
|
||||
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
|
||||
// a persistent map, a pooled store, released by a defaulted destructor that issues
|
||||
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
|
||||
// dead resource's generation, after which the incumbent's own FindByHandle refuses
|
||||
// it and it is silently handed a fresh, empty twin. That is a leak AND a resource
|
||||
// that loses its storage with no diagnostic, i.e. the shape commit d7655247 fixed
|
||||
// and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and
|
||||
// resets the twin, BACKWARD is refused - which is the same answer FindByHandle
|
||||
// below already gives the same input.
|
||||
Entry& entry = EntryAt(handle.Slot);
|
||||
if (entry.Live && entry.Gen > handle.Gen) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// PH-2: a stale peer generation must not silently shed the incumbent twin in a
|
||||
// release server. The funnel names the exact identity fault and notifies the peer.
|
||||
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
|
||||
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
|
||||
"GetOrCreate(handle) named generation %u at slot %u, behind live generation %u",
|
||||
handle.Gen, handle.Slot, entry.Gen);
|
||||
#else
|
||||
MOBILEGL_ASSERT(false,
|
||||
"GetOrCreate(handle) named generation %u at slot %u, which is BEHIND "
|
||||
"the live entry's %u - refusing rather than destroying the incumbent",
|
||||
handle.Gen, handle.Slot, entry.Gen);
|
||||
return m_nullTwin;
|
||||
#endif
|
||||
}
|
||||
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
|
||||
entry.Gen = handle.Gen;
|
||||
entry.Live = true;
|
||||
return entry.backend;
|
||||
}
|
||||
|
||||
// The generation of the LIVE entry at this slot, or 0 when the slot is out of range or
|
||||
// holds no live entry. It exists so a caller can DIAGNOSE - in a release build, where
|
||||
// MOBILEGL_ASSERT is inert - the refusal GetOrCreate(handle) above performs silently.
|
||||
Uint32 LiveGenAt(Uint32 slot) const {
|
||||
const Entry* const entry = EntryOrNull(slot);
|
||||
if (entry == nullptr) return 0;
|
||||
return entry->Live ? entry->Gen : 0;
|
||||
}
|
||||
|
||||
// Remember/read the frontend object last used by the monolith sync path.
|
||||
// Handles supplied to the server never need this weak state. Neither method
|
||||
// touches the allocator, so each carries its own unconditional apply guard.
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return;
|
||||
entry->stateRef = stateObj;
|
||||
}
|
||||
|
||||
// The frontend object noted for this handle, or null. The handle answers identity;
|
||||
// this answers only "which object was this twin last synced from".
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle");
|
||||
#endif
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
const Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
|
||||
return entry->stateRef.lock();
|
||||
}
|
||||
|
||||
// P3a: the death half of the overload above, for a kind whose announcement is its own
|
||||
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
|
||||
// than destroying it in place, because the caller may still have to decide what
|
||||
// happens to the driver id it owns - Espryt pools it, deletes it, or parks it on the
|
||||
// deferred-release list when no context is current on this thread - and every one of
|
||||
// those outcomes has to be reached with the entry already retired, so a re-entrant
|
||||
// GetOrCreate from a twin destructor cannot resurrect it.
|
||||
//
|
||||
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
|
||||
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
|
||||
// Gen bump rides the next handout, so a double free cannot skip a generation). An
|
||||
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
|
||||
// left alone: the successor's own GetOrCreate resets it.
|
||||
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return BackendPtr{};
|
||||
BackendPtr dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
return dead;
|
||||
}
|
||||
|
||||
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
|
||||
// mutates the table, so the returned pointer survives any later Find on it; only a
|
||||
// GetOrCreate that grows the vector can move it, and callers that hold one across a
|
||||
// possible insertion still copy the BackendPtr out.
|
||||
BackendPtr* Find(StateObject* stateObj) {
|
||||
if (stateObj == nullptr) return nullptr;
|
||||
return FindByHandle(HandleOf(stateObj));
|
||||
}
|
||||
|
||||
const BackendPtr* Find(StateObject* stateObj) const {
|
||||
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
|
||||
}
|
||||
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
|
||||
return &entry->backend;
|
||||
}
|
||||
|
||||
// The handle this object's twin is keyed on, or the null handle. This is what a backend
|
||||
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
|
||||
//
|
||||
// A NULL answer is never memoised. The memo is per table and the allocator is per
|
||||
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
|
||||
// a cached "no handle" here would then outlive the twin's creation over there, and
|
||||
// nothing on this table's own acquire path would ever refresh it. A miss costs the
|
||||
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve frontend identity only in the monolith path. A cache hit is
|
||||
// subject to the same guard as a fresh allocator lookup.
|
||||
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf");
|
||||
#endif
|
||||
const Uint64 lifetimeId = stateObj->GetLifetimeId();
|
||||
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// P2 step e2's backend half. The frontend object with this lifetime id has just been
|
||||
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
|
||||
// return the slot to the allocator - in that order, because the allocator forgets the
|
||||
// lifetime id on Free and a holder told second could no longer resolve it.
|
||||
//
|
||||
// The slot is returned whether or not any holder still had a twin at it: the lifetime
|
||||
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
|
||||
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
|
||||
// entries without freeing) would otherwise stay allocated for the life of the process.
|
||||
//
|
||||
// STATIC, and deliberately so: a notice is about an object, not about a table, and
|
||||
// "which table holds it" is exactly the question that produced the two-holder leak.
|
||||
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
|
||||
// a second call for the same id answers false because the allocator no longer maps it.
|
||||
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
|
||||
// a frontend death is announced to the server by the object_death record (ct),
|
||||
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
|
||||
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
|
||||
#endif
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
|
||||
// destructor, which is a driver call, and nothing that outlives it may be a
|
||||
// reference into this holder.
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
holder->ReleaseTwinAt(handle);
|
||||
holder = next;
|
||||
}
|
||||
MG_Pipe::MGPipeSlots().Free(kKind, handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
|
||||
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
|
||||
// handle IS the resolution - it crossed in the record's payload - so the client's
|
||||
// allocator is never asked: under an active transport MGPipeSlots() is a client-only
|
||||
// surface (rule E, §3.1) and this function runs on the apply thread. Every holder
|
||||
// lets go exactly as the notice arm does, in the same successor-first order. What
|
||||
// does NOT happen here is the allocator Free: the slot's owner - the client -
|
||||
// returned it itself after the record went out (PipeFill.cpp's NotifyAndFree), and a
|
||||
// double Free would be refused by generation anyway. Idempotent against the kind's
|
||||
// own delete opcode, exactly as the notice arm is: a twin the delete already released
|
||||
// fails ReleaseTwinAt's generation check and the walk moves on.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
Bool released = false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
holder = next;
|
||||
}
|
||||
return released;
|
||||
}
|
||||
|
||||
// How many tables of this type exist right now. For the tests that pin the holder
|
||||
// list; nothing on a shipping path asks.
|
||||
static Uint32 HolderCount() {
|
||||
Uint32 count = 0;
|
||||
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr;
|
||||
holder = holder->m_nextHolder) {
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// P5e (id), CONTRACT-P5E §4.1: fn(MGPipeHandle, const BackendPtr& twin) over every
|
||||
// live, twinned entry. Replaces the registry's begin()/end(), whose iterator exposed
|
||||
// the raw frontend address as the map key - the one place the backend read an identity
|
||||
// it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a
|
||||
// frontend SharedPtr OUT OF SERVER MEMORY on every step.
|
||||
//
|
||||
// WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing.
|
||||
// The walk no longer locks Entry::stateRef, so a caller that needs the frontend object
|
||||
// asks StateForHandle(handle) for it - which is the same weak reference, read at one
|
||||
// named site inside the scope that owns the debt, instead of at every step of an
|
||||
// iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip
|
||||
// because its stateRef did not lock is one whose StateForHandle answers null, and the
|
||||
// caller skips it there instead.
|
||||
//
|
||||
// The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot
|
||||
// is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the
|
||||
// client minted, never the index into m_band.
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
// Index loop and a COPIED twin, not a range-for over references: fn is arbitrary
|
||||
// backend code, and a nested GetOrCreate on this table would resize m_slots and
|
||||
// invalidate both the iterator and any reference into the vector that outlives the
|
||||
// call. The one caller today happens not to insert; that is not a property the
|
||||
// walk should depend on.
|
||||
for (SizeT index = 0; index < m_slots.size(); ++index) {
|
||||
const Entry& entry = m_slots[index];
|
||||
if (!entry.Live || !entry.backend) continue;
|
||||
const BackendPtr twin = entry.backend;
|
||||
fn(MG_Pipe::MGPipeHandle{static_cast<Uint32>(index), entry.Gen}, twin);
|
||||
}
|
||||
for (SizeT index = 0; index < m_band.size(); ++index) {
|
||||
const Entry& entry = m_band[index];
|
||||
if (!entry.Live || !entry.backend) continue;
|
||||
const BackendPtr twin = entry.backend;
|
||||
fn(MG_Pipe::MGPipeHandle{
|
||||
static_cast<Uint32>(index) + MG_Pipe::kMGPipeShaderCsoCompositeSlotBase,
|
||||
entry.Gen},
|
||||
twin);
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 LiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_slots) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
for (const Entry& entry : m_band) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// The band's own live count, so a case can tell "the composite is twinned" from "the
|
||||
// ordinary table grew to reach it" - which is the whole assertion the band exists for
|
||||
// and is untestable from LiveCount alone.
|
||||
Uint32 CompositeLiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_band) {
|
||||
if (entry.Live) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// How many entries each space has ALLOCATED, live or not. A leak case asserts on these
|
||||
// rather than on LiveCount: growth is what the band prevents, and a dense table that
|
||||
// never shrinks is invisible to a liveness count.
|
||||
SizeT OrdinaryCapacityForTest() const { return m_slots.size(); }
|
||||
SizeT CompositeCapacityForTest() const { return m_band.size(); }
|
||||
|
||||
private:
|
||||
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
|
||||
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
|
||||
// every holder has let go.
|
||||
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
|
||||
// Forget the memo whenever it names this slot, even if this table has no entry
|
||||
// there: a memo can be a handle learned from the allocator for an object another
|
||||
// holder twinned, and it must not survive the slot's next handout.
|
||||
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||
// The twin's destructor is a driver call and could, in principle, re-enter
|
||||
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
|
||||
// destructor may be a reference into m_slots: the twin is moved out into a local,
|
||||
// the entry is finished with, and only then is the local released.
|
||||
BackendPtr dead;
|
||||
{
|
||||
Entry* const entry = EntryOrNull(handle.Slot);
|
||||
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false;
|
||||
dead = std::move(entry->backend);
|
||||
entry->backend.reset();
|
||||
entry->stateRef.reset();
|
||||
entry->Live = false;
|
||||
}
|
||||
dead.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P5e (id): the two slot spaces, resolved in ONE place ---------------------------
|
||||
//
|
||||
// Every reader and writer below goes through these three, so a caller that forgot the
|
||||
// band cannot exist - the shape MGPipeSlotAllocator::EntryOf already uses one level
|
||||
// out. kHasCompositeBand folds to `false` at compile time for the five non-ShaderCso
|
||||
// instantiations, so their band is dead code and an empty Vector.
|
||||
static constexpr Bool SlotIsBanded(Uint32 slot) {
|
||||
return kHasCompositeBand && MG_Pipe::MGPipeIsCompositeShaderSlot(slot);
|
||||
}
|
||||
static constexpr SizeT IndexOfSlot(Uint32 slot) {
|
||||
return SlotIsBanded(slot)
|
||||
? static_cast<SizeT>(slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase)
|
||||
: static_cast<SizeT>(slot);
|
||||
}
|
||||
|
||||
// The entry a slot names, or null when its space has never grown that far. NEVER grows:
|
||||
// a lookup that resized would turn every miss into an allocation, which is the hazard
|
||||
// Find documents.
|
||||
Entry* EntryOrNull(Uint32 slot) {
|
||||
Vector<Entry>& table = SlotIsBanded(slot) ? m_band : m_slots;
|
||||
const SizeT index = IndexOfSlot(slot);
|
||||
if (index >= table.size()) return nullptr;
|
||||
return &table[index];
|
||||
}
|
||||
const Entry* EntryOrNull(Uint32 slot) const {
|
||||
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
|
||||
}
|
||||
|
||||
// Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting
|
||||
// overload because the allocator produced it, the handle overload against
|
||||
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
|
||||
// allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary
|
||||
// table never learns the band exists.
|
||||
Entry& EntryAt(Uint32 slot) {
|
||||
Vector<Entry>& table = SlotIsBanded(slot) ? m_band : m_slots;
|
||||
const SizeT index = IndexOfSlot(slot);
|
||||
if (index >= table.size()) table.resize(index + 1);
|
||||
return table[index];
|
||||
}
|
||||
|
||||
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
|
||||
m_memoLifetimeId = lifetimeId;
|
||||
m_memoHandle = handle;
|
||||
}
|
||||
void ForgetHandle() const {
|
||||
m_memoLifetimeId = 0;
|
||||
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
|
||||
// The holder list: intrusive and doubly linked, so registering and unregistering are
|
||||
// two pointer writes with no allocation, and its head is a constant-initialised
|
||||
// static - which is what lets the process-lifetime registry globals in Managers.cpp
|
||||
// link themselves in from their own constructors with no initialisation-order
|
||||
// question to answer. Single-threaded, like every table it links (the tables live and
|
||||
// die on the context thread, as the notice they answer does).
|
||||
void LinkHolder() {
|
||||
m_prevHolder = nullptr;
|
||||
m_nextHolder = s_firstHolder;
|
||||
if (s_firstHolder != nullptr) s_firstHolder->m_prevHolder = this;
|
||||
s_firstHolder = this;
|
||||
}
|
||||
void UnlinkHolder() {
|
||||
if (m_prevHolder != nullptr) {
|
||||
m_prevHolder->m_nextHolder = m_nextHolder;
|
||||
} else {
|
||||
s_firstHolder = m_nextHolder;
|
||||
}
|
||||
if (m_nextHolder != nullptr) m_nextHolder->m_prevHolder = m_prevHolder;
|
||||
m_prevHolder = nullptr;
|
||||
m_nextHolder = nullptr;
|
||||
}
|
||||
|
||||
static inline BackendSlotTable* s_firstHolder = nullptr;
|
||||
BackendSlotTable* m_prevHolder = nullptr;
|
||||
BackendSlotTable* m_nextHolder = nullptr;
|
||||
|
||||
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
|
||||
Vector<Entry> m_slots;
|
||||
// P5e (id): the ShaderCso COMPOSITE band, indexed by (slot -
|
||||
// kMGPipeShaderCsoCompositeSlotBase) and EMPTY for every other kind. See
|
||||
// kHasCompositeBand above for why it is a second vector and not more of the first.
|
||||
Vector<Entry> m_band;
|
||||
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
|
||||
BackendPtr m_nullTwin;
|
||||
|
||||
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
|
||||
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
|
||||
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
|
||||
// indexing".
|
||||
//
|
||||
// It is one entry and therefore only helps a caller that asks for the SAME object twice
|
||||
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
|
||||
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
|
||||
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
|
||||
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
|
||||
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
|
||||
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
|
||||
//
|
||||
// It cannot serve a stale answer, by three independent arguments:
|
||||
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
|
||||
// heap address cannot hit this memo the way it could hit an address-keyed one;
|
||||
// * a null answer is never stored, so another holder's acquire cannot be hidden by
|
||||
// a "no handle" this table remembered earlier; and
|
||||
// * even a hit for a slot that has since been freed and re-handed is caught, because
|
||||
// the caller resolves the handle through FindByHandle, which compares Gen.
|
||||
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
|
||||
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
|
||||
mutable Uint64 m_memoLifetimeId = 0;
|
||||
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
@@ -10,9 +10,6 @@
|
||||
#include "Utils.h"
|
||||
#include "Managers.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
@@ -20,7 +17,6 @@
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -31,45 +27,11 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// read_pixels writes server reply scratch. The client refuses PACK_BUFFER before
|
||||
// emission, so no frontend pack binding can affect this destination.
|
||||
static const SharedPtr<MG_State::GLState::BufferObject>& SplitReadbackPackBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
return none;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps() {
|
||||
// Under a live split session the SERVER's private backend is what owns the context on the
|
||||
// apply thread (and these reads all run there), so its cache is the authoritative one.
|
||||
// Before the session lands, or under monolith transport in a disaggregated build, fall
|
||||
// back to the process global exactly as the monolith path always has.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
|
||||
return &server->GetFormatCapabilities();
|
||||
}
|
||||
// P5c (hd, CONTRACT-P5C §3.7): the mirror fallback is REFUSED with an active
|
||||
// transport. The server's backend exists whenever the session does, so reaching
|
||||
// here is a bring-up ordering defect, and a silent read of the client caps mirror
|
||||
// (pActiveBackendObject is client memory, rule E) would hide it.
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"caps-mirror\"} - the format-capability "
|
||||
"fallback to the client caps mirror was reached with an active transport "
|
||||
"but no server backend; the server's own backend is the only legal source "
|
||||
"on the apply thread");
|
||||
std::abort();
|
||||
}
|
||||
return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
@@ -108,26 +70,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SizeT targetIndex,
|
||||
Bool caveat,
|
||||
FormatCapability capability) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
|
||||
if (activeCaps == nullptr || targetIndex >= kFormatCapabilityTargetCount) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!pActiveBackendObject || targetIndex >= kFormatCapabilityTargetCount) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
const SizeT formatIndex = static_cast<SizeT>(internalFormat);
|
||||
if (formatIndex >= kFormatCapabilityFormatCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const FormatCapabilityCache& cache = *activeCaps;
|
||||
#else
|
||||
const FormatCapabilityCache& cache = pActiveBackendObject->GetFormatCapabilities();
|
||||
#endif
|
||||
const FormatCapabilityFlags caps =
|
||||
caveat ? cache.CaveatCaps[targetIndex][formatIndex] : cache.FullCaps[targetIndex][formatIndex];
|
||||
return HasFormatCapability(caps, capability);
|
||||
@@ -171,11 +122,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
using namespace MobileGL::MG_Util::TextureFormatProcessor;
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() == nullptr || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
#else
|
||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
#endif
|
||||
options = GetRuntimeFallbackNormalizeOptions(
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
@@ -269,15 +216,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
|
||||
// forever. Once the backend exists, the first narrow-format image this process
|
||||
// creates runs the probe on a live context.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
|
||||
}
|
||||
|
||||
@@ -315,15 +256,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ActiveBackendFormatCaps() != nullptr && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
|
||||
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
@@ -2359,15 +2294,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? SplitReadbackPackBuffer() :
|
||||
#endif
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
|
||||
@@ -13,19 +13,6 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
|
||||
// THIS ROLE's backend. Under an active transport the server's own private
|
||||
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
|
||||
// lookup must read ITS probed cache - not pActiveBackendObject's, which under split is the
|
||||
// CLIENT's BackendObject_Remote mirror (a caps snapshot, generation-lagged, and on an
|
||||
// independent server not usable at all). Monolith build/transport: pActiveBackendObject, so a
|
||||
// pull build never sees this symbol. Null when no backend is up. The seven table-3 reads
|
||||
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
|
||||
// this instead of dereferencing pActiveBackendObject directly.
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps();
|
||||
#endif
|
||||
|
||||
namespace DebugImpl {
|
||||
class ErrorLopper {
|
||||
public:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Included in DirectGLES.cpp after the native packed-word readback helper.
|
||||
// The server needs owned tight rows for a storage promotion, not client PACK
|
||||
// layout or a fallback into a frontend texture's potentially stale shadow.
|
||||
// A storage promotion needs the driver's own texels as owned tight rows, not
|
||||
// client PACK layout or a fallback into a frontend texture's potentially stale
|
||||
// shadow.
|
||||
class ScopedWireTextureReadLevel {
|
||||
public:
|
||||
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
|
||||
@@ -279,31 +280,3 @@
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
|
||||
const auto* record = PipeTextureRecordForHandle(info.Res);
|
||||
if (!record) return false;
|
||||
TextureTarget target = TextureTarget::Unknown;
|
||||
for (Int i = 0; i < static_cast<Int>(TextureTarget::TextureTargetCount); ++i)
|
||||
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)) == record->Desc.Target)
|
||||
target = static_cast<TextureTarget>(i);
|
||||
if (target == TextureTarget::Unknown) return false;
|
||||
const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res);
|
||||
if (!twin) return false;
|
||||
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
|
||||
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
|
||||
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
|
||||
Vector<Uint8> result;
|
||||
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
|
||||
const auto uploadTarget = wholeCube
|
||||
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
|
||||
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
|
||||
Vector<Uint8> slice;
|
||||
if (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
|
||||
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
|
||||
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
|
||||
result.insert(result.end(), slice.begin(), slice.end());
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -330,25 +329,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() {
|
||||
// P12 review fix: A SERVER SESSION THAT ENDS WITHOUT A CLEAN TEARDOWN LEAVES NO RENDERER ON
|
||||
// THE WINDOW. The renderer - swapchain and VkSurfaceKHR on the window - is a process global
|
||||
// that only ReleaseEGLResources / a released surface drops, and a client that went away
|
||||
// without eglTerminate (a crash, a kill, a dropped connection) sends neither. Under a
|
||||
// transport this object is the server's and its destruction is the session's end (ServerLoop's
|
||||
// apply thread, after the final drain, before the display lease ends), so the renderer goes
|
||||
// here: nothing of the ended session keeps presenting into, or holding a surface on, the
|
||||
// server's window once its lease is over. Monolith keeps its renderer.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && pVulkanRenderer != nullptr) {
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
}
|
||||
#else
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||
#endif
|
||||
|
||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
||||
|
||||
@@ -363,7 +344,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Any renderer instance this assignment replaces is destroyed here;
|
||||
// fence/timer-query handles stamped with the old generation go stale.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
||||
VulkanRendererConfig config;
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
return true;
|
||||
@@ -383,18 +366,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::Initialize() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager::RegisterWireResourceOps();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). BESIDE the resource ops and for the same
|
||||
// reason: this is the one place both Magma server roles pass through - the inproc
|
||||
// role and the spawn/TCP session child - and ServerLoop::CreateBackend calls it at
|
||||
// step 1 of InitServerRoleCommon, before the client session exists and therefore
|
||||
// before any frontend object can die unheard. A per-transport install is how one of
|
||||
// the two roles ends up without it.
|
||||
InstallStateObjectDeathOps();
|
||||
#endif
|
||||
#endif
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
@@ -416,8 +387,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
@@ -486,6 +457,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
|
||||
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
BackendObject::SetEGLSwapInterval(interval);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
BackendObject::ReleaseEGLSurface(surface);
|
||||
@@ -544,7 +521,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Bool cubeMapArraySupported,
|
||||
Bool nvMeshShader) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
@@ -708,6 +686,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
|
||||
if (nvMeshShader) {
|
||||
extensions.push_back(E_GL_NV_mesh_shader);
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -727,6 +710,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool funcsTableInitialized = false;
|
||||
if (!funcsTableInitialized) {
|
||||
funcsTable.Present = Present;
|
||||
funcsTable.SetSwapInterval = SetSwapInterval;
|
||||
funcsTable.GL.DrawArrays = DrawArrays;
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
@@ -771,6 +755,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
@@ -814,8 +800,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
@@ -968,15 +954,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
|
||||
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
|
||||
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
|
||||
// them. Not clamped: unlike the block counts these are not amounts an application
|
||||
// allocates, and the frontend already raises them to the GL minimum.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
|
||||
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
|
||||
void SetEGLSwapInterval(Int interval) override;
|
||||
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||
void ReleaseEGLResources() override;
|
||||
|
||||
@@ -76,7 +77,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
Bool cubeMapArraySupported,
|
||||
Bool nvMeshShader = true);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DescriptorPoolCensus.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 <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A count of Magma's per-frame descriptor pools and the sets they hold (UniformManager), for
|
||||
// tests. Dependency-free on purpose: the integration scenarios include it next to the system GL
|
||||
// headers. They link the static library on desktop; the shipping shared library is built with
|
||||
// hidden visibility, so on Android nothing outside the library can call GetDescriptorPoolCensus.
|
||||
struct DescriptorPoolCensus {
|
||||
bool available = false; // false when no Magma renderer is live (e.g. the DirectGLES backend)
|
||||
uint32_t frameSlots = 0;
|
||||
uint32_t pools = 0; // descriptor pools across every frame slot
|
||||
uint32_t maxPoolsInOneSlot = 0;
|
||||
uint64_t capacitySets = 0; // sum of maxSets over those pools
|
||||
uint64_t cachedSets = 0; // sets allocated from them and held in the per-layout caches
|
||||
uint64_t slotTrims = 0; // whole-slot trims since the renderer came up
|
||||
};
|
||||
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,18 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
|
||||
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
|
||||
// non-null on every Magma role that owns a backend. See the definition for what the one
|
||||
// arm does and for the inproc gap it closes. Push-only and disaggregated-only: a pull
|
||||
// build declares no notice at all (StateObjectDeathNotice.h) and has nothing to emit.
|
||||
void InstallStateObjectDeathOps();
|
||||
// True when the notice currently installed is this backend's. Read by MG_Test so the
|
||||
// install can be asserted without a death having to happen first.
|
||||
Bool StateObjectDeathOpsInstalled();
|
||||
#endif
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
// safe because GL calls are serialized in this codebase, and any still-live
|
||||
@@ -107,6 +95,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
@@ -147,4 +137,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
|
||||
Int64 GetGpuTimestampNs();
|
||||
void Present();
|
||||
// eglSwapInterval. Remembered across renderers: one created for a later window surface
|
||||
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
|
||||
void SetSwapInterval(Int interval);
|
||||
Optional<Int> GetRequestedSwapInterval();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "BufferArena.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
|
||||
@@ -128,14 +125,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferDesc.usage = m_desc.usage;
|
||||
bufferDesc.memoryUsage = m_desc.memoryUsage;
|
||||
bufferDesc.allocationFlags = m_desc.allocationFlags;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Split staging may also be the destination of a GPU UBO range copy.
|
||||
// Persistent host writes must be coherent: Upload deliberately writes
|
||||
// the mapped pointer without a per-range VMA flush.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
|
||||
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
}
|
||||
#endif
|
||||
if (!buffer.Create(bufferDesc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "FrameContext.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
|
||||
@@ -220,13 +217,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
presentBarrier.srcAccessMask = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A layout transition writes the image too. Order it after the wire
|
||||
// render pass's storeOp, clears and copies instead of relying on the old
|
||||
// per-draw queue-idle path. This applies even when contents are discarded.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
#endif
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -238,14 +228,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
presentBarrier.subresourceRange.levelCount = 1;
|
||||
presentBarrier.subresourceRange.baseArrayLayer = 0;
|
||||
presentBarrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#else
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#endif
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
if (openedRecording) {
|
||||
@@ -307,10 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
// Under disaggregation VulkanRenderer waits every registered fence
|
||||
// through this slot's last submit before calling here. That aggregate
|
||||
// proof covers mid-frame pooled-fence flushes too; the slot fence alone
|
||||
// would not. Initialize's first acquire has no prior submissions.
|
||||
// The slot's fence has been waited: every command buffer this slot
|
||||
// submitted (including mid-frame flushes) has finished executing.
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
|
||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
||||
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
||||
// graph is unchanged (G1).
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||
// {slot, gen} mint the re-keyed sites are written against.
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
|
||||
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
|
||||
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
|
||||
//
|
||||
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
|
||||
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
|
||||
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
|
||||
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
|
||||
//
|
||||
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
|
||||
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
|
||||
// nothing outside Track H consults the legacy-memo lever at all - see
|
||||
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
|
||||
//
|
||||
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
|
||||
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
||||
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
||||
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D14's startup gate
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
|
||||
// that is actually running. It answers exactly one question and it answers it before the
|
||||
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
|
||||
//
|
||||
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
|
||||
//
|
||||
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
|
||||
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
||||
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
||||
// not kill an Espryt one.
|
||||
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
|
||||
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
|
||||
// a clear bit there simply means the client is not pushing render-state CSOs in this
|
||||
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
||||
// and 6 "Track H" and labels bit 0 nothing of the sort.
|
||||
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
||||
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
||||
// branch nobody needs on the hottest path in the backend.
|
||||
//
|
||||
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
|
||||
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
|
||||
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
|
||||
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
|
||||
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
|
||||
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
|
||||
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
||||
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
||||
inline void MagmaPipeValidateSubsystemConfiguration() {
|
||||
if (!MG_Config::Features.PipeLegacyMemos &&
|
||||
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
||||
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
||||
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
||||
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
|
||||
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
|
||||
"to measure it.",
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemRenderState));
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
||||
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||
if (MG_Config::Features.PipeLegacyMemos) return;
|
||||
#endif
|
||||
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
||||
#else
|
||||
const char* const why =
|
||||
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
||||
#endif
|
||||
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
||||
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
||||
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
||||
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
|
||||
why,
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return MagmaPipeSubsystemOn(trackHBit);
|
||||
#else
|
||||
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||
// running process can be on.
|
||||
(void)trackHBit;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||
// and a control that defeats two of three guards proves nothing.
|
||||
//
|
||||
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||
//
|
||||
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||
//
|
||||
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||
// which is exactly the vacuity this control exists to catch.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||
//
|
||||
// * THIS MINT still has no death notification, and that is now a narrower statement than
|
||||
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
|
||||
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
|
||||
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
|
||||
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
|
||||
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
|
||||
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
|
||||
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
|
||||
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
|
||||
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
|
||||
// from that is unchanged;
|
||||
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||
// across frames by design. The two requirements are mutually exclusive, so the
|
||||
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||
//
|
||||
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||
// suite; it is the only place in the tree where that deletion is caught.
|
||||
//
|
||||
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||
// "every safety net was switched off until something broke".
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||
//
|
||||
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||
// and with no `default:` there is no arm for it to read true from.
|
||||
//
|
||||
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||
// and could not make anything red or green.)
|
||||
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||
switch (kind) {
|
||||
// The two MagmaPipeIdentityTables really mints.
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
case MG_Pipe::MGPipeKind::Buffer:
|
||||
return true;
|
||||
// P4a's six object classes: still reached from their frontend objects on this
|
||||
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||
// the knob to defeat.
|
||||
case MG_Pipe::MGPipeKind::Texture:
|
||||
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||
// ...and everything else this backend does not mint a handle for.
|
||||
case MG_Pipe::MGPipeKind::None:
|
||||
case MG_Pipe::MGPipeKind::Xfb:
|
||||
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||
case MG_Pipe::MGPipeKind::Fence:
|
||||
case MG_Pipe::MGPipeKind::Query:
|
||||
case MG_Pipe::MGPipeKind::Context:
|
||||
case MG_Pipe::MGPipeKind::KindCount:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||
"defeat for it");
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||
"carries the answer into the lane");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The {slot, gen} mint
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||
// properties, and the third is the one review v2 got wrong:
|
||||
//
|
||||
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||
// never match a live object even if the allocator hands back the same heap address
|
||||
// (the ABA HandleRecycleScenario reproduces);
|
||||
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||
// no probe and no mix;
|
||||
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||
// drawn, whatever the working set size.
|
||||
//
|
||||
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||
//
|
||||
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||
//
|
||||
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||
// plus one map node per object EVER created, for the life of the process, on a
|
||||
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||
// ~1024 frames, never to a hot one.
|
||||
//
|
||||
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||
// no capacity to size off a device measurement because there is no capacity; what the
|
||||
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||
// draw - ROADMAP.md:7).
|
||||
//
|
||||
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||
// clock, across two live contexts.
|
||||
class MagmaPipeIdentityTable {
|
||||
public:
|
||||
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||
|
||||
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||
// Objects currently holding a slot - the live working set this table tracks.
|
||||
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||
|
||||
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||
// that a zero can never be minted into a slot and then indexed with.
|
||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||
// that has been handed back to the free list.
|
||||
if (lifetimeId == m_lastLifetimeId) {
|
||||
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||
return m_lastHandle;
|
||||
}
|
||||
Uint32 index = 0;
|
||||
const auto it = m_index.find(lifetimeId);
|
||||
if (it != m_index.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = ClaimSlot();
|
||||
m_entries[index].LifetimeId = lifetimeId;
|
||||
m_index.emplace(lifetimeId, index);
|
||||
}
|
||||
Entry& entry = m_entries[index];
|
||||
entry.LastUse = m_boundary;
|
||||
m_lastLifetimeId = lifetimeId;
|
||||
m_lastIndex = index;
|
||||
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||
entry.Gen};
|
||||
return m_lastHandle;
|
||||
}
|
||||
|
||||
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||
// slots' consumers use.
|
||||
void OnFrameBoundary() {
|
||||
++m_boundary;
|
||||
if ((m_boundary % kSweepInterval) != 0) return;
|
||||
SizeT retired = 0;
|
||||
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||
Entry& entry = m_entries[it->second];
|
||||
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||
entry.LifetimeId = 0;
|
||||
m_freeSlots.push_back(it->second);
|
||||
it = m_index.erase(it);
|
||||
++retired;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (retired != 0) {
|
||||
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||
// so a front memo pointing at one would still hand out a handle the consumer
|
||||
// tables would accept. Drop it.
|
||||
m_lastLifetimeId = 0;
|
||||
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||
m_kindName, retired, LiveCount(), Count());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Sweep cadence and retirement age, deliberately the same numbers
|
||||
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||
// entry is still live and still correct, which is a pure waste.
|
||||
static constexpr Uint64 kSweepInterval = 256;
|
||||
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
|
||||
struct Entry {
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 LastUse = 0;
|
||||
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||
// its generation, which is what makes a memo survive a reconfiguration.
|
||||
Uint32 Gen = 0;
|
||||
};
|
||||
|
||||
Uint32 ClaimSlot() {
|
||||
while (!m_freeSlots.empty()) {
|
||||
const Uint32 index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||
// stale handle match a live object. It costs one slot.
|
||||
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||
"retired for good; {slot, gen} stays unique",
|
||||
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
continue;
|
||||
}
|
||||
++m_entries[index].Gen;
|
||||
return index;
|
||||
}
|
||||
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||
m_entries.push_back(Entry{});
|
||||
m_entries[index].Gen = 1;
|
||||
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||
//
|
||||
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||
// and it cannot read a line that was compiled away.
|
||||
const SizeT minted = m_entries.size();
|
||||
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||
m_kindName, minted, LiveCount());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* m_kindName = "";
|
||||
Uint64 m_boundary = 0;
|
||||
Vector<Entry> m_entries;
|
||||
Vector<Uint32> m_freeSlots;
|
||||
UnorderedMap<Uint64, Uint32> m_index;
|
||||
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||
// object's lifetime id is never 0.
|
||||
Uint64 m_lastLifetimeId = 0;
|
||||
Uint32 m_lastIndex = 0;
|
||||
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||
class MagmaPipeIdentityTables {
|
||||
public:
|
||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||
: m_vaos.Acquire(lifetimeId);
|
||||
}
|
||||
void OnFrameBoundary() {
|
||||
m_vaos.OnFrameBoundary();
|
||||
m_buffers.OnFrameBoundary();
|
||||
}
|
||||
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||
|
||||
private:
|
||||
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||
};
|
||||
|
||||
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||
//
|
||||
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||
? 0u
|
||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||
}
|
||||
|
||||
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||
//
|
||||
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||
// it replaces gave, without the fixed capacity.
|
||||
template <typename T, Uint32 kChunkEntries = 256>
|
||||
class MagmaPipeSlotTable {
|
||||
public:
|
||||
T& operator[](Uint32 index) {
|
||||
const Uint32 chunk = index / kChunkEntries;
|
||||
while (m_chunks.size() <= chunk) {
|
||||
m_chunks.push_back(MakeUnique<Chunk>());
|
||||
}
|
||||
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||
}
|
||||
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
T Entries[kChunkEntries] = {};
|
||||
};
|
||||
Vector<UniquePtr<Chunk>> m_chunks;
|
||||
};
|
||||
|
||||
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||
// this code rather than a copy of it.
|
||||
//
|
||||
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||
//
|
||||
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||
// slot" and "the slot was reused and Gen did not move".
|
||||
//
|
||||
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||
template <typename Memos, Uint32 kChunkEntries>
|
||||
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||
const MG_Pipe::MGPipeHandle& handle) {
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
return table[kMagmaPipeAbaControlSlotIndex];
|
||||
}
|
||||
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||
if (!(memos.Owner == handle)) {
|
||||
memos = Memos{};
|
||||
memos.Owner = handle;
|
||||
}
|
||||
return memos;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,224 +0,0 @@
|
||||
// MobileGL - server-owned program input for Magma (P5f fm).
|
||||
#pragma once
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include "../DirectVulkanResourceState.h"
|
||||
// P7 wave 2 package C, OQ-8: spirv_reflect.h is GONE from this header, and its absence is the
|
||||
// gate. Everything this class needed it for - the storage-block index space - now travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
|
||||
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
|
||||
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A borrowed view, valid only while its frontend object or server record lives.
|
||||
// It never constructs a ProgramObject on the server. Post-link mutable values
|
||||
// come from the record tails, not from the archive's link-time defaults.
|
||||
class MagmaProgramSource {
|
||||
public:
|
||||
using Program = MG_State::GLState::ProgramObject;
|
||||
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
|
||||
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
|
||||
: m_handle(handle), m_record(&record) {
|
||||
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
|
||||
}
|
||||
|
||||
Bool IsWire() const { return m_record != nullptr; }
|
||||
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
|
||||
const Program* Frontend() const { return m_frontend; }
|
||||
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
}
|
||||
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
|
||||
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
|
||||
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
}
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
|
||||
Vector<ShaderStage> result;
|
||||
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
|
||||
return result;
|
||||
}
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
|
||||
const auto& stages = m_record->Archive->LinkedStages;
|
||||
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
|
||||
}
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
|
||||
// A view has no lifetime in which a memo can safely persist. The server
|
||||
// factory's content cache still owns compiled programs; hash from current
|
||||
// archive + binding tails rather than borrowing the client's mutable memo.
|
||||
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
|
||||
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
|
||||
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
|
||||
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
|
||||
|
||||
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
|
||||
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
|
||||
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
}
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
|
||||
auto found = Link().uniformBlockIndexByName.find(name);
|
||||
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
|
||||
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
|
||||
}
|
||||
Uint GetUniformBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
|
||||
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
|
||||
const auto* block = Block(index);
|
||||
static const String empty;
|
||||
return block ? block->name : empty;
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
|
||||
const auto* block = Block(index);
|
||||
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
|
||||
}
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
|
||||
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
|
||||
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformType(location);
|
||||
return IsValidUniformLocation(static_cast<Int>(location))
|
||||
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
|
||||
}
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const auto& units = m_record->SamplerUnits;
|
||||
const auto found = std::lower_bound(units.begin(), units.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
|
||||
return found != units.end() && found->Location == location ? found->Unit : -1;
|
||||
}
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformLocation(name);
|
||||
const auto& locations = Link().uniformLocations;
|
||||
auto found = locations.find(name);
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.empty()) return -1;
|
||||
found = locations.find(name + "[0]");
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.back() != ']' || name.size() < 4) return -1;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.size() - 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;
|
||||
}
|
||||
found = locations.find(name.substr(0, bracket) + "[0]");
|
||||
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
|
||||
if (found == locations.end()) return -1;
|
||||
const Int base = static_cast<Int>(found->second);
|
||||
if (!IsValidUniformLocation(base)) return -1;
|
||||
const Int tIndex = Link().uniformIndexInTProgram[base];
|
||||
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
|
||||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
|
||||
const Int location = base + static_cast<Int>(element);
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
Uint32 GetTransformFeedbackStride(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
|
||||
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const {
|
||||
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
|
||||
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
|
||||
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
|
||||
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
|
||||
// function of the linked program, so it is now built once at link time and travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
|
||||
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
|
||||
Uint GetShaderStorageBlockIndex(const String& name) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
// The archive carries NORMALISED names (the array subscript stripped), because
|
||||
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
|
||||
// ProgramFactory::ReflectLayout looks up. A caller that still holds a GL-style
|
||||
// "Foo[2]" gets the same second chance it always had.
|
||||
const auto find = [&blocks](const String& key) {
|
||||
return std::find_if(blocks.begin(), blocks.end(),
|
||||
[&](const MG_State::GLState::StorageBlockReflection& block) {
|
||||
return block.name == key;
|
||||
});
|
||||
};
|
||||
auto found = find(name);
|
||||
if (found == blocks.end() && !name.empty() && name.back() == ']') {
|
||||
const auto bracket = name.rfind('[');
|
||||
if (bracket != String::npos) found = find(name.substr(0, bracket));
|
||||
}
|
||||
return found == blocks.end() ? GL_INVALID_INDEX
|
||||
: static_cast<Uint>(found - blocks.begin());
|
||||
}
|
||||
Uint GetShaderStorageBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
if (index >= blocks.size()) return 0;
|
||||
const auto& block = blocks[index];
|
||||
// THE OVERRIDE STILL WINS, and it still comes off the RECORD rather than the
|
||||
// archive: glShaderStorageBlockBinding is a post-link mutable and the archive
|
||||
// carries link-time defaults only (rule on MagmaProgramSource, and the reason
|
||||
// StorageBlockReflection::binding is documented as the DECLARED binding).
|
||||
for (const auto& override : m_record->StorageOverrides) {
|
||||
if (override.Name == block.name) return override.Binding;
|
||||
}
|
||||
return block.binding;
|
||||
}
|
||||
|
||||
private:
|
||||
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
|
||||
const MG_State::GLState::BlockReflection* Block(Uint index) const {
|
||||
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
|
||||
const Int tIndex = Link().glBlockIndexToTProgram[index];
|
||||
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
|
||||
? &Link().blockReflection[tIndex] : nullptr;
|
||||
}
|
||||
const Program* m_frontend = nullptr;
|
||||
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
|
||||
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
|
||||
// were `mutable` precisely because this class is a borrowed const view - and that is
|
||||
// also why they never helped: a view lives for one draw, so the latch was cold every
|
||||
// time it was read.
|
||||
};
|
||||
#else
|
||||
using MagmaProgramSource = MG_State::GLState::ProgramObject;
|
||||
#endif
|
||||
}
|
||||
@@ -198,17 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (payload.wireRenderPassCompatibilityId != 0) {
|
||||
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
|
||||
sizeof(payload.wireRenderPassCompatibilityId)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
@@ -285,14 +275,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<unsigned long long>(payload.programHash));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Wire pipelines outlive their creator pass and
|
||||
// may serve another compatible pass in flight.
|
||||
// Never evict them by a recycled native handle.
|
||||
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
|
||||
#endif
|
||||
payload.renderPass,
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||
m_frameCounter});
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType vertexInputHash = 0;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Nonzero is an exact, renderer-lifetime compatibility identity.
|
||||
// renderPass remains the valid creation handle, not the cache key.
|
||||
Uint64 wireRenderPassCompatibilityId = 0;
|
||||
#endif
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
|
||||
@@ -1891,7 +1891,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
const MagmaProgramSource& program) {
|
||||
const MG_State::GLState::ProgramObject& program) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
@@ -2466,7 +2466,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program,
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
// We expect shader stages in program object are sorted
|
||||
@@ -2832,7 +2832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// key and every element of an array of arrays seeds texture unit 0. Resolving those elements
|
||||
// would then paint silently-wrong pixels with no diagnostic at all - strictly worse than
|
||||
// declining. The decline goes away together with the seeding fix, not before it.
|
||||
static Uint32 DescriptorCountForOpaqueUniformArray(const MagmaProgramSource& program,
|
||||
static Uint32 DescriptorCountForOpaqueUniformArray(const MG_State::GLState::ProgramObject& program,
|
||||
const String& uniformName, Uint32 binding, Int baseLocation,
|
||||
Uint32 reflectedCount, Uint32 maxBindings,
|
||||
const char* kindLabel, Bool& outDeclined) {
|
||||
@@ -2868,7 +2868,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return count;
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectLayout(const MagmaProgramSource& program,
|
||||
void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program,
|
||||
const Vector<Vector<Uint>>& spirv, VkProgramObject& entry) const {
|
||||
// Initialize layout vectors
|
||||
entry.bindingKinds.assign(m_maxBindings, DescriptorBindingKind::None);
|
||||
@@ -3068,12 +3068,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.bindingKinds[binding] = descriptorKind;
|
||||
|
||||
if (descriptorKind == DescriptorBindingKind::StorageBuffer) {
|
||||
const GLuint blockIndex =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
program.GetShaderStorageBlockIndex(uniformName);
|
||||
#else
|
||||
GetShaderStorageBlockIndex(program, uniformName);
|
||||
#endif
|
||||
const GLuint blockIndex = GetShaderStorageBlockIndex(program, uniformName);
|
||||
if (blockIndex == GL_INVALID_INDEX) {
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u",
|
||||
uniformName.c_str(), binding);
|
||||
@@ -3365,13 +3360,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
updateAfterBindSampledImages + updateAfterBindStorageImages;
|
||||
const auto& uab = m_updateAfterBindLimits;
|
||||
entry.usesUpdateAfterBind =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Vulkan forbids ANY update-after-bind binding in a set layout that
|
||||
// contains a dynamic buffer descriptor (VUID 03001/03011). Wire draws
|
||||
// retain UniformManager's per-frame, content-versioned descriptor sets;
|
||||
// this selects the legal ordinary pool, not in-place mutation of a live set.
|
||||
(!program.IsWire() || entry.dynamicBindings.empty()) &&
|
||||
#endif
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
@@ -3438,7 +3426,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags) {
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
|
||||
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
||||
// reuse the program's memoized hash while its backend state version is unchanged.
|
||||
// The memo keys on the flags word, which ComputeHash is no longer a pure function of:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
@@ -471,9 +471,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
~ProgramFactory();
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const;
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags);
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
// The default framebuffer's current image height, baked as a literal into every
|
||||
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
||||
@@ -600,7 +600,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MagmaProgramSource& program, const Vector<Vector<Uint>>& spirv,
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/SubmitFencePrefix.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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Collect every fence in the ordered submission prefix. Waiting on only the last matching
|
||||
// frame-serial record is insufficient: the same serial may have an earlier in-flight submit.
|
||||
template <typename Records, typename Fence>
|
||||
bool CollectSubmitFencePrefix(const Records& records, std::uint64_t submitIndex,
|
||||
Fence nullFence, std::vector<Fence>& fences) {
|
||||
fences.clear();
|
||||
bool foundTarget = false;
|
||||
for (const auto& record : records) {
|
||||
if (record.submitIndex > submitIndex) {
|
||||
break;
|
||||
}
|
||||
if (record.fence == nullFence) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
fences.push_back(record.fence);
|
||||
foundTarget = foundTarget || record.submitIndex == submitIndex;
|
||||
}
|
||||
if (!foundTarget) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -8,14 +8,11 @@
|
||||
|
||||
#include "SwapchainObject.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<vulkan/vk_enum_string_helper.h>)
|
||||
#include <vulkan/vk_enum_string_helper.h>
|
||||
@@ -140,12 +137,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes) {
|
||||
for (auto desiredPresentMode : s_desiredPresentModes) {
|
||||
for (const auto& presentMode : availablePresentModes) {
|
||||
if (presentMode == desiredPresentMode) {
|
||||
return presentMode;
|
||||
}
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
|
||||
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
|
||||
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
|
||||
VK_PRESENT_MODE_FIFO_KHR};
|
||||
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
|
||||
if (swapInterval) {
|
||||
if (*swapInterval == 0) {
|
||||
ranked = s_unthrottledPresentModes;
|
||||
} else if (*swapInterval > 0) {
|
||||
ranked = s_vsyncPresentModes;
|
||||
} else {
|
||||
ranked = s_adaptiveVsyncPresentModes;
|
||||
}
|
||||
}
|
||||
for (auto desiredPresentMode : ranked) {
|
||||
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
|
||||
availablePresentModes.end()) {
|
||||
return desiredPresentMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,9 +164,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return availablePresentModes[0];
|
||||
}
|
||||
|
||||
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
|
||||
return string_VkPresentModeKHR(presentMode);
|
||||
}
|
||||
|
||||
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
|
||||
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
|
||||
VkExtent2D desiredExtent) {
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
|
||||
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
|
||||
"SwapchainObject::Create failed: incomplete swapchain capabilities");
|
||||
@@ -174,8 +189,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
|
||||
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
|
||||
m_presentMode = presentMode;
|
||||
m_supportedPresentModes = swapchainCapabilities.presentModes;
|
||||
|
||||
const auto& swapchainCaps = swapchainCapabilities.capabilities;
|
||||
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
|
||||
@@ -285,6 +302,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<SizeT>(defaultFramebufferExtent.width) *
|
||||
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
|
||||
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
switch (m_depthStencilFormat) {
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
@@ -300,6 +322,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
break;
|
||||
}
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
// The default FBO's stencil attachment must track the swapchain extent:
|
||||
// FramebufferObject::CheckCompleteness requires every valid attachment
|
||||
@@ -319,44 +346,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilFormat = depthFormat;
|
||||
break;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
|
||||
// attachments are CLIENT memory and this thread may not write them - the backend
|
||||
// fills MGPSurfaceInfo and posts, and the client's consumer replays exactly the
|
||||
// allocate/format writes of the monolith arm below on the GL thread. The two
|
||||
// formats are always equal by the switches above, so one InternalFormat carries
|
||||
// both. The callback's presence IS the transport probe - the server session
|
||||
// installs it at Accept, and under monolith nobody ever does.
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = defaultFramebufferExtent.width;
|
||||
info.Height = defaultFramebufferExtent.height;
|
||||
info.InternalFormat = static_cast<Uint32>(depthFormat);
|
||||
info.Samples = 1;
|
||||
info.Layers = 1;
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
}
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
|
||||
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
|
||||
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
|
||||
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
|
||||
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
|
||||
// vsync) wants FIFO_RELAXED, else FIFO.
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
|
||||
Optional<Int> swapInterval = Nullopt);
|
||||
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
|
||||
|
||||
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
|
||||
Optional<Int> swapInterval);
|
||||
void Shutdown(VkDevice device);
|
||||
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
|
||||
// The surface's present modes as queried when this swapchain was created.
|
||||
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
@@ -79,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
Vector<VkPresentModeKHR> m_supportedPresentModes;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "VkSamplerManager.h"
|
||||
#include "VkTextureManager.h"
|
||||
#include "WirePlaceholderKey.h"
|
||||
#include "../DescriptorPoolCensus.h"
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
|
||||
@@ -55,25 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A present-less wire frame may run arbitrarily many draws. Before its
|
||||
// descriptor cursor reaches this budget, the renderer retires the
|
||||
// submission that last used the sets and rewinds every layout cursor.
|
||||
static constexpr Uint32 kWireDescriptorSetBudget = 2048;
|
||||
Bool WireDescriptorSetBudgetReached(Uint32 frameIndex) const;
|
||||
SizeT RewindWireDescriptorSets(Uint32 frameIndex);
|
||||
// How the wire arm answers a STORAGE image unit that GL 4.6 core 8.26 makes invalid (no
|
||||
// texture, or a (level, layer) its texture lacks): a load returns zero, a store and an
|
||||
// atomic are discarded. `deviceNullDescriptor` is the renderer's enablement of
|
||||
// VK_EXT_robustness2's nullDescriptor at device creation. With it the unit binds a NULL
|
||||
// storage-image descriptor, which is exactly that rule. Without it (or under
|
||||
// MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER=1, the host lanes' way onto this arm) the
|
||||
// unit binds a placeholder PRIVATE to its unit (per shape), cleared before each use, so no
|
||||
// two units ever alias; what that cannot give is a load of the SAME unit after its own
|
||||
// store inside one pass reading zero, nor privacy past kWirePrivateStoragePlaceholderCap
|
||||
// placeholders (CONTRACT-P7 §12). Called after Initialize.
|
||||
void SetWireInvalidStorageImageArm(Bool deviceNullDescriptor);
|
||||
#endif
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
@@ -84,9 +64,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
// references its sets. This is the only PER-LAYOUT eviction path for the
|
||||
// caches - a live layout's entry must never be purged on its own (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here. The
|
||||
// other path, TrimFrameDescriptorPools, drops a slot's whole cache together with
|
||||
// the pools behind it.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
|
||||
// in binding order, and within a binding in array-element order): the resolved texture
|
||||
@@ -99,7 +81,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MagmaProgramSource& program,
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
@@ -108,25 +90,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
|
||||
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve lazy texture uploads/promotions before the caller captures the
|
||||
// command buffer passed to BindProgramUniformBuffers. Preparation may
|
||||
// submit older work, but descriptor recording must never rotate it.
|
||||
Bool PrepareWireTextureResources(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
#endif
|
||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||
// every input of every combined-image-sampler resolution is unchanged since the
|
||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||
@@ -134,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// epochs AND per-resource layout values - so the per-binding cached
|
||||
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
@@ -155,16 +130,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
private:
|
||||
// ---- per-frame descriptor pools ----------------------------------------------------
|
||||
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
|
||||
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
|
||||
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
|
||||
// updateAfterBind flavour, because a set can only come from a pool created with the flag
|
||||
// its layout was.
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
|
||||
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
|
||||
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
|
||||
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
|
||||
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
|
||||
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
|
||||
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
|
||||
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
|
||||
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
|
||||
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
|
||||
// smallest one, and doubling IT made every grown pool the same size.
|
||||
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
|
||||
Uint32 initialMaxSets);
|
||||
// Trim policy (see TrimFrameDescriptorPools). A slot is "quiet" in an epoch whose peak set
|
||||
// count times kDescriptorTrimHeadroom still fits its capacity; a slot that has been quiet for
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES consecutive epochs, and holds at least
|
||||
// kDescriptorTrimMinCapacity sets, is trimmed. The headroom is what keeps the trim from
|
||||
// thrashing: geometric growth leaves a re-grown slot at least a third full (the pool a growth
|
||||
// adds is at most twice the largest before it, so at most twice everything the slot held, and
|
||||
// the epoch that grew it needed more than that), so the epoch that grew a slot is never quiet
|
||||
// for it. The floor keeps a slot sized for ordinary
|
||||
// frames from being trimmed at all - the trim exists to give back a burst.
|
||||
static constexpr Uint32 kDescriptorTrimHeadroom = 4;
|
||||
static constexpr Uint32 kDescriptorTrimMinCapacity = 1024;
|
||||
// For tests (DescriptorPoolCensus.h): pools, capacity, cached sets and trims over every slot.
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() const;
|
||||
|
||||
private:
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
@@ -179,82 +187,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 cursor = 0;
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ResolveWireImageDescriptor emits no pNext chain. Key every view-create
|
||||
// value explicitly: hash collisions must never alias different windows,
|
||||
// formats or swizzles. The root handle and allocation epoch also prevent
|
||||
// a recycled native image handle from reviving an older view.
|
||||
struct WireImageViewKey {
|
||||
MG_Pipe::MGPipeHandle root{};
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
Uint64 imageEpoch = 0;
|
||||
VkImageViewCreateFlags flags = 0;
|
||||
VkImageViewType type = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkComponentMapping components{};
|
||||
VkImageSubresourceRange range{};
|
||||
|
||||
Bool operator==(const WireImageViewKey& other) const {
|
||||
return root == other.root && image == other.image && imageEpoch == other.imageEpoch &&
|
||||
flags == other.flags && type == other.type && format == other.format &&
|
||||
components.r == other.components.r && components.g == other.components.g &&
|
||||
components.b == other.components.b && components.a == other.components.a &&
|
||||
range.aspectMask == other.range.aspectMask && range.baseMipLevel == other.range.baseMipLevel &&
|
||||
range.levelCount == other.range.levelCount && range.baseArrayLayer == other.range.baseArrayLayer &&
|
||||
range.layerCount == other.range.layerCount;
|
||||
}
|
||||
};
|
||||
|
||||
struct WireImageViewKeyHash {
|
||||
SizeT operator()(const WireImageViewKey& key) const;
|
||||
};
|
||||
|
||||
struct WirePlaceholderImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
VkRenderPass clearPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer clearFramebuffer = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Uint32 layers = 1;
|
||||
};
|
||||
// Keyed by shape, and a storage placeholder on the arm without a null descriptor by its
|
||||
// image unit too (WirePlaceholderKey.h: ChooseWirePlaceholderKey, and its cap).
|
||||
mutable UnorderedMap<WirePlaceholderKey, WirePlaceholderImage, WirePlaceholderKeyHash> m_wirePlaceholderImages;
|
||||
// How many of those are private to a unit (against kWirePrivateStoragePlaceholderCap).
|
||||
mutable SizeT m_wirePrivateStoragePlaceholders = 0;
|
||||
// See SetWireInvalidStorageImageArm.
|
||||
Bool m_wireInvalidStorageImagesBindNull = false;
|
||||
// boundStorageFormat: for a storage binding with no reflected format, the format the
|
||||
// unit's glBindImageTexture named, when the unit holds a texture it cannot address
|
||||
// (GL 4.6 core 8.26); UNDEFINED keeps the numeric-domain R32 default. `unit` is the image
|
||||
// unit of a storage binding (what its private placeholder is private to); unused for a sampler.
|
||||
Bool ResolveWirePlaceholderImage(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
|
||||
VkDescriptorImageInfo& out, VkFormat boundStorageFormat = VK_FORMAT_UNDEFINED, Uint32 unit = ~0u) const;
|
||||
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
|
||||
#endif
|
||||
|
||||
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
|
||||
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
|
||||
// the cache only ever grows - except through TrimFrameDescriptorPools, which gives a slot's
|
||||
// whole burst back at once (and OnDescriptorSetLayoutDestroyed, one dead layout at a time).
|
||||
struct FrameResources {
|
||||
Vector<DescriptorPoolBucket> descriptorPools;
|
||||
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
|
||||
Vector<VkBufferView> texelBufferViews;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
mutable Vector<VkImageView> wireImageViews;
|
||||
// Own views in the vector above until this slot's fence/idle proof.
|
||||
// Cache hits preserve the handle, enabling descriptor-content reuse.
|
||||
mutable UnorderedMap<WireImageViewKey, VkImageView, WireImageViewKeyHash> wireImageViewCache;
|
||||
#endif
|
||||
Uint32 activeDescriptorPoolIndex = 0;
|
||||
Uint32 allocatedSetsThisFrame = 0;
|
||||
Uint32 peakAllocatedSetsThisFrame = 0;
|
||||
// Sum of maxSets over descriptorPools, kept in step by Initialize, Grow and Trim.
|
||||
Uint64 capacitySets = 0;
|
||||
// Consecutive epochs (see NoteEndedEpochForTrim) that were quiet for THIS slot's capacity.
|
||||
Uint32 quietEpochs = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
@@ -263,7 +213,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||
Bool ResolveSampledBinding(const MagmaProgramSource& program,
|
||||
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
@@ -271,7 +221,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
@@ -318,27 +268,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||
// obligations the caller carries). The cache is keyed by binding alone, so it is
|
||||
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint = false) const;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
|
||||
Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, Bool storage, VkBufferView& out);
|
||||
Uint32 m_wireFrameIndex = 0;
|
||||
VkDeviceSize m_wireStorageOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireTexelOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0};
|
||||
VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0};
|
||||
Uint32 m_wireMaxTexelElements = ~Uint32{0};
|
||||
#endif
|
||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
|
||||
@@ -346,19 +282,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
|
||||
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
|
||||
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
|
||||
Bool ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||
// binding point, buffer and glBindBufferRange window.
|
||||
Bool ResolveStorageBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
// `element` indexes an image ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL image unit.
|
||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
|
||||
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
|
||||
@@ -371,18 +307,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* payload = nullptr; // fallback UploadTransient path
|
||||
VkDeviceSize payloadSize = 0;
|
||||
};
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex,
|
||||
Uint32 bindingPoint, UboBindResult& out) const;
|
||||
#endif
|
||||
Bool ResolveUniformBufferPayload(const MagmaProgramSource& program,
|
||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, UboBindResult& out) const;
|
||||
// Shared resolution of one dynamic-UBO binding element into the
|
||||
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
|
||||
// bind, global-slice reuse, or transient upload. Used by the full walk
|
||||
// and by the dynamic-offset-only rebind (see FastRebindMemo).
|
||||
Bool ResolveDynamicUboDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
|
||||
VkDeviceSize& outRange, Uint32& outDynamicOffset);
|
||||
@@ -395,6 +327,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
// An epoch is the stretch between two BeginFrame calls: one frame, or the part of one between
|
||||
// two drains (VulkanRenderer::TryDrainFrameTransients). Called by BeginFrame with the epoch
|
||||
// that just ended, it updates every slot's quiet count - a burst in any slot counts against
|
||||
// all of them, so bursts that land in the other slots still hold a slot's trim off.
|
||||
void NoteEndedEpochForTrim(Uint32 epochPeakSets);
|
||||
// Gives a slot's whole descriptor burst back: clears its per-layout cache, destroys every
|
||||
// grown pool and resets the base pool, so the slot re-grows geometrically from setsPerFrame.
|
||||
// Whole-slot rather than per-set: vkFreeDescriptorSets fragments a pool and can never empty
|
||||
// a grown one. Called only from BeginFrame, which runs only when no pending command buffer
|
||||
// references the slot's sets:
|
||||
// - Present path (VulkanRenderer::Present, after WaitAndAcquireNextImage): the slot's
|
||||
// imageInFlightFence has been waited. That fence rides the slot's LAST submission - the
|
||||
// Present submit - and every mid-frame submission of the slot (FlushPendingCommands,
|
||||
// SplitOversizedRecording, readbacks) went to the same queue earlier, so the fence's
|
||||
// signal covers them (vkQueueSubmit's fence scope includes all earlier submissions).
|
||||
// - Drain path (VulkanRenderer::TryDrainFrameTransients): every submission has been
|
||||
// observed complete and no recording is open.
|
||||
// Sets are per slot: a slot's sets are only ever bound into recordings made while it is the
|
||||
// current slot, so trimming one slot never touches another slot's in-flight work.
|
||||
void TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex);
|
||||
// Drops every handle this class holds to a descriptor set outside the per-layout caches.
|
||||
void InvalidateDescriptorSetMemos();
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
@@ -412,6 +366,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_maxBindings = 0;
|
||||
Uint32 m_setsPerFrame = 0;
|
||||
Uint32 m_peakDescriptorSetsObserved = 0;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES, latched at Initialize (0 = never trim).
|
||||
Uint32 m_trimQuietEpochs = 0;
|
||||
// The slot whose epoch is open (the last one BeginFrame was called for); valid once
|
||||
// m_epochOpen is set. Its peak is the ended epoch's peak at the next BeginFrame.
|
||||
Uint32 m_epochSlot = 0;
|
||||
Bool m_epochOpen = false;
|
||||
Uint64 m_descriptorSlotTrims = 0;
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
@@ -472,15 +433,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||
// advances, so the recorded set is never re-written within its frame.
|
||||
// P7 M2 round 2 (ID-P7-43): a WIRE store can die MID-FRAME on the reclaim
|
||||
// path (VkBufferManager::DeferredWireRelease), and the next mint can hand
|
||||
// its VkBuffer handle value back - a heap pointer under lavapipe - so "the
|
||||
// same VkBuffer+range" is no longer proof of the same store. The memo
|
||||
// therefore records VkBufferManager::GetWireStoreDestroyEpoch() and misses
|
||||
// once any wire store has been destroyed since; the descriptor-reuse
|
||||
// signature below folds the same epoch in. On the wire arm this memo is
|
||||
// never consulted (SetupWireDraw passes no sampler hint), so the field is
|
||||
// the defensive half; the signature is the half the wire arm reaches.
|
||||
struct FastRebindMemo {
|
||||
Bool valid = false;
|
||||
Uint32 frameIndex = 0;
|
||||
@@ -490,9 +442,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize uboRange = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
};
|
||||
FastRebindMemo m_fastRebindMemo;
|
||||
|
||||
|
||||
@@ -7,13 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -50,149 +45,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
//
|
||||
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
|
||||
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
|
||||
// into every server-side content hash". The two are equally ABA-proof (the
|
||||
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
|
||||
// changes is that the key is now the identity the SERVER will be handed once
|
||||
// buffers travel as handles, instead of a number only the client can mint.
|
||||
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (attr.Buffer) {
|
||||
// The SAME arm question the other four re-keyed sites ask, through the same
|
||||
// helper: a site that decided for itself could silently key on the pre-handle
|
||||
// identity while its neighbours keyed on the handle.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||
}
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
|
||||
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
|
||||
// wearing two hats, and a control that defeated only the retired one would
|
||||
// say nothing about the key P2 ships.
|
||||
//
|
||||
// The identity is replaced by a constant rather than by the raw
|
||||
// BufferObject*, because the address is not recycled in practice and so
|
||||
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
|
||||
// key with NO buffer identity in it looks like - the exact defect this
|
||||
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
|
||||
// accepts as proof that a memoised binding still reads the buffer it was
|
||||
// resolved from", and with the identity gone it accepts a binding resolved
|
||||
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
|
||||
// replacement VAO and gets its dead predecessor's vertex data.
|
||||
bufferKey = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||
//
|
||||
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Uint64& outHash) const {
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||
outHash = memos.Hash;
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
HashType hash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||
return memos.Hash;
|
||||
}
|
||||
hash = ComputeHash(vao);
|
||||
memos.Hash = hash;
|
||||
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||
return hash;
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
if (!vao.GetBackendHashMemo(hash)) {
|
||||
hash = ComputeHash(vao);
|
||||
vao.SetBackendHashMemo(hash);
|
||||
}
|
||||
#endif
|
||||
return hash;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||
// memo does not change that.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||
memos.StateEpoch == m_evictionEpoch) {
|
||||
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *memoEntry;
|
||||
}
|
||||
const BackendVertexInputState& resolved =
|
||||
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||
// the reference is not worth holding across a call that can resize the table.
|
||||
VaoBackendMemos& stamp = MemosFor(vao);
|
||||
stamp.State = &resolved;
|
||||
stamp.StateEpoch = m_evictionEpoch;
|
||||
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||
return resolved;
|
||||
}
|
||||
#endif
|
||||
#if !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||
// time true, so the handle arm above always returns. Written out rather than left to
|
||||
// fall off the end so the function still has a return on every path a compiler sees.
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
#else
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
@@ -212,7 +83,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||
return entry;
|
||||
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
@@ -250,16 +120,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// module, so a float32 stream would be fed to a Float64 input.
|
||||
const Bool narrowFloat64Arrays =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
|
||||
// SERVER's own backend's - the client caps mirror is client memory (rule E).
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
||||
@@ -435,146 +295,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return entry;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
|
||||
out = BackendVertexInputState{};
|
||||
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
|
||||
const auto& attr = elements.Attributes[location];
|
||||
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-layout`: THE ONE STRING BECAME TWO
|
||||
// VERDICTS, and the split is by what the reason is ABOUT rather than by severity.
|
||||
//
|
||||
// Every reason here used to return false, and the single caller answered that with
|
||||
// one P7-marked MagmaWireFatal - an abort, bypassing Session::Fail, invisible
|
||||
// to the census gate and with no equivalent on the monolith arm at all. But the
|
||||
// reasons are not one kind of thing:
|
||||
//
|
||||
// * `buffer-window` is a statement about the RECORD: an enabled attribute the
|
||||
// program reads names a slot outside the window set_vertex_buffers published.
|
||||
// Nothing about the device or the format is involved; the two halves of the
|
||||
// protocol disagree about what crossed. It stays a named Fatal through the hook
|
||||
// (see the caller), and it is load-bearing rather than defensive - it is what
|
||||
// caught the Redmi VAO hash collision fixed in 376c04be.
|
||||
//
|
||||
// * every other reason is a statement about what VULKAN CAN EXPRESS on this
|
||||
// device: a GL type with no VkFormat, a format without
|
||||
// VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT, a component size we cannot compute, an
|
||||
// offset sum that does not fit. The monolith arm has had an answer for these
|
||||
// since it was written, and it is not an abort: mask the attribute out of the
|
||||
// vertex input state and carry on (the `unsupportedAttribMask |= ...; continue;`
|
||||
// sites in GetOrCreateVertexInputState above). The draw path then decides,
|
||||
// loudly and once, whether the masked attribute was one the program actually
|
||||
// reads. This arm now does exactly that, which is what "same observable as the
|
||||
// monolith lane" means for this row.
|
||||
//
|
||||
// THE COUNT: the contract says "the other six"; there are SEVEN return sites below
|
||||
// it, because `offset-overflow` was added after the audit's count and is an
|
||||
// arithmetic guard rather than a shape one. It is masked with the rest: an
|
||||
// attribute whose base offsets cannot be added is one Vulkan cannot fetch, the
|
||||
// monolith arm never computes that sum at all, and masking is strictly safer than
|
||||
// a Fatal for a number no application can reach on purpose. Recorded in
|
||||
// notes/p7/magma-c.md rather than silently reconciled.
|
||||
const auto describe = [&](const char* reason, VkFormat format) {
|
||||
MGLOG_E_ONCE("Magma wire vertex layout: %s location=%u type=%u size=%u normalized=%u integer=%u long=%u bgra=%u stride=%d offset=%llu format=%d bufferWindow=%u+%u activeMask=0x%x",
|
||||
reason, location, attr.Type, static_cast<Uint32>(attr.Size), static_cast<Uint32>(attr.Normalized),
|
||||
static_cast<Uint32>(attr.IsInteger), static_cast<Uint32>(attr.IsLong), static_cast<Uint32>(attr.IsBgra),
|
||||
attr.Stride, static_cast<unsigned long long>(attr.Offset), static_cast<Int>(format),
|
||||
state.VertexBufferStart, state.VertexBufferCount, activeMask);
|
||||
};
|
||||
// The protocol verdict: the caller turns a false into the named Fatal.
|
||||
const auto reject = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
return false;
|
||||
};
|
||||
// The device verdict: MGLOG_E_ONCE + mask + continue, the monolith arm's shape.
|
||||
// MGLOG_E_ONCE and not MGLOG_E, also the monolith arm's: an unmappable attribute is
|
||||
// a property of the VAO and the device, so it repeats every draw, and the per-draw
|
||||
// line is what made the old abort look preferable to whoever wrote it.
|
||||
const auto maskOut = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
out.unsupportedAttribMask |= 1u << location;
|
||||
};
|
||||
// set_vertex_buffers is flattened PER ATTRIBUTE (VertexInputEmit.h), not
|
||||
// indexed by the original ARB binding point in attr.BindingIndex.
|
||||
if (location < state.VertexBufferStart ||
|
||||
location - state.VertexBufferStart >= state.VertexBufferCount) return reject("buffer-window");
|
||||
const auto& buffer = state.VertexBuffers[location];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
if (attr.Stride < 0 || attr.Size < 1 || attr.Size > 4) { maskOut("attribute-shape"); continue; }
|
||||
VkFormat format = ToVkVertexFormat(type, attr.Size, attr.Normalized, attr.IsInteger,
|
||||
attr.IsBgra, attr.IsLong);
|
||||
auto conversion = VertexStreamConversion::None;
|
||||
if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) {
|
||||
const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
if (backend && backend->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
// Exactly the monolith arm's answer for the same state: with native fp64
|
||||
// the module KEPT its 64-bit inputs (DemoteFloat64Pass did not run), so
|
||||
// narrowing the stream would feed float32 to a Float64 input - and the
|
||||
// monolith build therefore leaves sourceVkFormat UNDEFINED and falls into
|
||||
// its own mask-out below. Same place, same mask.
|
||||
maskOut("native-fp64-format");
|
||||
continue;
|
||||
}
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED) { maskOut("format-map"); continue; }
|
||||
if (!SupportsVertexBufferFormat(format)) {
|
||||
if (!IsScaledIntegerVertexFormat(format)) { maskOut("native-format-feature", format); continue; }
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
if (!SupportsVertexBufferFormat(format)) { maskOut("converted-format-feature", format); continue; }
|
||||
}
|
||||
const SizeT elementSize = GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (!elementSize) { maskOut("element-size", format); continue; }
|
||||
const Uint64 offset = attr.Offset + buffer.Offset;
|
||||
if (offset < attr.Offset) { maskOut("offset-overflow", format); continue; }
|
||||
const SizeT alignment = (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev)
|
||||
? elementSize : GetComponentSize(type);
|
||||
if (conversion == VertexStreamConversion::None && alignment > 1 &&
|
||||
(offset % alignment || static_cast<Uint32>(attr.Stride) % alignment))
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
Uint32 stride = static_cast<Uint32>(attr.Stride);
|
||||
if (stride && conversion != VertexStreamConversion::None)
|
||||
stride = conversion == VertexStreamConversion::Repack ? static_cast<Uint32>(elementSize)
|
||||
: static_cast<Uint32>(attr.Size) * sizeof(Float);
|
||||
const Uint32 binding = static_cast<Uint32>(out.bindings.size());
|
||||
out.bindings.push_back({binding, stride,
|
||||
buffer.Divisor ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
out.attributes.push_back({location, binding, format, 0});
|
||||
out.bindingAttributeLocations.push_back(location);
|
||||
out.bindingBaseOffsets.push_back(static_cast<SizeT>(offset));
|
||||
out.bindingConversions.push_back(conversion);
|
||||
if (buffer.Divisor > 1) out.bindingDivisors.push_back({binding, buffer.Divisor});
|
||||
out.attributeLocationMask |= 1u << location;
|
||||
}
|
||||
// Native handles/offsets do not decide the pipeline layout. Include only the
|
||||
// resolved binding/attribute/divisor values, including a legal zero stride.
|
||||
Uint64 hash = XXH64(out.bindings.data(), out.bindings.size() * sizeof(out.bindings[0]), 0);
|
||||
hash = XXH64(out.attributes.data(), out.attributes.size() * sizeof(out.attributes[0]), hash);
|
||||
hash = XXH64(out.bindingDivisors.data(),
|
||||
out.bindingDivisors.size() * sizeof(out.bindingDivisors[0]), hash);
|
||||
// THE MASK IS PART OF THE LAYOUT NOW, for the same reason the monolith entry's hash
|
||||
// carries it (the XXH64_update over unsupportedAttribMask in GetOrCreateVertexInputState
|
||||
// above). Before this package the mask was always zero here, so leaving it out of the
|
||||
// hash was free; now two VAOs can produce the SAME bindings, attributes and divisors
|
||||
// and differ only in which enabled attribute was masked out, and a hash blind to that
|
||||
// would serve one of them the other's pipeline.
|
||||
out.layoutHash = XXH64(&out.unsupportedAttribMask, sizeof(out.unsupportedAttribMask), hash);
|
||||
out.state.vertexBindingDescriptionCount = static_cast<Uint32>(out.bindings.size());
|
||||
out.state.pVertexBindingDescriptions = out.bindings.data();
|
||||
out.state.vertexAttributeDescriptionCount = static_cast<Uint32>(out.attributes.size());
|
||||
out.state.pVertexAttributeDescriptions = out.attributes.data();
|
||||
if (!out.bindingDivisors.empty()) {
|
||||
out.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(out.bindingDivisors.size());
|
||||
out.divisorState.pVertexBindingDivisors = out.bindingDivisors.data();
|
||||
out.state.pNext = &out.divisorState;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
@@ -596,14 +316,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment). With no legacy arm the memos
|
||||
// live in this factory and die with it, so a per-instance bump is
|
||||
// enough - P2 D12.5.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// instances (see the member comment).
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
++m_evictionEpoch;
|
||||
#endif
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
@@ -653,17 +367,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// demoted `vec` input - the same thing DirectGLES does for the same state. The
|
||||
// frontend RECORDS the format either way, so this gate is the only thing standing
|
||||
// between a legal glVertexAttribLFormat and a mismatched pipeline.
|
||||
if (
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): see the narrowFloat64Arrays site above.
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
if (MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
@@ -847,17 +551,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
}
|
||||
#endif
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
|
||||
@@ -7,15 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
||||
// so the pull build gains nothing from it.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
#include "Config.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
@@ -77,18 +70,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
};
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||
MagmaPipeIdentityTables& identity):
|
||||
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||
#else
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
#endif
|
||||
~VertexInputStateFactory() = default;
|
||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||
|
||||
@@ -103,13 +86,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||
// is unchanged. Use this on per-draw paths.
|
||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||
// question ask the factory instead, because that is where the memo lives once the
|
||||
// frontend object stops carrying the backend's state.
|
||||
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
||||
#endif
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
@@ -128,13 +104,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
// an unknown/unsupported type.
|
||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A wire VAO is a layout record plus a separate per-attribute buffer window.
|
||||
// No frontend VAO identity, address or memo participates in this layout.
|
||||
Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
|
||||
BackendVertexInputState& out) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||
@@ -143,51 +112,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||
//
|
||||
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||
// backend is in another process and its cache entry has no address a client could
|
||||
// store, so the memo has to live on the side that owns the pointee.
|
||||
//
|
||||
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||
struct VaoBackendMemos {
|
||||
// Whose memos these are. The identity table can recycle a slot for a different
|
||||
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||
// the contents are this object's and not its predecessor's.
|
||||
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Hash = 0;
|
||||
Uint32 HashConfigVersion = ~0u;
|
||||
const void* State = nullptr;
|
||||
Uint64 StateEpoch = 0;
|
||||
Uint32 StateConfigVersion = ~0u;
|
||||
};
|
||||
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||
// someone else's.
|
||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#endif
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Physical-device format capabilities never change during this factory's
|
||||
// lifetime. The wire path rebuilds layouts without the legacy VAO memo.
|
||||
mutable UnorderedMap<VkFormat, Bool> m_wireVertexFormatSupport;
|
||||
#endif
|
||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||
// invalidates references to stored values - and so does ERASE, which shifts
|
||||
// the rest of the probe cluster into the hole and therefore moves entries
|
||||
@@ -204,24 +130,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale memo.
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
|
||||
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||
// factory restarting at a dead factory's epoch value would honor its
|
||||
// dangling entry pointers. The constructor takes a value strictly greater
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
//
|
||||
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
static inline Uint64 s_evictionEpochSource = 0;
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -10,63 +10,8 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "MG_Pipe/MGPipeCallbacks.h"
|
||||
#include "MG_Pipe/PipeApply.h"
|
||||
#include "MG_Remote/Server/StagedShadow.h"
|
||||
#include <Config.h>
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P7 wave 2 package B3, INVESTIGATION PROBE (split-only, read once, not in
|
||||
// ConfigLoader's accepted-env table - package B's MGITEST_MAGMA_FORCE_SHADER_MIPMAP
|
||||
// shape). MOBILEGL_MAGMA_WIREBUF_PROBE=1 prints one line per streamed-buffer event so
|
||||
// the `glBufferSubData -> draw` ordering of a trace can be read off a log instead of
|
||||
// inferred. Costs a branch on a cached bool when unset.
|
||||
Bool WireBufProbeEnabled() {
|
||||
static const Bool enabled = [] {
|
||||
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
|
||||
return value && value[0] == '1';
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// P7 wave 2 package B3, FORCING KNOB (split-only, read once, not in ConfigLoader's
|
||||
// accepted-env table - package B's MGITEST_MAGMA_FORCE_SHADER_MIPMAP shape).
|
||||
//
|
||||
// WriteWireBuffer has exactly one unsafe shape: taking the immediate host memcpy
|
||||
// because `lastUseSerial > GetCompletedSerial()` said the buffer is idle. That
|
||||
// predicate's first term is PURE COUNTING (`m_frameSerial - frameCount`, no fence),
|
||||
// so on any arm where the frame serial advances without the GPU having proved the
|
||||
// frame's submissions complete, the answer is a guess. Host lanes never reach that
|
||||
// state on their own (measured: 0 mid-frame frame-boundary drains over the whole
|
||||
// OpenRA replay), so the lane cannot arm against it without forcing it.
|
||||
//
|
||||
// 0/unset = off. 1 = the serial term answers "idle" for every write it would have
|
||||
// ordered. N>1 = only the Nth such write (1-based), which is how one
|
||||
// `glBufferSubData -> draw` pair is poisoned in isolation and the device's "one draw
|
||||
// contributed nothing" signature is reproduced rather than the whole frame destroyed.
|
||||
//
|
||||
// The knob forces ONLY the counting term to lie. It does NOT bypass the ordered copy,
|
||||
// so it stays a valid red-once across the fix: before the submission term existed the
|
||||
// lie reached the host memcpy (red); with the submission term the lie is caught and
|
||||
// the copy is still ordered (green).
|
||||
Uint64 ForceStaleSerialSelector() {
|
||||
static const Uint64 selector = [] {
|
||||
const char* value = std::getenv("MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL");
|
||||
if (!value || !value[0]) return Uint64{0};
|
||||
return static_cast<Uint64>(std::strtoull(value, nullptr, 10));
|
||||
}();
|
||||
return selector;
|
||||
}
|
||||
// Counts only the writes the busy predicate wanted to ORDER, so the selector indexes
|
||||
// the streamed subdata -> draw pairs and nothing else.
|
||||
Uint64 g_orderedWireWriteCounter = 0;
|
||||
#endif
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
constexpr SizeT kLiveResourcePruneThreshold = 256;
|
||||
@@ -172,541 +117,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
||||
.ReadbackFromGpu = Ops_ReadbackFromGpu,
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager& WireManager() {
|
||||
if (g_activeBufferManager == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"wire-buffer-manager\"}");
|
||||
std::abort();
|
||||
}
|
||||
return *g_activeBufferManager;
|
||||
}
|
||||
|
||||
const MG_Pipe::MGPipeResourceOps g_vulkanWireResourceOps = {
|
||||
.Create = [](auto res, const auto& desc) { WireManager().CreateWireBuffer(res, desc); },
|
||||
.Respecify = [](auto res, const auto& desc, const void* bytes) {
|
||||
WireManager().RespecifyWireBuffer(res, desc, bytes);
|
||||
},
|
||||
.SubData = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
},
|
||||
.SubDataResident = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
},
|
||||
.FlushRange = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().FlushWireBuffer(res, record.Offset, record.Size, bytes);
|
||||
},
|
||||
.Readback = [](auto res, const auto& record) {
|
||||
WireManager().ReadbackWireBuffer(res, record.Offset, record.Size);
|
||||
},
|
||||
.Destroy = [](auto res) { WireManager().DestroyWireBuffer(res); },
|
||||
// Split mapping remains T2: the client owns its map and pushes exact
|
||||
// modified ranges. A server pointer is never donated across the wire.
|
||||
.MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; },
|
||||
.UnmapPersistent = [](MG_Pipe::MGPipeHandle) {},
|
||||
};
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void VkBufferManager::RegisterWireResourceOps() {
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
MG_Pipe::MGPipeSetResourceOps(&g_vulkanWireResourceOps);
|
||||
}
|
||||
}
|
||||
|
||||
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
|
||||
const auto found = m_wireBuffers.find(WireBufferKey(res));
|
||||
return found == m_wireBuffers.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
void VkBufferManager::CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) {
|
||||
if (FindWireBuffer(res) != nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ProtocolCorruption, \"duplicate-buffer-create\"} {slot=%u, gen=%u}",
|
||||
res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
m_wireBuffers.try_emplace(WireBufferKey(res));
|
||||
RespecifyWireBuffer(res, desc, nullptr);
|
||||
}
|
||||
|
||||
void VkBufferManager::RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
|
||||
const void* initialBytes) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource) {
|
||||
MGLOG_F("Magma: Fatal{ProtocolCorruption, \"unknown-buffer-respecify\"} {slot=%u, gen=%u}",
|
||||
res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
// THE SERIAL IS READ BEFORE THE RESET BELOW ZEROES IT. Whether it is zero - whether any
|
||||
// GPU command has named this store - decides whether the orphan can go at once or has
|
||||
// to wait for a submission (DeferredWireRelease), and three lines from now the record
|
||||
// no longer carries it.
|
||||
const Uint64 orphanedUseSerial = resource->lastUseSerial;
|
||||
DeferWireRelease(std::move(resource->buffer), orphanedUseSerial);
|
||||
resource->size = desc.Width;
|
||||
resource->lastUseSerial = 0;
|
||||
resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store)
|
||||
resource->gpuWritesPending = false;
|
||||
resource->stagedCoverage.clear();
|
||||
++m_sliceEpochCounter;
|
||||
if (resource->size == 0) return;
|
||||
|
||||
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
|
||||
if (!resource->buffer.Create({
|
||||
.allocator = m_initInfo.allocator,
|
||||
.size = resource->size,
|
||||
.usage = usage,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
||||
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
}) || resource->buffer.Map() == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-storage\"} {slot=%u, gen=%u, size=%llu}",
|
||||
res.Slot, res.Gen, static_cast<unsigned long long>(resource->size));
|
||||
std::abort();
|
||||
}
|
||||
++m_wireStoreCount;
|
||||
NoteWireStorePeaks();
|
||||
PublishWireReclaimGauges();
|
||||
// Under transport, defined bytes follow as resource_subdata records. An
|
||||
// undefined store has no shadow to upload and no implicit zero snapshot.
|
||||
if (initialBytes != nullptr && desc.HasDefinedContent) {
|
||||
WriteWireBuffer(res, 0, resource->size, initialBytes);
|
||||
}
|
||||
}
|
||||
|
||||
Bool VkBufferManager::WaitForWireBufferHostAccess(WireBufferResource& resource) {
|
||||
// B3 review: the early return must consult the same submission term WriteWireBuffer
|
||||
// does, or a caller that reaches here with the serial term idle skips the wait the
|
||||
// term would have demanded.
|
||||
if (resource.lastUseSerial <= GetCompletedSerial() && !resource.gpuWritesPending &&
|
||||
(pVulkanRenderer == nullptr || pVulkanRenderer->IsSubmitIndexComplete(resource.lastUseSubmitIndex))) {
|
||||
return true;
|
||||
}
|
||||
if (!m_copyProvider || !pVulkanRenderer) return false;
|
||||
const VkCommandBuffer commands = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commands == VK_NULL_HANDLE) return false;
|
||||
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &barrier, 0, nullptr, 0, nullptr);
|
||||
if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
|
||||
return false;
|
||||
}
|
||||
resource.lastUseSerial = 0;
|
||||
// B3: the wait above proved every submission at or below the sync point complete, so
|
||||
// the submission term must be cleared with the serial or it would keep answering busy.
|
||||
resource.lastUseSubmitIndex = 0;
|
||||
resource.gpuWritesPending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkBufferManager::WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const void* bytes) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size || size > resource->size - offset || (size && !bytes)) {
|
||||
MGLOG_F("Magma: Fatal{ProtocolCorruption, \"buffer-write-range\"} {slot=%u, gen=%u}",
|
||||
res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
if (size == 0) return;
|
||||
Bool uploaded = false;
|
||||
const Uint64 probeLastUse = resource->lastUseSerial;
|
||||
const Uint64 probeCompleted = GetCompletedSerial();
|
||||
|
||||
// ---- P7 wave 2 package B3: WHAT EACH TERM OF THIS PREDICATE IS FOR -----------------
|
||||
//
|
||||
// A streamed `glBufferSubData -> draw` pair is only correct if the write is ordered
|
||||
// against every draw that still reads the bytes it overwrites; a write this predicate
|
||||
// answers "idle" is applied as an unsynchronised host memcpy, and a draw still reading
|
||||
// the range then renders bytes that are not its own - silently, with no log line, no GL
|
||||
// error and no Fatal. That was the OpenRA device divergence (magma-b3.md §2).
|
||||
//
|
||||
// THE GUARANTEE IS THE SERIAL TERM, and it is sound because of the FLOOR, not because
|
||||
// of anything here. GetCompletedSerial() is max(m_frameSerial - frameCount,
|
||||
// m_completedSerialFloor). The floor used to be raised to a retired submission's frame
|
||||
// serial even while another submission carrying the SAME serial was still executing -
|
||||
// on the wire arm a serial has two (a mid-frame FlushPendingCommands under a pooled
|
||||
// fence, then Present) - so "frame N-1 complete" was asserted on a fence nobody waited.
|
||||
// VulkanRenderer::OnSubmitsCompletedUpTo now raises it only to a serial no in-flight
|
||||
// submission still carries, and run_trace_case.cmake reds a split retrace on any
|
||||
// "MGWIRE-FLOOR unsound-serial-complete" line, which is that fix's lane.
|
||||
//
|
||||
// THE SUBMISSION TERM IS A PROBE AND A DEFENCE, NOT A SECOND GUARANTEE. It is not sound
|
||||
// on its own: lastUseSubmitIndex is stamped at AcquireWireSlice with the next submission
|
||||
// index, but the draw that acquired the slice can still be pushed into a LATER
|
||||
// submission - BindProgramUniformBuffers -> SyncWireTextureShape's preserve path ->
|
||||
// FlushWirePendingCommandsForTextureUpdate submits the stamped index without the draw -
|
||||
// so the stamp can name S1 while the draw rides S2. What it does do: with the floor
|
||||
// sound it should never be the term that says "busy" (the WBUF probe reports bySubmit
|
||||
// separately, so a device that shows one has found a floor hole), and it is the only
|
||||
// thing the MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL knob leaves standing, which is what
|
||||
// the StaleSerial. entries pin.
|
||||
Bool busyBySerial = resource->lastUseSerial > GetCompletedSerial();
|
||||
if (busyBySerial && ForceStaleSerialSelector() != 0) {
|
||||
const Uint64 index = ++g_orderedWireWriteCounter;
|
||||
const Uint64 selector = ForceStaleSerialSelector();
|
||||
if (selector == 1 || selector == index) {
|
||||
busyBySerial = false;
|
||||
MGLOG_W("MGITEST_MAGMA_FORCE_STALE_BUFFER_SERIAL: write #%llu {slot=%u, gen=%u} off=%llu "
|
||||
"size=%llu - the counting term is forced to answer \"idle\"",
|
||||
static_cast<unsigned long long>(index), res.Slot, res.Gen,
|
||||
static_cast<unsigned long long>(offset), static_cast<unsigned long long>(size));
|
||||
}
|
||||
}
|
||||
const Bool busyBySubmission =
|
||||
pVulkanRenderer != nullptr && !pVulkanRenderer->IsSubmitIndexComplete(resource->lastUseSubmitIndex);
|
||||
if (busyBySerial || busyBySubmission) {
|
||||
// Vulkan buffer copies require four-byte aligned ranges. An odd GL
|
||||
// byte update waits, then writes only the exact range; rounding it
|
||||
// from a CPU shadow could overwrite neighbouring GPU-written bytes.
|
||||
if (((offset | size) & 3u) == 0) {
|
||||
uploaded = StagedWireRangeCopy(*resource, bytes, static_cast<SizeT>(offset),
|
||||
static_cast<SizeT>(size));
|
||||
}
|
||||
if (!uploaded && !WaitForWireBufferHostAccess(*resource)) WireBufferSyncFatal("host-write");
|
||||
}
|
||||
if (!uploaded) {
|
||||
uploaded = resource->buffer.Upload(bytes, size, offset);
|
||||
if (uploaded && MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, size);
|
||||
}
|
||||
}
|
||||
if (!uploaded) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-upload\"}");
|
||||
std::abort();
|
||||
}
|
||||
if (WireBufProbeEnabled()) {
|
||||
MGLOG_I("WBUF write slot=%u gen=%u off=%llu size=%llu branch=%s bySerial=%d bySubmit=%d "
|
||||
"lastUse=%llu completed=%llu frameSerial=%llu floor=%llu lastUseSubmit=%llu",
|
||||
res.Slot, res.Gen, static_cast<unsigned long long>(offset),
|
||||
static_cast<unsigned long long>(size),
|
||||
(busyBySerial || busyBySubmission) ? "STAGED-or-WAIT" : "HOST-IMMEDIATE",
|
||||
busyBySerial ? 1 : 0, busyBySubmission ? 1 : 0,
|
||||
static_cast<unsigned long long>(probeLastUse), static_cast<unsigned long long>(probeCompleted),
|
||||
static_cast<unsigned long long>(m_frameSerial),
|
||||
static_cast<unsigned long long>(m_completedSerialFloor),
|
||||
static_cast<unsigned long long>(resource->lastUseSubmitIndex));
|
||||
}
|
||||
MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage,
|
||||
static_cast<SizeT>(offset),
|
||||
static_cast<SizeT>(offset + size));
|
||||
++m_sliceEpochCounter;
|
||||
}
|
||||
|
||||
void VkBufferManager::FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const void* bytes) {
|
||||
if (bytes != nullptr) {
|
||||
WriteWireBuffer(res, offset, size, bytes);
|
||||
return;
|
||||
}
|
||||
const auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size || size > resource->size - offset ||
|
||||
!MG_Remote::Server::StagedShadowStore::CoverageHas(resource->stagedCoverage,
|
||||
static_cast<SizeT>(offset), static_cast<SizeT>(offset + size))) {
|
||||
MGLOG_F("Magma: Fatal{StageSnapshotTooNarrow, \"buffer-flush\"} {slot=%u, gen=%u}",
|
||||
res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
// SubData has already copied the owned record bytes into GPU storage,
|
||||
// ordered after earlier uses. There is no stale shadow to replay here.
|
||||
}
|
||||
|
||||
Bool VkBufferManager::AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice) {
|
||||
(void)kind; // The store carries every buffer usage, so changing roles never orphans it.
|
||||
outSlice = {};
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || !resource->buffer.IsValid() || resource->size == 0) return false;
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
// B3: the draw this slice is being acquired for is normally recorded into the NEXT
|
||||
// submission - normally, not always: a mid-draw flush (SyncWireTextureShape's preserve
|
||||
// path) can submit this index without the draw. This stamp is the defence term's input
|
||||
// (see WriteWireBuffer); the floor behind lastUseSerial is the guarantee.
|
||||
if (pVulkanRenderer) resource->lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex();
|
||||
outSlice = resource->buffer.GetSlice(0, resource->size);
|
||||
outSlice.mapped = nullptr;
|
||||
if (WireBufProbeEnabled()) {
|
||||
MGLOG_I("WBUF bind slot=%u gen=%u frameSerial=%llu", res.Slot, res.Gen,
|
||||
static_cast<unsigned long long>(m_frameSerial));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size || size > resource->size - offset || (size && !dst)) return false;
|
||||
if (size == 0) return true;
|
||||
// Concurrent GPU reads do not prevent a CPU read. Only staged copies or
|
||||
// shader writes need a host visibility barrier and a submission wait;
|
||||
// ordinary index/vertex inspection must not stall once per draw.
|
||||
if (resource->gpuWritesPending) {
|
||||
if (!WaitForWireBufferHostAccess(*resource)) return false;
|
||||
// A draw may already hold a native slice of this store while another
|
||||
// attribute needs a CPU conversion. The wait completes previous GPU
|
||||
// work, not the draw that will bind that previously acquired slice.
|
||||
// Preserve its reservation even if the idle drain advanced the frame.
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
// B3 review: and the submission half of the same reservation - the wait cleared it,
|
||||
// and the draw holding the slice has not been submitted yet.
|
||||
if (pVulkanRenderer) resource->lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex();
|
||||
}
|
||||
if (!resource->buffer.Invalidate(size, offset)) return false;
|
||||
Memcpy(dst, static_cast<const Uint8*>(resource->buffer.GetMappedData()) + offset, static_cast<SizeT>(size));
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const BufferSlice& dst) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size || size > resource->size - offset || size > dst.size) return false;
|
||||
if (size == 0) return true;
|
||||
if (!m_copyProvider || dst.buffer == VK_NULL_HANDLE || ((offset | size | dst.offset) & 3u) != 0) return false;
|
||||
const VkCommandBuffer commands = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commands == VK_NULL_HANDLE) return false;
|
||||
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy copy{offset, dst.offset, size};
|
||||
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), dst.buffer, 1, ©);
|
||||
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &after, 0, nullptr, 0, nullptr);
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset,
|
||||
Uint64 size, Uint32 frameIndex,
|
||||
const BufferSlice& dst, Uint64 dstSkip) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size || size > resource->size - offset) return false;
|
||||
if (dstSkip > dst.size || size > dst.size - dstSkip) return false;
|
||||
if (size == 0) return true;
|
||||
if (!m_copyProvider || dst.buffer == VK_NULL_HANDLE) return false;
|
||||
|
||||
// Round the read of the application's store OUT to whole words. The tail can only be
|
||||
// clipped by the end of the store itself, and [offset, offset + size) already fits
|
||||
// inside it, so the widened window still covers every byte the caller asked for.
|
||||
const Uint64 alignedOffset = offset & ~Uint64{3};
|
||||
const Uint64 stagedEnd = std::min<Uint64>((offset + size + 3) & ~Uint64{3}, resource->size);
|
||||
const Uint64 headPad = offset - alignedOffset;
|
||||
const Uint64 stagedSize = stagedEnd - alignedOffset;
|
||||
if (stagedSize < headPad + size) return false;
|
||||
|
||||
// Uninitialized on purpose: the widening copy below writes every byte of it. The
|
||||
// arena can grow to satisfy this, which parks (never frees) the buffer `dst` may
|
||||
// already name - see BufferArena::EnsureCapacity - so the two slices are allowed to
|
||||
// sit in different VkBuffers and the copy names each slice's own handle.
|
||||
BufferSlice staging{};
|
||||
if (!m_transientUploadArena.Allocate(frameIndex, stagedSize, 4, staging) || !staging.IsValid())
|
||||
return false;
|
||||
|
||||
const VkCommandBuffer commands = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commands == VK_NULL_HANDLE) return false;
|
||||
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy widen{alignedOffset, staging.offset, stagedSize};
|
||||
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), staging.buffer, 1, &widen);
|
||||
// The shift reads what the widening copy just wrote, so it needs its own edge even
|
||||
// though both halves are transfers on one command buffer.
|
||||
VkMemoryBarrier staged{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
staged.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
staged.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 1, &staged, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy shift{staging.offset + headPad, dst.offset + dstSkip, size};
|
||||
vkCmdCopyBuffer(commands, staging.buffer, dst.buffer, 1, &shift);
|
||||
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &after, 0, nullptr, 0, nullptr);
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) {
|
||||
Vector<Uint8> bytes(static_cast<SizeT>(size));
|
||||
if (!ReadWireBuffer(res, offset, size, bytes.data())) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}");
|
||||
std::abort();
|
||||
}
|
||||
// The reverse producer synchronously copies into SEG_EVENT before
|
||||
// this owned vector is released; no mapped Vulkan pointer crosses roles.
|
||||
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(res, offset,
|
||||
{reinterpret_cast<Uint64>(bytes.data()), size, MG_Pipe::kMGHostSpanSegNone, 0});
|
||||
++m_sliceEpochCounter;
|
||||
}
|
||||
|
||||
void VkBufferManager::MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) {
|
||||
auto* resource = FindWireBuffer(res);
|
||||
if (!resource || offset > resource->size) return;
|
||||
if (size == MG_Pipe::kMGPipeWholeBuffer) size = resource->size - offset;
|
||||
if (size == 0 || size > resource->size - offset) return;
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
resource->stagedCoverage.clear();
|
||||
resource->gpuWritesPending = true;
|
||||
++m_sliceEpochCounter;
|
||||
// The existing client dirty-state protocol accepts one whole-store
|
||||
// notification. The store remains on the GPU and exact later SubData
|
||||
// only overwrites its own range, preserving every other GPU-written byte.
|
||||
const MG_Pipe::MGPRange range{0, MG_Pipe::kMGPipeWholeBuffer};
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-gpu-written-callback\"}");
|
||||
std::abort();
|
||||
}
|
||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range);
|
||||
}
|
||||
|
||||
void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) {
|
||||
const auto found = m_wireBuffers.find(WireBufferKey(res));
|
||||
if (found == m_wireBuffers.end()) return;
|
||||
DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial);
|
||||
m_wireBuffers.erase(found);
|
||||
++m_sliceEpochCounter;
|
||||
PublishWireReclaimGauges();
|
||||
}
|
||||
|
||||
void VkBufferManager::DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial) {
|
||||
if (!buffer.IsValid()) return;
|
||||
// Two stores go NOW rather than into the list. With no frame slots (DeferRelease's own
|
||||
// teardown guard, and for its reason) there is no manager left to reclaim anything
|
||||
// later. And a store whose last-use serial is 0 was never named by a GPU command since
|
||||
// it was minted or since a host-access wait retired everything recorded before it - see
|
||||
// DeferredWireRelease - so there is nothing to wait for.
|
||||
if (m_deferredBufferReleases.empty() || lastUseSerial == 0) {
|
||||
buffer.Destroy();
|
||||
++m_wireStoreDestroyEpoch;
|
||||
if (m_wireStoreCount > 0) --m_wireStoreCount;
|
||||
return;
|
||||
}
|
||||
DeferredWireRelease entry;
|
||||
entry.lastUseSerial = lastUseSerial;
|
||||
// The last submission that can name this store: see DeferredWireRelease. Zero when there
|
||||
// is no renderer, which is also when there is no queue and nothing to wait for.
|
||||
entry.submitIndex = pVulkanRenderer != nullptr ? pVulkanRenderer->GetSyncPointSubmitIndex() : 0;
|
||||
entry.bytes = static_cast<Uint64>(buffer.GetSize());
|
||||
entry.buffer = std::move(buffer);
|
||||
m_deferredWireBytes += entry.bytes;
|
||||
m_deferredWireReleases.push_back(std::move(entry));
|
||||
NoteWireStorePeaks();
|
||||
// EVERY PARK SWEEPS. The frame boundary is not a reclaim point this arm can lean on - a
|
||||
// pbuffer replay that snapshot-exits delivers ONE present for 1.3 M calls (ID-P7-32) -
|
||||
// so the only cadence that tracks the workload is the workload itself.
|
||||
SweepDeferredWireReleases();
|
||||
EnforceWireDeferredWatermark();
|
||||
}
|
||||
|
||||
void VkBufferManager::EnforceWireDeferredWatermark() {
|
||||
const Uint64 budget = static_cast<Uint64>(MG_Config::Ipc.WireDeferredMb) * 1024u * 1024u;
|
||||
// TWO TRIGGERS, ONE SWITCH. Bytes are what the knob names, but a VkBuffer costs per
|
||||
// OBJECT as well as per byte, and small orphans never reach a byte budget: measured on
|
||||
// bsl-esc-menu-854 (spawn, lavapipe), one stretch with no submission parked 12,498 stores
|
||||
// in 39.5 MB against 64 MiB. So a fixed count ceiling rides the same sync point; the
|
||||
// knob's 0 turns both off, which keeps it one negative control.
|
||||
if (budget == 0) return;
|
||||
if (m_deferredWireBytes <= budget && m_deferredWireReleases.size() <= kWireDeferredCountCeiling) return;
|
||||
// What is left after the sweep is named by work that is recorded but unsubmitted, or
|
||||
// submitted but unretired. Every entry is tagged at or below the sync point taken HERE,
|
||||
// so waiting it out - the host-access wait's own call, which flushes the recording first
|
||||
// - proves all of them dead. A mid-frame flush is what WaitForWireBufferHostAccess and a
|
||||
// glClientWaitSync already do to this command stream; it costs one submission per
|
||||
// budget's worth of orphans, not one per glBufferData.
|
||||
if (pVulkanRenderer == nullptr ||
|
||||
!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
|
||||
WireBufferSyncFatal("deferred-watermark");
|
||||
}
|
||||
++m_wireDeferredSyncs;
|
||||
SweepDeferredWireReleases();
|
||||
}
|
||||
|
||||
void VkBufferManager::NoteWireStorePeaks() {
|
||||
m_wireStoreCountPeak = std::max(m_wireStoreCountPeak, m_wireStoreCount);
|
||||
m_deferredWireBytesPeak = std::max(m_deferredWireBytesPeak, m_deferredWireBytes);
|
||||
}
|
||||
|
||||
void VkBufferManager::PublishWireReclaimGauges() {
|
||||
if (!MG_Util::PipeStats::Enabled()) return;
|
||||
using MG_Util::PipeStats::Gauge;
|
||||
MG_Util::PipeStats::PublishGauge(Gauge::WireBuffers, static_cast<Uint64>(m_wireBuffers.size()));
|
||||
MG_Util::PipeStats::PublishGauge(Gauge::WireStoresPeak, m_wireStoreCountPeak);
|
||||
MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredBytesPeak, m_deferredWireBytesPeak);
|
||||
MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredSyncs, m_wireDeferredSyncs);
|
||||
}
|
||||
|
||||
void VkBufferManager::WireBufferSyncFatal(const char* site) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"} {site=%s}", site);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
SizeT VkBufferManager::SweepDeferredWireReleases() {
|
||||
if (m_deferredWireReleases.empty()) return 0;
|
||||
SizeT dead = 0;
|
||||
if (pVulkanRenderer == nullptr ||
|
||||
pVulkanRenderer->IsSubmitIndexComplete(pVulkanRenderer->GetSyncPointSubmitIndex())) {
|
||||
// Idle: nothing recorded, every submission retired - or no renderer, so no queue.
|
||||
// Every parked store is dead, including one tagged for a batch that was abandoned
|
||||
// instead of submitted.
|
||||
dead = m_deferredWireReleases.size();
|
||||
} else {
|
||||
// The list is in park order and GetSyncPointSubmitIndex() steps back only when a
|
||||
// pending recording is abandoned instead of submitted (RecreateSwapchain and the
|
||||
// minimized Present force-clear the recording flags, so `m_submitCounter + 1`
|
||||
// becomes `m_submitCounter`). Everywhere else the submit indices are non-decreasing
|
||||
// and the dead entries are a PREFIX: the first entry the renderer will not call
|
||||
// complete proves none after it is either. Across an abandonment a later entry can
|
||||
// carry the LOWER index; the walk then stops at the earlier, higher one and holds
|
||||
// both until the next submission takes that index and retires - conservative, never
|
||||
// early, and the idle rule above retires them regardless. That bounds the walk to a
|
||||
// couple of fence polls, which matters because this runs on every glBufferData the
|
||||
// wire carries.
|
||||
while (dead < m_deferredWireReleases.size() &&
|
||||
pVulkanRenderer->IsSubmitIndexComplete(m_deferredWireReleases[dead].submitIndex)) {
|
||||
++dead;
|
||||
}
|
||||
}
|
||||
if (dead == 0) return 0;
|
||||
for (SizeT i = 0; i < dead; ++i) {
|
||||
DeferredWireRelease& entry = m_deferredWireReleases[i];
|
||||
m_deferredWireBytes -= entry.bytes;
|
||||
entry.buffer.Destroy();
|
||||
}
|
||||
// One bump per sweep that destroyed anything is enough: what a memo needs to know
|
||||
// is "some handle it may name has been freed since", not how many.
|
||||
++m_wireStoreDestroyEpoch;
|
||||
m_deferredWireReleases.erase(m_deferredWireReleases.begin(),
|
||||
m_deferredWireReleases.begin() + static_cast<std::ptrdiff_t>(dead));
|
||||
const Uint64 destroyedCount = static_cast<Uint64>(dead);
|
||||
m_wireStoreCount = destroyedCount <= m_wireStoreCount ? m_wireStoreCount - destroyedCount : 0;
|
||||
return dead;
|
||||
}
|
||||
|
||||
void VkBufferManager::DestroyAllDeferredWireReleases() {
|
||||
if (!m_deferredWireReleases.empty()) ++m_wireStoreDestroyEpoch;
|
||||
for (auto& entry : m_deferredWireReleases) {
|
||||
entry.buffer.Destroy();
|
||||
if (m_wireStoreCount > 0) --m_wireStoreCount;
|
||||
}
|
||||
m_deferredWireReleases.clear();
|
||||
m_deferredWireBytes = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
Bool VkBufferManager::Initialize(const VkBufferManagerInitInfo& initInfo) {
|
||||
Shutdown();
|
||||
|
||||
@@ -724,20 +136,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
g_activeBufferManager = this;
|
||||
MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RegisterWireResourceOps();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkBufferManager::Shutdown() {
|
||||
if (g_activeBufferManager == this) {
|
||||
g_activeBufferManager = nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) {
|
||||
MG_Pipe::MGPipeSetResourceOps(nullptr);
|
||||
}
|
||||
#endif
|
||||
if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {
|
||||
MG_State::GLState::SetBufferBackendOps(nullptr);
|
||||
}
|
||||
@@ -747,16 +151,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_unboundTexelBuffer.Destroy();
|
||||
DestroyAllDeferredReleases();
|
||||
ReleaseAllLiveResources();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the
|
||||
// records still hold, so nothing this arm minted outlives the count.
|
||||
if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch;
|
||||
m_wireBuffers.clear();
|
||||
m_wireStoreCount = 0;
|
||||
m_wireStoreCountPeak = 0;
|
||||
m_deferredWireBytesPeak = 0;
|
||||
m_wireDeferredSyncs = 0;
|
||||
#endif
|
||||
m_copyProvider = nullptr;
|
||||
m_initInfo = {};
|
||||
m_currentFrameIndex = 0;
|
||||
@@ -785,13 +179,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"VkBufferManager::BeginFrame frame index out of range");
|
||||
m_currentFrameIndex = frameIndex;
|
||||
++m_frameSerial;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (WireBufProbeEnabled()) {
|
||||
MGLOG_I("WBUF BEGINFRAME idx=%u newSerial=%llu floor=%llu (arena slot rewound)", frameIndex,
|
||||
static_cast<unsigned long long>(m_frameSerial),
|
||||
static_cast<unsigned long long>(m_completedSerialFloor));
|
||||
}
|
||||
#endif
|
||||
CollectDeferredReleases(frameIndex);
|
||||
m_transientUploadArena.BeginFrame(frameIndex);
|
||||
}
|
||||
@@ -808,9 +195,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
|
||||
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
|
||||
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
|
||||
// exactly this way). Most mid-frame drains do not advance m_frameSerial; every
|
||||
// eighth does, but only through BeginFrame after the arena/memo boundary work.
|
||||
// The drain's per-resource sweep must not free arena storage: ResetFrame/BeginFrame is where
|
||||
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
|
||||
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
|
||||
// the slot's slices stop being reachable, and that is where these releases land.
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||
CollectDeferredReleases(frameIndex);
|
||||
@@ -851,38 +237,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
|
||||
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
|
||||
// absent: its bytes are counted by the caller, which is the only place that
|
||||
// knows whether the payload is the default block (stage-ubo-global) or a named
|
||||
// one repacked into the ring (stage-ubo-named), and counting here as well would
|
||||
// double every uniform byte.
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Index:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Indirect:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::TextureBuffer:
|
||||
case BufferKind::ShaderStorage:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Uniform:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
(void)kind;
|
||||
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
|
||||
}
|
||||
|
||||
Bool VkBufferManager::InitializeTransientArenas() {
|
||||
@@ -891,9 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.frameCount = m_initInfo.frameCount,
|
||||
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT |
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
(MG_Config::Transport != MG_Config::TransportMode::Monolith ? VkBufferUsageFlags{VK_BUFFER_USAGE_TRANSFER_DST_BIT} : 0u) |
|
||||
#endif
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
.memoryUsage = m_initInfo.transientMemoryUsage,
|
||||
.allocationFlags = m_initInfo.transientAllocationFlags,
|
||||
@@ -994,9 +347,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource.pendingFullUpload = false;
|
||||
return true;
|
||||
}
|
||||
@@ -1011,11 +361,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), 16, staging)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
|
||||
// half of the same bytes and is not counted twice.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commandBuffer == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
@@ -1046,58 +391,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VkBufferManager::StagedWireRangeCopy(WireBufferResource& resource, const void* data,
|
||||
SizeT offset, SizeT size) {
|
||||
if (!m_copyProvider) {
|
||||
return false;
|
||||
}
|
||||
BufferSlice staging{};
|
||||
if (!m_transientUploadArena.Upload(m_currentFrameIndex, data,
|
||||
static_cast<VkDeviceSize>(size), 16, staging)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
|
||||
// half of the same bytes and is not counted twice.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commandBuffer == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Order the copy after every prior read/write of this buffer, both from
|
||||
// in-flight frames (submission order) and from commands already recorded
|
||||
// in this frame's command buffer.
|
||||
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&beforeBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
VkBufferCopy region{};
|
||||
region.srcOffset = staging.offset;
|
||||
region.dstOffset = static_cast<VkDeviceSize>(offset);
|
||||
region.size = static_cast<VkDeviceSize>(size);
|
||||
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion);
|
||||
|
||||
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
|
||||
&afterBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
resource.lastUseSerial = m_frameSerial;
|
||||
// B3: the copy just recorded rides the next submission too, so a later host write to
|
||||
// the same bytes must wait for it exactly as a draw's read must.
|
||||
if (pVulkanRenderer) resource.lastUseSubmitIndex = pVulkanRenderer->GetWireNextSubmitIndex();
|
||||
resource.gpuWritesPending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) {
|
||||
auto* resource = ResourceOf(bufferObject);
|
||||
if (!resource) {
|
||||
@@ -1137,8 +430,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,9 +455,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1225,9 +513,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1298,13 +583,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint8* seed = bufferObject.MappedData();
|
||||
if (seed != nullptr) {
|
||||
resource->buffer.Upload(seed, size, 0);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The one-time seed of a persistent map. Everything the app writes AFTER
|
||||
// this goes straight through the mapping and is persistent-map-push
|
||||
// territory (unwired, D4/D-B4), not this class.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
resource->persistentMapped = true;
|
||||
resource->pendingFullUpload = false;
|
||||
@@ -1353,10 +631,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->usageFlags = 0;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
resource->pendingFullUpload = false;
|
||||
}
|
||||
|
||||
@@ -1436,9 +710,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource->transientSlice = outSlice;
|
||||
resource->transientFrameSerial = m_frameSerial;
|
||||
resource->transientChangeSerial = changeSerial;
|
||||
@@ -1473,12 +744,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"VkBufferManager::CollectDeferredReleases frame index out of range");
|
||||
m_deferredBufferReleases[frameIndex].clear();
|
||||
m_deferredResourceReleases[frameIndex].clear();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The wire list is not per-slot and does not wait for a frame boundary; a boundary is
|
||||
// just one more point to sweep at (BeginFrame after its slot fence, and every slot of
|
||||
// the idle drain, where the renderer-idle rule empties the list).
|
||||
SweepDeferredWireReleases();
|
||||
#endif
|
||||
}
|
||||
|
||||
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
|
||||
@@ -1574,10 +839,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferManager::DestroyAllDeferredReleases() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Both callers (Shutdown, RecreateTransientArenas) have proven the device idle.
|
||||
DestroyAllDeferredWireReleases();
|
||||
#endif
|
||||
for (auto& releases : m_deferredBufferReleases) {
|
||||
for (auto& buffer : releases) {
|
||||
buffer.Destroy();
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "MG_Pipe/MGPipeTypes.h"
|
||||
#include <unordered_map>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class BufferKind : Uint8 {
|
||||
@@ -103,39 +99,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Registered before caps publication; the initialized renderer owns the storage.
|
||||
static void RegisterWireResourceOps();
|
||||
// Transport resources are owned by their complete wire handle, never by a
|
||||
// frontend BufferObject. Acquires expose the full GPU store, without a CPU
|
||||
// pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes.
|
||||
Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice);
|
||||
Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst);
|
||||
Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const BufferSlice& dst);
|
||||
// P7 A.1, the sub-word half of the one above: the same copy for a window whose start
|
||||
// or end does not land on a four-byte boundary. The read of the APPLICATION's store is
|
||||
// rounded OUT to whole words and clamped to the store's end, so the widened window can
|
||||
// never touch a byte the application does not own; it lands in a transient staging
|
||||
// slice, and only the staging -> dst shift is sub-word - inside our own arena, where an
|
||||
// unaligned region cannot alias anything else. vkCmdCopyBuffer places no alignment rule
|
||||
// on a region's offsets or size (unlike vkCmdUpdateBuffer / vkCmdFillBuffer), so the
|
||||
// shift is a plain legal copy. `dstSkip` is the byte inside `dst` the window starts at.
|
||||
// This retires `uniform-buffer-byte-tail`.
|
||||
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
|
||||
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
|
||||
// Resource-op entry points. All run on the server apply owner.
|
||||
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
|
||||
void RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
|
||||
const void* initialBytes);
|
||||
void WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
#endif
|
||||
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
@@ -208,13 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Bumped every time this manager destroys a WIRE store's VkBuffer (see
|
||||
// m_wireStoreDestroyEpoch). Unchanged since a memo was taken means no VkBuffer handle
|
||||
// that memo names can have been freed and re-minted in between, which is the one
|
||||
// fact a handle-keyed memo of wire descriptors needs and cannot read off the handle.
|
||||
Uint64 GetWireStoreDestroyEpoch() const { return m_wireStoreDestroyEpoch; }
|
||||
#endif
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
@@ -222,121 +178,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Busy = potentially referenced by GPU work that has not been fenced yet
|
||||
// (including commands recorded for the current, unsubmitted frame).
|
||||
Bool IsResourceBusy(const VkBufferResource& resource) const;
|
||||
// Hand the manager a buffer to destroy once the frame that recorded commands
|
||||
// naming it has completed. The renderer's blit path needs a device-local
|
||||
// scratch store for one recorded operation, which cannot be a stack local:
|
||||
// the glBlitFramebuffer that records the commands returns long before the
|
||||
// command buffer is submitted.
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
|
||||
private:
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
struct WireBufferResource {
|
||||
VkBufferObject buffer;
|
||||
Uint64 size = 0;
|
||||
Uint64 lastUseSerial = 0;
|
||||
// P7 wave 2 package B3: the submission expected to carry the most recent GPU use
|
||||
// of this buffer - the busy predicate's DEFENCE term, not its guarantee (see
|
||||
// WriteWireBuffer). IsSubmitIndexComplete polls the real fence and reports an
|
||||
// unsubmitted index as incomplete, but the stamp is taken before the draw is
|
||||
// recorded and a mid-draw flush can submit it without the draw.
|
||||
Uint64 lastUseSubmitIndex = 0;
|
||||
Bool gpuWritesPending = false;
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
Vector<Range1D> stagedCoverage;
|
||||
};
|
||||
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
|
||||
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
|
||||
}
|
||||
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
|
||||
|
||||
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
|
||||
// BOUNDARY.
|
||||
//
|
||||
// RespecifyWireBuffer - which is every glBufferData that crosses the wire - orphans the
|
||||
// old store unconditionally, because unlike OnRespecify it has no shadow to upload in
|
||||
// place from and no cheap way to know the client is re-sending the same size. The orphan
|
||||
// is legitimate (M1 measured the conditional-orphan mirror: 6701 live against 6702).
|
||||
// What was missing is the RECLAIM. DeferRelease parks into m_deferredBufferReleases,
|
||||
// whose only sweep is CollectDeferredReleases from a frame boundary, and on a pbuffer
|
||||
// replay the server sees ONE present record for the whole run - so on
|
||||
// minecraft-1.21.4-fabric-iris-bsl-esc-menu-854 the buckets held 25,923 dead stores
|
||||
// against 28 live wire buffers, one memfd mapping each, and the server died in scudo's
|
||||
// secondary allocator.
|
||||
//
|
||||
// So a wire store is parked HERE instead, with the facts that say when it is dead, and
|
||||
// THE DEFER PATH ITSELF RECLAIMS - every park sweeps, and the frame boundary is only one
|
||||
// more sweep point (CollectDeferredReleases), never the one this depends on:
|
||||
//
|
||||
// * `lastUseSerial == 0` - read BEFORE RespecifyWireBuffer zeroes it. Zero means no
|
||||
// GPU command has named the store since it was minted or since the last host-access
|
||||
// wait proved every recorded command complete (WaitForWireBufferHostAccess); every
|
||||
// path that hands the store to the GPU stamps m_frameSerial, which starts at 1. Such
|
||||
// a store is destroyed at the park and never enters the list.
|
||||
// * `submitIndex` - the renderer's GetSyncPointSubmitIndex() at park time, which is by
|
||||
// construction the LAST submission that can name this store: every command that
|
||||
// names it was recorded before the park (the record left behind a bumped slice
|
||||
// epoch, so no memo can hand it out again), and a recorded command is either already
|
||||
// submitted (<= m_submitCounter) or in the batch that becomes m_submitCounter + 1.
|
||||
// IsSubmitIndexComplete(submitIndex) is a fence observation, so this is the gate
|
||||
// that empties the set MID-FRAME - the shape CollectWireObjects already uses for
|
||||
// the wire image/view tables.
|
||||
// * THE RENDERER IS IDLE - IsSubmitIndexComplete(GetSyncPointSubmitIndex()): nothing
|
||||
// is recorded and every submission has retired. Then every parked store is dead,
|
||||
// including one tagged for a batch that was abandoned rather than submitted (a
|
||||
// minimized Present drops its recording), whose own index may never complete.
|
||||
//
|
||||
// THE FRAME-SERIAL FLOOR IS DELIBERATELY NOT A PROOF HERE. It cannot move inside a
|
||||
// frame (NotifyFrameSerialComplete refuses the current serial), so it frees nothing in
|
||||
// the one-present replay this exists for; the submit index above is the fact that CAN
|
||||
// move mid-frame, and it is a fence observation rather than a count.
|
||||
//
|
||||
// AND EVERY DESTROY HERE HAPPENS MID-FRAME, which the memos above this manager were not
|
||||
// written for. UniformManager's descriptor memos are keyed on the VkBuffer HANDLE, and
|
||||
// before M2 a wire store only ever died at the same boundary that clears those memos
|
||||
// (UniformManager::BeginFrame, from Present or a drain). A store destroyed here can
|
||||
// have its handle value re-minted by the next Create - a heap pointer under lavapipe -
|
||||
// while a memo still maps that handle to a descriptor set baked to the dead store's
|
||||
// memory: silent wrong bytes, no Fatal (ID-P7-43). m_wireStoreDestroyEpoch is the
|
||||
// fact the memos fold in: every wire-store destroy bumps it, so a memo taken before
|
||||
// the destroy cannot match after it. Deliberately NOT m_sliceEpochCounter, which every
|
||||
// WriteWireBuffer bumps and which would defeat the memo on every glBufferSubData.
|
||||
struct DeferredWireRelease {
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseSerial = 0;
|
||||
Uint64 submitIndex = 0;
|
||||
// Cached: GetSize() is gone once the object is destroyed, and the watermark's
|
||||
// running total has to stay exact as entries leave.
|
||||
Uint64 bytes = 0;
|
||||
};
|
||||
// Park a wire store, sweep, then hold the watermark. `lastUseSerial` is the releasing
|
||||
// resource's, captured before the caller resets it.
|
||||
void DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial);
|
||||
// Destroy every parked store proven dead by the rules above. Returns how many.
|
||||
SizeT SweepDeferredWireReleases();
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (Config.h has the semantics): when the parked bytes
|
||||
// still exceed the budget after a sweep, take the sync point WaitForWireBufferHostAccess
|
||||
// takes - flush what is recorded, wait for it - and sweep again, which retires every
|
||||
// parked store, because none can be tagged past the sync point it just waited out.
|
||||
void EnforceWireDeferredWatermark();
|
||||
// ...and the same sync point when more than this many stores are parked, whatever their
|
||||
// bytes (see the definition for the measurement). Not a knob: it bounds an object count
|
||||
// the byte budget cannot see, and MOBILEGL_IPC_WIRE_DEFERRED_MB=0 disables it too.
|
||||
static constexpr SizeT kWireDeferredCountCeiling = 1024;
|
||||
// Teardown: the caller has proven the device idle (Shutdown / RecreateTransientArenas).
|
||||
void DestroyAllDeferredWireReleases();
|
||||
// THE one Fatal{ResourceUnavailable, "buffer-write-sync"} site (rule I: no second abort
|
||||
// for the census to count), shared by the host write that cannot wait for the GPU and
|
||||
// the watermark that cannot. `site` says which.
|
||||
[[noreturn]] static void WireBufferSyncFatal(const char* site);
|
||||
// The wbuf[] gauges (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs). The peaks are
|
||||
// taken at the two points the numbers can rise - a park and a mint - and published when
|
||||
// the stats channel is on; MagmaWireReclaimScenario reads them off the server's line.
|
||||
void NoteWireStorePeaks();
|
||||
void PublishWireReclaimGauges();
|
||||
#endif
|
||||
Bool InitializeTransientArenas();
|
||||
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
|
||||
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
@@ -349,9 +192,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// in-flight and already-recorded GPU work.
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, const void* data,
|
||||
SizeT offset, SizeT size);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size);
|
||||
#endif
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyAllDeferredReleases();
|
||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||
@@ -370,26 +211,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
|
||||
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
|
||||
// the whole point is that these entries do not wait for a frame slot to come round.
|
||||
Vector<DeferredWireRelease> m_deferredWireReleases;
|
||||
// Bytes currently parked in that list, kept exact so the watermark needs no walk.
|
||||
Uint64 m_deferredWireBytes = 0;
|
||||
// Live VkBuffers this arm owns: the stores held by m_wireBuffers plus the parked ones.
|
||||
// Maintained rather than counted, because the publish runs on every park.
|
||||
Uint64 m_wireStoreCount = 0;
|
||||
// Run maxima of the two numbers above and the watermark's sync count, for the gauges.
|
||||
Uint64 m_wireStoreCountPeak = 0;
|
||||
Uint64 m_deferredWireBytesPeak = 0;
|
||||
Uint64 m_wireDeferredSyncs = 0;
|
||||
// See GetWireStoreDestroyEpoch and the DeferredWireRelease comment. Bumped on every
|
||||
// path that destroys a wire store's VkBuffer, and never reset (not even by Shutdown),
|
||||
// for m_sliceEpochCounter's reason: a memo taken before a re-initialize must not match
|
||||
// a handle minted after it.
|
||||
Uint64 m_wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
|
||||
SizeT m_liveResourcesLastPruned = 0;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
@@ -55,7 +54,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (payload.colorEncoding != ClearColorEncoding::Float) return;
|
||||
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
|
||||
// is exactly right and there is nothing to undo.
|
||||
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
|
||||
|
||||
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
@@ -611,7 +610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
@@ -963,7 +962,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
@@ -1109,7 +1108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
|
||||
@@ -15,37 +15,7 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// An accessor view over the wire's POD values, not a GL object with constructor,
|
||||
// lifetime ID, allocator hooks or mutation callbacks. Both sources below use
|
||||
// one sampler policy, including format-sensitive border-color conversion.
|
||||
struct SamplerParametersSource {
|
||||
const SamplerParameters& Params;
|
||||
SamplerWrapMode GetWrapS() const { return Params.wrapS; }
|
||||
SamplerWrapMode GetWrapT() const { return Params.wrapT; }
|
||||
SamplerWrapMode GetWrapR() const { return Params.wrapR; }
|
||||
SamplerFilterMode GetMinFilter() const { return Params.minFilter; }
|
||||
SamplerFilterMode GetMagFilter() const { return Params.magFilter; }
|
||||
SamplerMipmapMode GetMipmapMode() const { return Params.mipmapMode; }
|
||||
Float GetMinLod() const { return Params.minLod; }
|
||||
Float GetMaxLod() const { return Params.maxLod; }
|
||||
Float GetLodBias() const { return Params.lodBias; }
|
||||
Float GetMaxAnisotropy() const { return Params.maxAnisotropy; }
|
||||
SamplerCompareMode GetCompareMode() const { return Params.compareMode; }
|
||||
SamplerCompareFunc GetSamplerCompareFunc() const { return Params.compareFunc; }
|
||||
const FloatVec4& GetBorderColor() const { return Params.borderColor; }
|
||||
const IntVec4& GetBorderColorI() const { return Params.borderColorI; }
|
||||
const UintVec4& GetBorderColorUI() const { return Params.borderColorUI; }
|
||||
// Cache identity is entirely content-based; these fields are diagnostics.
|
||||
Uint GetExternalIndex() const { return 0; }
|
||||
Uint16 GetVersion() const { return 0; }
|
||||
};
|
||||
|
||||
template <class SamplerSource>
|
||||
Bool UsesBorderColor(const SamplerSource& sampler) {
|
||||
#else
|
||||
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
@@ -221,24 +191,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return std::fabs(lhs - rhs) <= 1e-6f;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMaxLod(const SamplerSource& sampler) {
|
||||
#else
|
||||
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
|
||||
return 0.0f;
|
||||
}
|
||||
return sampler.GetMaxLod();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) {
|
||||
#else
|
||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||
#endif
|
||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||
}
|
||||
|
||||
@@ -249,12 +209,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) {
|
||||
#else
|
||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
||||
#endif
|
||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
||||
}
|
||||
@@ -275,12 +230,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
|
||||
#else
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering) const {
|
||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||
if (forceNearestFiltering) return 1.0f;
|
||||
@@ -339,12 +289,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler,
|
||||
#else
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
@@ -390,14 +335,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler,
|
||||
TextureInternalFormat format,
|
||||
#else
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
||||
@@ -409,11 +348,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format);
|
||||
#else
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
#endif
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
@@ -473,20 +408,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return vkSampler;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
|
||||
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
|
||||
forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
|
||||
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
|
||||
}
|
||||
@@ -542,14 +463,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const SamplerSource& sampler, TextureInternalFormat format) const {
|
||||
#else
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
#endif
|
||||
ResolvedBorderColor resolved{};
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
@@ -557,9 +472,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
const auto format = texture.GetFormat();
|
||||
#endif
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
|
||||
@@ -46,12 +46,6 @@ public:
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false,
|
||||
Uint32 viewLevelCount = 0);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The applier owns these values: effective bound/builtin sampler parameters and
|
||||
// the sampled texture view's format/range. No frontend object is constructed.
|
||||
VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
|
||||
TextureInternalFormat format, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0);
|
||||
#endif
|
||||
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
||||
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
||||
// anisotropy), so an app animating those would otherwise mint an unbounded
|
||||
@@ -92,37 +86,19 @@ private:
|
||||
Bool usesCustomBorderColor = false;
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSampler GetOrCreateSamplerImpl(const SamplerSource& sampler, TextureInternalFormat format,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount);
|
||||
template <class SamplerSource>
|
||||
Uint64 BuildSamplerKey(const SamplerSource& sampler, Bool forceNearestFiltering,
|
||||
#else
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
|
||||
#endif
|
||||
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
ResolvedBorderColor ResolveBorderColor(const SamplerSource& sampler, TextureInternalFormat format) const;
|
||||
#else
|
||||
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
#endif
|
||||
// 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.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
|
||||
#else
|
||||
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,21 +17,9 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape
|
||||
// MagmaPipeArms.h already gives this directory.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class ITextureObject;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct MGPipeResourceRecord;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
@@ -332,6 +320,11 @@ public:
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// The same, for the last recorded command that WRITES the image (a render pass it is an
|
||||
// attachment of, a clear, a blit/copy into it, a mip generation, a storage-image binding).
|
||||
// An upload into an image written this recording has to be ordered after that write
|
||||
// (VulkanRenderer::OrderPendingUploadAfterRecording).
|
||||
Uint64 lastRecordingWriteGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -345,13 +338,6 @@ public:
|
||||
// shape version and nothing else. Without this in the early-out key the image, its views
|
||||
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
|
||||
Uint64 syncedShapeVersion = 0;
|
||||
// The handle-keyed arm's (P5f fm) early-out key: the applier record's server-owned
|
||||
// Serial at the last successful sync. 0 also means "this resource has only ever been
|
||||
// synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves
|
||||
// on every mutation, so the first handle sync of a fresh image always re-checks.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 syncedWireSerial = 0;
|
||||
#endif
|
||||
|
||||
TextureResource() = default;
|
||||
TextureResource(const TextureResource&) = delete;
|
||||
@@ -381,12 +367,10 @@ public:
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->lastRecordingWriteGeneration, that.lastRecordingWriteGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::swap(this->syncedWireSerial, that.syncedWireSerial);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
@@ -451,9 +435,6 @@ public:
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
syncedShapeVersion = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
syncedWireSerial = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
~TextureResource() {
|
||||
@@ -480,12 +461,6 @@ public:
|
||||
// preserve-on-recreate copy are the existing callers. No-op when the
|
||||
// batch is empty.
|
||||
void FlushPendingUploads();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Non-blocking idle proof for wire-object retirement. Independent texture
|
||||
// submissions are not represented by VulkanRenderer's submit counter.
|
||||
Bool WireUploadsAreIdle();
|
||||
#endif
|
||||
|
||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
@@ -545,47 +520,6 @@ public:
|
||||
|
||||
TextureResource* SyncTextureAndGetDescriptor(
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport
|
||||
// the apply thread may not name the client's ITextureObject (rule E), so the clear / blit /
|
||||
// readback / mipmap verbs resolve their attachment textures from the wire handle the
|
||||
// framebuffer or mip record carried: shape from the applier's resource record
|
||||
// (MGPipeApplier().TextureResources), texels from the server's staged-texture store
|
||||
// (P5c tx), pending-upload bookkeeping consumed from the same record. Returns null - never
|
||||
// a frontend fallback - when the record or the shape does not resolve; the caller declines
|
||||
// loudly, which is the honest answer a missing record deserves.
|
||||
//
|
||||
// The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle -
|
||||
// a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen}
|
||||
// can never inherit its predecessor's image. Texture views resolve the storage owner from
|
||||
// Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm.
|
||||
Bool ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, Uint32 level,
|
||||
const IntVec3& extent, VkFormat& format, Vector<Uint8>& bytes);
|
||||
TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false,
|
||||
Bool requireStorage = false);
|
||||
// Map a texture/view-relative subresource to its live storage owner. layerCount, when
|
||||
// supplied, receives the number of accessible layers starting at the input layer.
|
||||
// viewFormat is UNDEFINED for an ordinary texture, or the outer view's format.
|
||||
// outsideWindow, when supplied, says WHY a null came back: true only when every record on
|
||||
// the way was live and consistent and the (level, layer) simply is not there - past the
|
||||
// owner's levels or layers, or past a view's own window. A dead record, a missing or stale
|
||||
// view CSO and an unresolvable view format leave it false.
|
||||
MG_Pipe::MGPipeHandle ResolveWireTextureStorage(MG_Pipe::MGPipeHandle handle, Uint32& level,
|
||||
Uint32& layer, VkFormat* viewFormat = nullptr, Uint32* layerCount = nullptr,
|
||||
Bool* outsideWindow = nullptr);
|
||||
// Grows the live image of a handle-keyed resource to `requiredMipLevels`, carrying the
|
||||
// existing levels' content across with an in-command-buffer copy. The caller
|
||||
// (GenerateMipmap's record arm) has flushed every pending submission first, so the old
|
||||
// image is GPU-idle and the copy may be recorded into `commandBuffer` directly; the old
|
||||
// image goes to the deferred-release ring. False when the chain cannot grow (the resource
|
||||
// stays as it was).
|
||||
Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels,
|
||||
VkCommandBuffer commandBuffer);
|
||||
// Records that GPU writes superseded the staged CPU snapshot of this level. Layer
|
||||
// ranges select cube faces; array and 3D levels share one staged level shadow.
|
||||
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
#endif
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||
@@ -623,6 +557,26 @@ public:
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// A write is a use too. See TextureResource::lastRecordingWriteGeneration.
|
||||
void StampResourceRecordingWrite(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
resource.lastRecordingWriteGeneration = m_recordingGeneration;
|
||||
}
|
||||
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
|
||||
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
|
||||
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
|
||||
//
|
||||
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
|
||||
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
|
||||
// before that write, which then overwrites the newer texels. (An image the recording only
|
||||
// read keeps the old trade: its earlier reads see the newer texels.)
|
||||
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The next sync would record client texels into the image that is live now: same shape (a
|
||||
// shape change mints a new image instead) and content moved since the last sync.
|
||||
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The live resource, or null.
|
||||
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
@@ -742,22 +696,6 @@ private:
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
SizeT PruneDeadTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The two halves of SyncTextureResourceByHandle: shape from the resource record's
|
||||
// descriptor (create / recreate / compatibility), then the pending-upload walk against the
|
||||
// server's staged store. UploadPendingWireLevels mutates the applier's record (consumed
|
||||
// entries are dropped), which is why the record is a non-const reference.
|
||||
Bool SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource,
|
||||
Bool requireStorage);
|
||||
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
|
||||
TextureResource& resource);
|
||||
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
|
||||
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
|
||||
// across map-touching calls.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireTextureResources;
|
||||
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
|
||||
#endif
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@
|
||||
#pragma once
|
||||
#include "Config.h"
|
||||
#include "FrameContext.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "ProgramFactory.h"
|
||||
#include "SwapchainObject.h"
|
||||
@@ -22,19 +21,10 @@
|
||||
#include "VkSamplerManager.h"
|
||||
#include "VkTextureManager.h"
|
||||
#include "VkTimerQueryManager.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireRenderPassCompatibility.h"
|
||||
#endif
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo
|
||||
// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged.
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
@@ -144,23 +134,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// IBufferCopyCommandProvider: recording command buffer, outside any
|
||||
// render pass, for immediate staged buffer copies.
|
||||
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager& GetWireBufferManager() { return m_bufferManager; }
|
||||
Bool FlushWirePendingCommandsForTextureUpdate() {
|
||||
// A new texture upload must not race graphics work that samples
|
||||
// the old texels. No current recording does not imply no in-flight
|
||||
// submission: finish the recorded work, then wait its whole prefix.
|
||||
if (HasPendingRecordedWork() && !FlushPendingCommands()) return false;
|
||||
return WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX);
|
||||
}
|
||||
// P7 wave 2 package B3: the submission that will carry whatever is recorded NEXT.
|
||||
// RetireWireObjects (WireDraw.inc) already tags future objects with exactly this, and
|
||||
// for the same reason: a draw being set up now is not in any submission yet, so the
|
||||
// index that names it is one past the counter. Deliberately NOT
|
||||
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
|
||||
// true for a fence taken at that instant, wrong for work about to be recorded.
|
||||
Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; }
|
||||
#endif
|
||||
|
||||
// FrameContext::IRecordingObserver: prepares the frame's timer-query
|
||||
// pool (harvest + reset) right after the frame command buffer begins
|
||||
@@ -233,11 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Server resource handle in, tightly packed owned bytes out. No frontend
|
||||
// texture, pixel-pack state or PBO is consulted by this readback.
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& ownedBytes);
|
||||
#endif
|
||||
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
|
||||
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
|
||||
// CPU repacking into the requested client layout).
|
||||
@@ -267,16 +235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
// One depth/stencil copy whose source and destination are the same format and whose region
|
||||
// is a same-size, same-layer rectangle: the aspect named by `aspect` is staged through a
|
||||
// device-local buffer so the destination's other aspect cannot be overwritten by the
|
||||
// packed native word a whole-image copy moves.
|
||||
Bool CopyDepthStencilAspectThroughBuffer(FrameContext::FrameData& frame, VkImage srcImage,
|
||||
VkImage dstImage, VkFormat format, Uint32 srcMipLevel,
|
||||
Uint32 srcBaseArrayLayer, Uint32 dstMipLevel,
|
||||
Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
|
||||
GLint dstY, GLsizei width, GLsizei height,
|
||||
VkImageAspectFlagBits aspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
@@ -327,6 +285,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// signal as soon as the GPU actually finishes, which MC 1.21.5's
|
||||
// fence-paced ring buffers rely on to recycle their space.
|
||||
Uint64 GetSyncPointSubmitIndex() const;
|
||||
// For tests: the per-frame descriptor pools (see DescriptorPoolCensus.h).
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() const {
|
||||
return m_uniformManager ? m_uniformManager->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
|
||||
}
|
||||
// Non-blocking: polls outstanding submission fences and reports
|
||||
// whether every submission up to `submitIndex` has completed.
|
||||
Bool IsSubmitIndexComplete(Uint64 submitIndex);
|
||||
@@ -390,6 +352,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
|
||||
|
||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||
// eglSwapInterval. Records the request; Present rebuilds the swapchain for it once the
|
||||
// current frame is on screen, and only when it maps to a different present mode. A
|
||||
// renderer created without an app window ignores it: its surface (headless, or an
|
||||
// AImageReader nobody consumes) is never displayed, and FIFO there would block once
|
||||
// the reader's queue fills.
|
||||
void SetSwapInterval(Int interval);
|
||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
||||
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
||||
@@ -416,20 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Int padding[3] = {0, 0, 0};
|
||||
};
|
||||
|
||||
// P7 wave 2-B2, CONTRACT-P7 §5.2 (B'): THE FRONTEND MEMBERS ARE PULL-BUILD ONLY. In a
|
||||
// disaggregated build the monolith arm blits and generates depth mips with the baked
|
||||
// modules (WireColorBlit.inc, WireDepthMipmap.inc), so no DirectVulkan object holds a
|
||||
// ProgramObject, a ShaderObject or a SamplerObject any more - which is what takes
|
||||
// ProgramObject::{AllocateLifetimeId, AttachShader, Link, ~ProgramObject},
|
||||
// ShaderObject::{SetShaderSource, Compile, JoinPendingCompile, ReleaseCompileNode,
|
||||
// DropCompileNode} and SamplerObject::{SetWrapS, SetWrapT, SetWrapR, SetLodRange} out
|
||||
// of the link ratchet's `p7-magma` bucket (§4.2). The destructor of a SharedPtr member
|
||||
// is a reference too, so the MEMBER has to go and not only its construction.
|
||||
//
|
||||
// The structs stay, empty, so ShutdownBlitResources and the two `= {}` assignments read
|
||||
// the same on both builds.
|
||||
struct BlitResources {
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
SharedPtr<MG_State::GLState::ProgramObject> program;
|
||||
SharedPtr<MG_State::GLState::SamplerObject> nearestSampler;
|
||||
SharedPtr<MG_State::GLState::SamplerObject> linearSampler;
|
||||
@@ -437,18 +392,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Int dstRectLocation = -1;
|
||||
Int surfaceTransformLocation = -1;
|
||||
Uint32 samplerBinding = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct DepthMipmapResources {
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
SharedPtr<MG_State::GLState::ProgramObject> program;
|
||||
Int srcRectLocation = -1;
|
||||
Int dstRectLocation = -1;
|
||||
Int surfaceTransformLocation = -1;
|
||||
Int srcTexelSizeLocation = -1;
|
||||
Uint32 samplerBinding = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
// A single-sample staging image for multisample-resolve blits that also have to change
|
||||
@@ -484,189 +436,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
struct WireImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkExtent2D extent{};
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImageLayout* trackedLayout = nullptr;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageAspectFlags aspect = 0;
|
||||
// Zero for the default framebuffer, which no arm that reads this ever reaches.
|
||||
VkImageUsageFlags usageFlags = 0;
|
||||
Uint32 level = 0, layer = 0, layers = 1, levels = 1;
|
||||
Bool isDefault = false;
|
||||
// Set when isDefault: the swapchain image index this role resolved to.
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
Bool is3D = false;
|
||||
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
// `isWriteTarget` selects which side of the default framebuffer this resolves: a write
|
||||
// re-points it at the image Present() acquired, a read keeps the current one (see
|
||||
// m_defaultFramebufferImageIndex). Reads must pass false.
|
||||
WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
|
||||
Bool isWriteTarget = false);
|
||||
void TransitionWireImage(WireImage& image, VkImageLayout layout);
|
||||
void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer = -1);
|
||||
void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter);
|
||||
void ResolveWireDepthStencil(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1);
|
||||
// One aspect of a depth/stencil image through a buffer (source in TRANSFER_SRC_OPTIMAL,
|
||||
// destination in TRANSFER_DST_OPTIMAL, one format). The returned buffer is named by the
|
||||
// recorded commands and must be retired with them.
|
||||
UniquePtr<VkBufferObject> CopyWireAspectThroughBuffer(VkImage srcImage, Uint32 srcLevel, Uint32 srcLayer,
|
||||
GLint sx, GLint sy, const WireImage& destination,
|
||||
GLint dx, GLint dy, Uint32 width, Uint32 height,
|
||||
VkImageAspectFlags aspect, VkFormat format,
|
||||
Bool mirrorY, const char* fatalDetail);
|
||||
// A single-sample depth/stencil blit whose rectangles make it a plain copy, performed
|
||||
// without vkCmdBlitImage (P7 gate 5). False when the shape is not a plain copy.
|
||||
Bool CopyWireDepthStencilBlit(WireImage& source, WireImage& destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, VkImageAspectFlags aspect);
|
||||
// True when the rectangles give a multisample depth/stencil resolve a shape it declines
|
||||
// (a scale, an X mirror, a Y mirror across two formats); the decline has then been
|
||||
// logged and its INVALID_OPERATION recorded. BlitWireFramebuffers asks it BEFORE any
|
||||
// aspect runs, so a call the SHAPE declines leaves every attachment alone - the user
|
||||
// and the default draw framebuffer alike. The CAPABILITY decline inside
|
||||
// ResolveWireDepthStencil (a stencil-only resolve without VK_EXT_shader_stencil_export
|
||||
// on a device without VK_KHR_depth_stencil_resolve) is still decided per aspect, after
|
||||
// colour ran; rule I (a) accepts that (notes/p7/magma-b2.md §2.10).
|
||||
Bool DeclineWireDepthStencilResolveShape(const WireImage& source, const WireImage& destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1);
|
||||
PFN_vkCreateRenderPass2 m_wireCreateRenderPass2 = nullptr;
|
||||
VkResolveModeFlags m_wireDepthResolveModes = 0;
|
||||
VkResolveModeFlags m_wireStencilResolveModes = 0;
|
||||
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the SECOND arm of the
|
||||
// multisample depth/stencil resolve, for the device that does not carry
|
||||
// VK_KHR_depth_stencil_resolve. One baked pass per aspect, sample zero, no frontend
|
||||
// object - see WireMultisampleResolve.inc. `resolved` must be a single-sample image of
|
||||
// the source's format and extent; the rect is expressed by the scissor.
|
||||
Bool ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
|
||||
VkImageAspectFlags aspect, GLint sx0, GLint sy0,
|
||||
Uint32 width, Uint32 height);
|
||||
struct WireMultisampleResolveResources;
|
||||
WireMultisampleResolveResources* m_wireMultisampleResolveResources = nullptr;
|
||||
void DestroyWireMultisampleResolveResources();
|
||||
Bool m_wireShaderStencilExport = false;
|
||||
// VK_EXT_robustness2 (or its KHR promotion) with ONLY nullDescriptor enabled, wire arms
|
||||
// only: an invalid image unit binds a null storage descriptor (codex closeout finding 2).
|
||||
Bool m_wireNullDescriptor = false;
|
||||
// The resolve probe's verdict for this device (WireDepthResolveArm.h,
|
||||
// WireDepthResolveProbe.h): true when the no-draw render pass was measured leaving its
|
||||
// target unwritten while the shader control resolved - the shader arm then resolves
|
||||
// first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe).
|
||||
Bool m_wirePreferShaderDepthResolve = false;
|
||||
// Runs the probe (memoized per device identity) and sets the member above. Called at the end of
|
||||
// device creation, after ArmPrimGenReroute: it records on m_graphicsQueue.
|
||||
void ArmWireDepthResolveOrder();
|
||||
Bool BlitWireColorToDefault(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter);
|
||||
Bool BlitWireColorImage(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap);
|
||||
// P7 wave 2-B, CONTRACT-P7 §3.2 (`default-color-blit-shape`): the rotated arm. Moves the
|
||||
// source region into an owned single-sample 2D image of a sampled-float format in the
|
||||
// same size-compatibility class, then shader-blits THAT to the rotated default
|
||||
// framebuffer - the shapes BlitWireColorImage itself cannot sample (an integer format, a
|
||||
// 3D or array source) become shapes it can.
|
||||
Bool BlitWireColorToDefaultThroughScratch(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1,
|
||||
GLenum filter);
|
||||
struct WireColorBlitResources;
|
||||
WireColorBlitResources* m_wireColorBlitResources = nullptr;
|
||||
void DestroyWireColorBlitResources();
|
||||
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): the baked depth-mip program (WireDepthMipmap.inc).
|
||||
// One level of a depth chain, source and destination being two depth subresources of
|
||||
// the same image; the caller loops. It owns no frontend object, which is what the
|
||||
// monolith arm's GenerateDepthMipmapWithShader cannot say.
|
||||
Bool GenerateWireDepthMipLevel(WireImage source, WireImage destination);
|
||||
struct WireDepthMipmapResources;
|
||||
WireDepthMipmapResources* m_wireDepthMipmapResources = nullptr;
|
||||
void DestroyWireDepthMipmapResources();
|
||||
// P7 wave 2-B (exit gate 3): the readback's own write-visibility barrier, recorded before
|
||||
// the copy instead of inferred from the image's tracked layout. See WireFramebuffer.inc.
|
||||
void RecordWireReadbackWriteBarrier();
|
||||
void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
GLenum format, GLenum type, void* pixels);
|
||||
void GenerateWireMipmap();
|
||||
Bool SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||
const DrawCmdParam& drawParams, const IndexBufferView* indices);
|
||||
void DestroyWireDrawPass();
|
||||
void RetireWireDrawPass();
|
||||
struct WireDrawAttachmentKey {
|
||||
MG_Pipe::MGPipeHandle storage = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint32 resourceKind = 0;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = 0, requestedAspect = 0;
|
||||
Uint32 level = 0, layer = 0, layers = 1, imageLevels = 1;
|
||||
Uint32 width = 0, height = 0;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
Bool operator==(const WireDrawAttachmentKey&) const = default;
|
||||
};
|
||||
struct WireDrawPassKey {
|
||||
Vector<WireDrawAttachmentKey> attachments;
|
||||
Vector<Uint32> colorReferences;
|
||||
Uint32 depthReference = VK_ATTACHMENT_UNUSED;
|
||||
Uint32 width = 0, height = 0, layers = 1;
|
||||
Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0;
|
||||
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
Bool isDefault = false, framebufferSrgb = false;
|
||||
Bool operator==(const WireDrawPassKey&) const = default;
|
||||
};
|
||||
struct WireDrawPassCacheEntry {
|
||||
WireDrawPassKey key;
|
||||
UniquePtr<RenderPassEntry> pass;
|
||||
Vector<VkImageView> views;
|
||||
};
|
||||
// Objects only: every draw still ends/begins its pass and records the
|
||||
// existing memory dependencies. Each slot is cleared after its fence.
|
||||
Bool AcquireCachedWireDrawPass(const WireDrawPassKey& key);
|
||||
void ClearWireDrawPassCache(Uint32 frameIndex);
|
||||
void ClearAllWireDrawPassCaches();
|
||||
Vector<Vector<WireDrawPassCacheEntry>> m_wireDrawPassCaches;
|
||||
WireDrawPassKey m_wireDrawPassKey;
|
||||
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
|
||||
struct WireRetiredObjects {
|
||||
Uint64 submitIndex = 0;
|
||||
UniquePtr<RenderPassEntry> drawPass;
|
||||
Vector<VkFramebuffer> framebuffers;
|
||||
Vector<VkRenderPass> renderPasses;
|
||||
Vector<VkImageView> imageViews;
|
||||
Vector<VkDescriptorPool> descriptorPools;
|
||||
// Transfer/resolve scratch storage belongs to the submission that
|
||||
// consumes it, just like the views and framebuffer above.
|
||||
Vector<UniquePtr<VkTextureManager::TextureResource>> textures;
|
||||
Vector<UniquePtr<VkBufferObject>> buffers;
|
||||
};
|
||||
void RetireWireObjects(WireRetiredObjects objects);
|
||||
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
|
||||
Vector<WireRetiredObjects> m_wireRetiredObjects;
|
||||
Uint32 m_wirePreparationDepth = 0;
|
||||
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
|
||||
void RewindWireDescriptorSetsIfDue();
|
||||
UniquePtr<RenderPassEntry> m_wireDrawPass;
|
||||
Vector<VkImageView> m_wireDrawViews;
|
||||
VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId = 0);
|
||||
|
||||
void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||
GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
#endif
|
||||
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer,
|
||||
const ClearAttachmentPayload& clearPayload);
|
||||
@@ -713,11 +482,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// was waited or the device was idled); drops their records and
|
||||
// recycles pooled fences.
|
||||
void OnSubmitsCompletedUpTo(Uint64 submitIndex);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A later fence alone cannot retire earlier submissions. Wait for the
|
||||
// whole still-live prefix before advancing the completed submit floor.
|
||||
Bool WaitForSubmitsUpTo(Uint64 submitIndex, Uint64 timeoutNs);
|
||||
#endif
|
||||
VkFence AcquirePooledSubmitFence();
|
||||
void DestroySubmitFencePool();
|
||||
Bool HasPendingRecordedWork() const;
|
||||
@@ -730,6 +494,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only), so the presenting path's frames-in-flight pipelining is
|
||||
// untouched. Returns true when the drain ran.
|
||||
Bool TryDrainFrameTransients();
|
||||
// Bounded recording: called at the top of every GL draw and dispatch, before anything
|
||||
// of that call is recorded. Once the open frame command buffer holds
|
||||
// MagmaMaxDrawsPerCommandBuffer draws/dispatches, submits it (FlushPendingCommands) so
|
||||
// the call continues on a fresh one, then waits until at most frames-in-flight + 1
|
||||
// submissions are outstanding. A driver backs a command buffer with GPU memory it only
|
||||
// returns when the buffer is freed (Adreno: one 16 KiB mapping per chunk), so a frame
|
||||
// that is never split grows it without bound - rd12's loading frame records ~1M draws
|
||||
// into one buffer and runs the process out of vm.max_map_count.
|
||||
void SplitOversizedRecording();
|
||||
|
||||
Vector<SubmitRecord> m_inFlightSubmits;
|
||||
Vector<VkFence> m_freeSubmitFences;
|
||||
@@ -741,6 +514,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// aging clocks, while present-less loops still cross a boundary every few
|
||||
// iterations. Reset in Present.
|
||||
Uint32 m_drainsSinceLastPresent = 0;
|
||||
// GL draws/dispatches recorded into the open frame command buffer; reset whenever a
|
||||
// recording begins (OnFrameCommandRecordingBegan). See SplitOversizedRecording.
|
||||
Uint32 m_drawsInRecording = 0;
|
||||
Uint64 m_oversizedRecordingSplits = 0;
|
||||
|
||||
NativeWindowType m_window = 0;
|
||||
void* m_platformDisplay = nullptr;
|
||||
@@ -760,6 +537,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shutdown() deletes it.
|
||||
void* m_fallbackImageReader = nullptr;
|
||||
VulkanRendererConfig m_config;
|
||||
// Constructed for the app's window rather than a pbuffer (m_window is later filled in
|
||||
// for the pbuffer fallbacks too, so it cannot tell them apart).
|
||||
Bool m_presentsToAppWindow = false;
|
||||
// The m_config.SwapInterval the live swapchain was built for; Present compares the two.
|
||||
Optional<Int> m_swapchainSwapInterval;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
// swapchain is unusable/out of date, so Present drops frames instead of
|
||||
@@ -921,18 +703,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// per object: one group of four slots each, handed out on first use.
|
||||
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
// Which transform feedback object owns each slot group, by the frontend's never-reused
|
||||
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
|
||||
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
|
||||
// inherited the dead one's slot, and since nothing ever removed an entry the map also
|
||||
// grew for the life of the context. A fixed table cannot do either: a group is taken over
|
||||
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
|
||||
// counters can still be resumed never loses them, and a dead object's group comes back.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
|
||||
// Tie-break among reclaimable groups only; never on its own, because the paused span the
|
||||
// groups exist for is by construction the least recently used one.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
|
||||
Uint64 m_xfbCounterSlotUseSerial = 0;
|
||||
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||
Uint32 m_xfbNextCounterSlot = 0;
|
||||
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||
// counter-buffer resume on the next captured draw of the same span.
|
||||
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||
@@ -942,14 +714,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
||||
// when GL transform feedback is active; binds capture buffers on demand.
|
||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
Bool BeginXfbCaptureWithBuffers(FrameContext::FrameData& frame, Uint32 count,
|
||||
const VkBuffer* buffers, const VkDeviceSize* offsets,
|
||||
const VkDeviceSize* sizes);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool BeginWireXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
#endif
|
||||
Uint32 m_currentDrawXfbBufferCount = 0;
|
||||
Uint32 m_currentDrawXfbBufferMask = 0;
|
||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||
// Makes the captured bytes visible to whatever reads them next. Deferred rather than
|
||||
// recorded next to the capture, because the capturing draw runs inside a render pass
|
||||
@@ -1057,29 +821,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferManager m_bufferManager;
|
||||
|
||||
Uint m_imageIndexAcquired = 0;
|
||||
// The swapchain image GL addresses as the default framebuffer: the one a readback, a
|
||||
// blit source or a copy out of it resolves, and the one the app last rendered into.
|
||||
// Present() runs ahead - it presents the frame and acquires the next image without
|
||||
// waiting for the apply that would render into it (kWaitPresent) - so this must NOT
|
||||
// follow m_imageIndexAcquired. Before it existed a readback resolved the freshly
|
||||
// acquired image, which nothing had written yet: the pure-black sundial-lite frame.
|
||||
Uint m_defaultFramebufferImageIndex = 0;
|
||||
FrameContext m_frameContext;
|
||||
|
||||
// Resolves the swapchain image index a READ of the default framebuffer must use. A
|
||||
// non-default framebuffer ignores the index; it keeps the acquired one, which still
|
||||
// keys its render-pass cache.
|
||||
Uint32 DefaultFramebufferReadIndex(Bool isDefaultFramebuffer) const {
|
||||
return isDefaultFramebuffer ? m_defaultFramebufferImageIndex : m_imageIndexAcquired;
|
||||
}
|
||||
|
||||
// Same for a WRITE: the default framebuffer starts addressing the image Present()
|
||||
// acquired only once something renders into it again.
|
||||
Uint32 DefaultFramebufferWriteIndex(Bool isDefaultFramebuffer) {
|
||||
if (isDefaultFramebuffer) m_defaultFramebufferImageIndex = m_imageIndexAcquired;
|
||||
return DefaultFramebufferReadIndex(isDefaultFramebuffer);
|
||||
}
|
||||
|
||||
UniquePtr<PipelineFactory> m_pipelineFactory;
|
||||
// Single-slot "last pipeline" memo: skip the per-draw GetOrCreatePipeline work (state
|
||||
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
|
||||
@@ -1095,34 +838,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 wireRenderPassCompatibilityId = 0;
|
||||
#endif
|
||||
// The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the
|
||||
// handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see
|
||||
// ComputePipelineStateHash), not the monotonic pipeline-state version: the version
|
||||
// never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even
|
||||
// though the state alternates between two values the memo already holds.
|
||||
// VALUE hash of the pipeline-relevant fixed-function state (see
|
||||
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
||||
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
||||
// all entries forever even though the state alternates between two
|
||||
// values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The HANDLE arm's key component, and the whole of D12.1: the CLIENT already
|
||||
// hashed the pipeline subset of RenderStateParameters and minted a content-
|
||||
// addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache),
|
||||
// so re-hashing the same 396 bytes here was work the boundary had already done.
|
||||
// Two draws share a CSO handle exactly when their pipeline bytes are equal, and
|
||||
// the client's subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||
// so the handle discriminates at least as finely as the hash it replaces.
|
||||
//
|
||||
// renderPassHash STAYS beside it and is what keeps this key complete: the CSO
|
||||
// carries GL state only, while colorAttachmentCount and the rasterization sample
|
||||
// count - which ComputePipelineStateHash folded in through its signature and
|
||||
// through ResolveEffectiveSampleMask - are render-pass facts that the render-pass
|
||||
// hash already separates.
|
||||
//
|
||||
// Null in an entry minted by the legacy arm, so entries of the two arms can never
|
||||
// match each other: the compare below tests BOTH components.
|
||||
MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||
@@ -1136,142 +857,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 0;
|
||||
Uint32 m_pipelineMemoNext = 0;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the
|
||||
// render-state CSO this draw is keyed on, or the null handle when the pre-handle arm
|
||||
// is the one that runs.
|
||||
//
|
||||
// Under the handle arm the memo's state key IS this handle. The client hashed those
|
||||
// 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so
|
||||
// recomputing an overlapping hash here was work the boundary had already done; the
|
||||
// client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||
// so the handle discriminates at least as finely as the hash it replaces. What the
|
||||
// handle does NOT carry is the render-pass side - colorAttachmentCount and the
|
||||
// rasterization sample count, which ComputePipelineStateHash folded in through its
|
||||
// signature and through ResolveEffectiveSampleMask - and that is exactly why
|
||||
// entry.renderPassHash stays in the key beside it.
|
||||
//
|
||||
// The arm is live only when the render-state subsystem is migrated in this run AND the
|
||||
// client has actually bound a CSO. The second half is not belt and braces: a tree whose
|
||||
// tracker does not emit create/bind_render_state yet has no handle to key on, and
|
||||
// delete_render_state clears the binding (MG_Pipe/PipeApply.cpp), so the null handle is
|
||||
// reachable on any tree. Keying every draw on it would alias every render state onto
|
||||
// one memo entry, so a null handle means "fall back to a state hash" - never an abort,
|
||||
// and never a per-draw consultation of the legacy-memo lever: bit 0 is not a Track-H
|
||||
// subsystem (D14 labels only bits 5 and 6 that), and the lever's Fatal is a STARTUP
|
||||
// one, in MagmaPipeValidateSubsystemConfiguration.
|
||||
//
|
||||
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
||||
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
||||
// at every shipped log level.
|
||||
//
|
||||
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
|
||||
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
|
||||
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
|
||||
// this comment claimed - and this site is on the per-draw pipeline path in the very
|
||||
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
|
||||
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
|
||||
// load of a member that is false exactly once. Single-threaded like the rest of the
|
||||
// renderer, and per renderer rather than per process, which is also the right scope: a
|
||||
// second context that never binds a CSO deserves to say so.
|
||||
//
|
||||
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
|
||||
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
|
||||
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
|
||||
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
|
||||
//
|
||||
// Push-only by construction: the pull build does not compile this function at all, so
|
||||
// its two callers are statement-for-statement what they were (G1).
|
||||
//
|
||||
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
|
||||
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
|
||||
// specifies. In a multi-context process this reads whatever CSO another context last
|
||||
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
|
||||
// consumer, so it is named here rather than left for both reviews to assume the other
|
||||
// caught it.
|
||||
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
||||
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
return MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
|
||||
m_pipelineCsoFallbackWarned = true;
|
||||
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
||||
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
||||
"handle (no tracker on this build, or a draw between "
|
||||
"delete_render_state and the next bind)");
|
||||
}
|
||||
return boundCso;
|
||||
}
|
||||
// Latch for the warning above. Mutable because the resolve is const and the latch is
|
||||
// not part of the renderer's observable state.
|
||||
mutable Bool m_pipelineCsoFallbackWarned = false;
|
||||
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
||||
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
||||
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
||||
// (version, colorAttachmentCount, sampleCount) proves the bytes are unchanged.
|
||||
//
|
||||
// [deviation from D12.1] The brief deletes this gate and its cached fields outright.
|
||||
// They cannot go while a no-CSO draw is reachable - and it is, on any tree: a draw
|
||||
// between delete_render_state and the next bind has no handle. On a tree whose tracker
|
||||
// binds a CSO these five words are written once and never read again; they retire for
|
||||
// real when the pull path does, at P13.
|
||||
Uint64 ResolveFallbackPipelineStateHash(Uint renderStateVersion, Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) {
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != colorAttachmentCount ||
|
||||
m_pipelineStateHashSampleCount != rasterizationSamples) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(colorAttachmentCount, rasterizationSamples);
|
||||
#else
|
||||
m_pipelineStateHash = ComputePipelineSubsetStateHashFallback();
|
||||
#endif
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = colorAttachmentCount;
|
||||
m_pipelineStateHashSampleCount = rasterizationSamples;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
return m_pipelineStateHash;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
#if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The same answer as ComputePipelineStateHash, computed from the P2 chunk table
|
||||
// instead of from a hand-written field list, for the build that compiles no
|
||||
// pre-handle arm (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF). It is the CLIENT's own
|
||||
// hash function - MGPipeComputePipelineSubsetHash over the 396 pipeline bytes - so a
|
||||
// draw keyed on it and a draw keyed on a CSO handle are keyed on the same equivalence
|
||||
// class of state, and the render-pass facts stay separated by renderPassHash either
|
||||
// way. This is what makes the no-legacy build RUNNABLE rather than a configuration
|
||||
// that aborts on the first draw that arrives without a CSO.
|
||||
Uint64 ComputePipelineSubsetStateHashFallback() const;
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// THE PRE-HANDLE ARM (P2 brief D12.1 / D14). Hash of every fixed-function GL state the
|
||||
// pipeline payload reads that the memo key's other fields (mode / program / vertex
|
||||
// input / render pass / transform flags) do not already pin down. Equal hash under an
|
||||
// equal rest of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||
// Hash of every fixed-function GL state the pipeline payload reads that the
|
||||
// memo key's other fields (mode / program / vertex input / render pass /
|
||||
// transform flags) do not already pin down. Equal hash under an equal rest
|
||||
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
//
|
||||
// The handle arm computes none of this: the client hashed the same bytes when it
|
||||
// minted the CSO, so all five cached-hash members below exist only to avoid a
|
||||
// re-hash the handle arm never performs.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const;
|
||||
#endif
|
||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||
// and the pipeline-state memo word so the two cannot disagree. NOT part of the legacy
|
||||
// arm: it is a PAYLOAD computation that depends on rasterizationSamples, so it survives
|
||||
// the re-key and keeps reading Multisample / SampleMask / SampleMaskValue out of the
|
||||
// working block.
|
||||
// and the pipeline-state memo word so the two cannot disagree.
|
||||
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
||||
// ResolveFallbackPipelineStateHash's cache. Written once and never read again on a
|
||||
// build whose client binds a render-state CSO; see that function for why it survives
|
||||
// the re-key at all.
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||
@@ -1297,8 +895,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||
// context whose monotonic version the cache is keyed on. The handle arm has no
|
||||
// such cache to drop - a CSO handle is not derived from a monotonic version.
|
||||
// context whose monotonic version the cache is keyed on.
|
||||
void InvalidatePipelineMemo() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
@@ -1389,13 +986,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// common shape), and "the VAO did not move" would then skip the layout
|
||||
// re-resolve for a different VAO.
|
||||
Uint64 vaoLifetimeId = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.4: the handle arm's answer to the same question, and one compare rather
|
||||
// than the pair above. Kept BESIDE them rather than replacing them because the
|
||||
// pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot
|
||||
// is a value struct, not a wire type.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||
@@ -1670,13 +1260,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||
// hash, per-binding live buffer pointers and slice epochs).
|
||||
struct alignas(64) VaoDrawMemo {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an
|
||||
// identity, so the pointer-plus-lifetime-id pair below stops being a key here;
|
||||
// the slot also picks the table entry, so the address hash and the two-way probe
|
||||
// go with it. Null in an entry that has never been claimed.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||
@@ -1703,48 +1286,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||
// stable for the duration of a draw, which the EBO memo handoff
|
||||
// (m_currentDrawResolvedEntry) relies on.
|
||||
//
|
||||
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
|
||||
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
|
||||
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
|
||||
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
|
||||
// whole of review v2's MAJOR 1:
|
||||
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
|
||||
// table with the live VAO set is megabytes on a platform with an LMK, where the
|
||||
// other two memos are 48 B and can afford it;
|
||||
// * this is the ONLY one of the three memos that had a capacity before this package.
|
||||
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
|
||||
// cost on the base ref, so at any working-set size this table is no worse than what
|
||||
// it replaces - and strictly better below capacity, where the handle is a bijection
|
||||
// with the slot and the two-way probe never collides at all. The other two memos
|
||||
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
|
||||
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
|
||||
// derive the same handle for the same VAO. Per renderer, never a process-global: a
|
||||
// global would share one table and one reclamation clock across two live contexts and
|
||||
// outlive every one of them (review v2 minor 4).
|
||||
MagmaPipeIdentityTables m_pipeIdentity;
|
||||
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
|
||||
// first, so there is no second memo in front of it here.
|
||||
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
||||
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
|
||||
vao.GetLifetimeId());
|
||||
}
|
||||
#endif
|
||||
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
||||
// memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL
|
||||
// build's three readers keep compiling to the very same two loads they always did -
|
||||
// G1 admits no resize, and an out-of-line call here would be one.
|
||||
[[gnu::always_inline]] inline Bool VaoContentHashIfKnown(
|
||||
const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash);
|
||||
#else
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#endif
|
||||
}
|
||||
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||
@@ -1779,7 +1322,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
@@ -1791,14 +1334,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// flush the pending recording (see the body), which retires the current command buffer.
|
||||
Bool PrepareStorageImageTextures(
|
||||
FrameContext::FrameData& frame,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
||||
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
||||
// storage descriptor continues to name the application texture.
|
||||
Bool PrepareSamplerImageFeedbackSnapshots(
|
||||
FrameContext::FrameData& frame,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
VkPipelineStageFlags consumerShaderStageMask);
|
||||
|
||||
@@ -1861,8 +1404,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
// Records the texture's queued clears. By default the texture is synced first, which
|
||||
// uploads any pending texels BEFORE the clear - right only for texels written before it.
|
||||
// `uploadPendingTexels = false` leaves them pending (OrderPendingUploadAfterRecording).
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
Bool uploadPendingTexels = true);
|
||||
// Texture uploads run in a batch submitted ahead of the frame's recording, so the upload
|
||||
// the next sync of `texture` records would land BEFORE a write already recorded into the
|
||||
// same image (a render, clear, blit or copy), and before a clear still queued for it (such
|
||||
// a clear predates the texels: SettleTexelsBeforeQueuedClear). When either holds, this
|
||||
// records the queued clear without the texels and submits the recording, so the upload
|
||||
// follows both. An image the recording only READ keeps the old trade: no submit, and its
|
||||
// earlier reads see the newer texels. It may end the render pass and replace
|
||||
// frame.commandBuffer (recording again on return), so call it only where the caller holds
|
||||
// no render-pass or command-buffer state across it. Returns whether it submitted.
|
||||
Bool OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
// Called for each texture a glClear/glClearBuffer* is about to queue a clear on: uploads
|
||||
// whatever texels are still pending (and mints the image if there is none), so that any
|
||||
// texel pending when the clear is materialized was written after it.
|
||||
void SettleTexelsBeforeQueuedClear(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// The multisample arm of the above. Split out rather than branched inline because it
|
||||
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
|
||||
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
|
||||
@@ -1885,12 +1448,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const ClearAttachmentPayload& payload);
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P7 wave 2-B2 (CONTRACT-P7 §5.2, (B')): the hidden blit program's pipeline. A
|
||||
// disaggregated build has no hidden program on either arm, so this has neither a body
|
||||
// nor a caller there.
|
||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||
#endif
|
||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#version 450
|
||||
layout(set = 0, binding = 0) uniform sampler2D sourceImage;
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
// Source framebuffer clipping preserves the original scale, rather than
|
||||
// extending the edge texels into pixels outside the source framebuffer.
|
||||
if (any(lessThan(texCoord, vec2(0.0))) || any(greaterThan(texCoord, vec2(1.0)))) discard;
|
||||
// Explicit LOD is required on Adreno 650: implicit LOD through a rotated
|
||||
// single-mip UBWC render target can read outside its allocation.
|
||||
outColor = textureLod(sourceImage, texCoord, 0.0);
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc.
|
||||
// The native blit owns no frontend object and never invokes the GL shader compiler.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireColorBlitSpirv.h"
|
||||
|
||||
struct VulkanRenderer::WireColorBlitResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireColorBlitResources() {
|
||||
if (!m_wireColorBlitResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
|
||||
// descriptor pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
vkDestroySampler(m_device, cache.nearest, nullptr);
|
||||
vkDestroySampler(m_device, cache.linear, nullptr);
|
||||
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.fragment, nullptr);
|
||||
delete m_wireColorBlitResources;
|
||||
m_wireColorBlitResources = nullptr;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BlitWireColorToDefault(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
|
||||
if (!destination.isDefault) return false;
|
||||
return BlitWireColorImage(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1, filter, false);
|
||||
}
|
||||
|
||||
// P7 wave 2-B (CONTRACT-P7 §3.2, `default-color-blit-shape`). The shader blit below can only
|
||||
// SAMPLE its source: sampler2D, a float result, and one of two 2D view types. A framebuffer
|
||||
// whose colour attachment is RGBA8UI, or a layer of a 3D texture, is none of those, and on a
|
||||
// rotated Android surface there is no other arm - an axis-aligned vkCmdBlitImage cannot
|
||||
// express a quarter turn. So the source region becomes an owned 2D image first, through
|
||||
// vkCmdCopyImage, which cares about the texel BLOCK SIZE and not about how the block is
|
||||
// interpreted; the scratch then takes the shader blit as an ordinary 2D float source.
|
||||
//
|
||||
// REINTERPRETATION IS THE POINT FOR THE INTEGER CASE, and GL agrees: glBlitFramebuffer
|
||||
// between an integer and a normalized format is INVALID_OPERATION (GL 4.6 core 18.3.1), so
|
||||
// there is no defined result to be wrong about - only a shape that used to end the session
|
||||
// and now produces the source's bits. For a float source of the wrong VIEW TYPE the copy is
|
||||
// a plain slice extraction and the result is exactly right.
|
||||
Bool VulkanRenderer::BlitWireColorToDefaultThroughScratch(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
|
||||
if (!destination.isDefault || source.isDefault) return false;
|
||||
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
|
||||
return false;
|
||||
if (!source.image || !destination.image || source.image == destination.image) return false;
|
||||
const Int64 width = std::abs(static_cast<Int64>(sx1) - sx0);
|
||||
const Int64 height = std::abs(static_cast<Int64>(sy1) - sy0);
|
||||
if (!width || !height || dx0 == dx1 || dy0 == dy1) return true;
|
||||
if (std::min(sx0, sx1) < 0 || std::min(sy0, sy1) < 0 ||
|
||||
static_cast<Uint64>(std::max(sx0, sx1)) > source.extent.width ||
|
||||
static_cast<Uint64>(std::max(sy0, sy1)) > source.extent.height) return false;
|
||||
// A compressed source cannot be a colour attachment, so it cannot be a framebuffer blit's
|
||||
// source either - but the block arithmetic below would silently produce nonsense for one,
|
||||
// and "cannot happen" is the reason the guards this package retires were written as
|
||||
// asserts. The size-compatibility classes are Vulkan's own (VkFormat texel block size);
|
||||
// the destination format is whatever the swapchain uses and is always sampled-float.
|
||||
if (vkuFormatIsCompressed(source.format)) return false;
|
||||
VkFormat scratchFormat = VK_FORMAT_UNDEFINED;
|
||||
switch (vkuFormatTexelBlockSize(source.format)) {
|
||||
case 1: scratchFormat = VK_FORMAT_R8_UNORM; break;
|
||||
case 2: scratchFormat = VK_FORMAT_R8G8_UNORM; break;
|
||||
case 4: scratchFormat = VK_FORMAT_R8G8B8A8_UNORM; break;
|
||||
case 8: scratchFormat = VK_FORMAT_R16G16B16A16_SFLOAT; break;
|
||||
case 16: scratchFormat = VK_FORMAT_R32G32B32A32_SFLOAT; break;
|
||||
default: return false;
|
||||
}
|
||||
VkFormatProperties scratchProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, scratchFormat, &scratchProperties);
|
||||
const VkFormatFeatureFlags scratchRequired = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
|
||||
if ((scratchProperties.optimalTilingFeatures & scratchRequired) != scratchRequired) return false;
|
||||
|
||||
auto scratch = MakeUnique<VkTextureManager::TextureResource>();
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = scratchFormat;
|
||||
info.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
info.mipLevels = info.arrayLayers = 1;
|
||||
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
VmaAllocationCreateInfo allocation{};
|
||||
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
allocation.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
if (vmaCreateImage(m_allocator, &info, &allocation, &scratch->image, &scratch->allocation,
|
||||
nullptr) != VK_SUCCESS)
|
||||
return false;
|
||||
scratch->format = scratchFormat;
|
||||
scratch->extent = {info.extent.width, info.extent.height};
|
||||
scratch->mipLevels = scratch->arrayLayers = scratch->depth = 1;
|
||||
scratch->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
WireImage staged{};
|
||||
staged.image = scratch->image;
|
||||
staged.format = scratchFormat;
|
||||
staged.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
staged.extent = scratch->extent;
|
||||
staged.trackedLayout = &scratch->layout;
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
TransitionWireImage(staged, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
VkImageCopy copy{};
|
||||
// A 3D source carries its slice on z and always names layer 0; every other source
|
||||
// carries it on the layer axis, which is the same split ResolveWireImage records.
|
||||
copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, source.level, source.is3D ? 0u : source.layer, 1};
|
||||
copy.srcOffset = {std::min(sx0, sx1), std::min(sy0, sy1),
|
||||
source.is3D ? static_cast<Int32>(source.layer) : 0};
|
||||
copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.dstOffset = {0, 0, 0};
|
||||
copy.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
vkCmdCopyImage(m_frameContext.GetCurrent().commandBuffer, source.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staged.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
staged.layout = scratch->layout;
|
||||
// The scratch holds the region at its own origin, so a reversed source rectangle
|
||||
// becomes a reversed rectangle OVER THE SCRATCH - the multisample resolve arm's shape.
|
||||
const GLint rx0 = sx1 > sx0 ? 0 : static_cast<GLint>(width);
|
||||
const GLint rx1 = sx1 > sx0 ? static_cast<GLint>(width) : 0;
|
||||
const GLint ry0 = sy1 > sy0 ? 0 : static_cast<GLint>(height);
|
||||
const GLint ry1 = sy1 > sy0 ? static_cast<GLint>(height) : 0;
|
||||
const Bool blitted = BlitWireColorImage(staged, destination, rx0, ry0, rx1, ry1,
|
||||
dx0, dy0, dx1, dy1, filter, false);
|
||||
// Retired whether or not the blit took: the copy above is already recorded against this
|
||||
// image, so the submission that consumes it owns the storage either way.
|
||||
WireRetiredObjects retired{};
|
||||
retired.textures.push_back(Move(scratch));
|
||||
RetireWireObjects(Move(retired));
|
||||
return blitted;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BlitWireColorImage(WireImage source, WireImage destination,
|
||||
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
||||
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap) {
|
||||
const Bool sameImage = source.image == destination.image;
|
||||
if (source.isDefault || (sameImage && source.level == destination.level && source.layer == destination.layer) ||
|
||||
source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT ||
|
||||
source.is3D || destination.is3D || (source.viewType != VK_IMAGE_VIEW_TYPE_2D &&
|
||||
source.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
|
||||
(destination.viewType != VK_IMAGE_VIEW_TYPE_2D && destination.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
|
||||
!vkuFormatIsSampledFloat(source.format)) return false;
|
||||
if (sx0 == sx1 || sy0 == sy1 || dx0 == dx1 || dy0 == dy1) return true;
|
||||
if (!source.extent.width || !source.extent.height ||
|
||||
!destination.extent.width || !destination.extent.height) return false;
|
||||
const auto transform = destination.isDefault ? m_swapchainObject.GetPreTransform() :
|
||||
VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR &&
|
||||
transform != VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) return false;
|
||||
VkFormatProperties sourceProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
|
||||
if ((sourceProperties.optimalTilingFeatures & required) != required) return false;
|
||||
// Mip generation samples and writes the texture's own color space;
|
||||
// GL_FRAMEBUFFER_SRGB only controls framebuffer operations.
|
||||
if (!mipmap) destination.format = ResolveSrgbAttachmentWriteFormat(destination.format,
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
VkFormatProperties destinationProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, &destinationProperties);
|
||||
if (!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) return false;
|
||||
const VkImageLayout sourceLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
const VkImageLayout destinationLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
|
||||
struct Constants {
|
||||
float srcRect[4];
|
||||
float dstRect[4];
|
||||
Int32 surfaceTransform;
|
||||
};
|
||||
static_assert(sizeof(Constants) == 36);
|
||||
if (!m_wireColorBlitResources) {
|
||||
m_wireColorBlitResources = new WireColorBlitResources;
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
|
||||
};
|
||||
shader(kWireColorBlitVertexSpirv, sizeof(kWireColorBlitVertexSpirv), cache.vertex);
|
||||
shader(kWireColorBlitFragmentSpirv, sizeof(kWireColorBlitFragmentSpirv), cache.fragment);
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(Constants)};
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
|
||||
pipelineLayout.pushConstantRangeCount = 1;
|
||||
pipelineLayout.pPushConstantRanges = &push;
|
||||
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_LINEAR;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.linear));
|
||||
}
|
||||
auto& cache = *m_wireColorBlitResources;
|
||||
WireColorBlitResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets) {
|
||||
if (entry.format == destination.format && entry.layout == destinationLayout) { target = &entry; break; }
|
||||
}
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = destination.format;
|
||||
target->layout = destinationLayout;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = destination.format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = attachment.finalLayout = destinationLayout;
|
||||
const VkAttachmentReference color{0, destinationLayout};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &color;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1;
|
||||
pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1;
|
||||
pass.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = cache.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = cache.fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipelineColorBlendAttachmentState colorMask{};
|
||||
colorMask.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
||||
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.attachmentCount = 1;
|
||||
blend.pAttachments = &colorMask;
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
pipeline.stageCount = 2;
|
||||
pipeline.pStages = stages;
|
||||
pipeline.pVertexInputState = &vertex;
|
||||
pipeline.pInputAssemblyState = &assembly;
|
||||
pipeline.pViewportState = &viewport;
|
||||
pipeline.pRasterizationState = &raster;
|
||||
pipeline.pMultisampleState = &samples;
|
||||
pipeline.pColorBlendState = &blend;
|
||||
pipeline.pDynamicState = &dynamic;
|
||||
pipeline.layout = cache.pipelineLayout;
|
||||
pipeline.renderPass = target->renderPass;
|
||||
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, &target->pipeline));
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
// Distinct mip/layer views of the same image are legal shader input and
|
||||
// output. GENERAL keeps the whole-image layout tracker truthful; the
|
||||
// transition also orders the previous mip's attachment write.
|
||||
TransitionWireImage(source, sourceLayout);
|
||||
if (sameImage) destination.layout = source.layout;
|
||||
else TransitionWireImage(destination, destinationLayout);
|
||||
const auto makeView = [&](const WireImage& image) {
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image;
|
||||
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
view.format = image.format;
|
||||
// A layered framebuffer blit reads its first layer, as the native path does.
|
||||
view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, image.level, 1, image.layer, 1};
|
||||
VkImageView handle = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
|
||||
return handle;
|
||||
};
|
||||
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &destinationView;
|
||||
framebufferInfo.width = destination.extent.width;
|
||||
framebufferInfo.height = destination.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
// Each in-flight blit owns its descriptor set. Updating a cached set
|
||||
// would rewrite the source view of an older queued blit.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
// GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the
|
||||
// four transforms) is a copy and samples NEAREST, which is exact where linear filtering
|
||||
// at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still
|
||||
// asks for SAMPLED_IMAGE_FILTER_LINEAR whenever GL said GL_LINEAR, so which blits reach
|
||||
// this pass is unchanged.
|
||||
const VkFilter samplerFilter = WireColorBlitFilterFor({sx0, sy0, sx1, sy1}, {dx0, dy0, dx1, dy1},
|
||||
filter, transform);
|
||||
const VkDescriptorImageInfo sampled{samplerFilter == VK_FILTER_LINEAR ? cache.linear : cache.nearest,
|
||||
sourceView, sourceLayout};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
const float sw = static_cast<float>(source.extent.width), sh = static_cast<float>(source.extent.height);
|
||||
const Bool quarterTurn = transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
|
||||
const float dw = static_cast<float>(quarterTurn ? destination.extent.height : destination.extent.width);
|
||||
const float dh = static_cast<float>(quarterTurn ? destination.extent.width : destination.extent.height);
|
||||
// The shared vertex shader flips window Y. Offscreen images keep the
|
||||
// source row order, so undo that flip in the destination rectangle.
|
||||
if (!destination.isDefault) {
|
||||
dy0 = static_cast<GLint>(destination.extent.height) - dy0;
|
||||
dy1 = static_cast<GLint>(destination.extent.height) - dy1;
|
||||
}
|
||||
const Constants constants{{sx0 / sw, sy0 / sh, (static_cast<float>(sx1) - sx0) / sw,
|
||||
(static_cast<float>(sy1) - sy0) / sh},
|
||||
{dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw,
|
||||
(static_cast<float>(dy1) - dy0) / dh},
|
||||
static_cast<Int32>(ToBlitSurfaceTransform(transform))};
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
static_cast<float>(destination.extent.height), 0.0f, 1.0f};
|
||||
const VkRect2D scissor{{0, 0}, destination.extent};
|
||||
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
|
||||
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
||||
vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, sizeof(constants), &constants);
|
||||
vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0);
|
||||
vkCmdEndRenderPass(frame.commandBuffer);
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the
|
||||
// application's descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, destinationView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan blit: no GL program, registry entry, or runtime transpilation.
|
||||
layout(push_constant) uniform BlitRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
} rect;
|
||||
layout(location = 0) out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
// A quad, rather than an oversized triangle, also clips partial blits to
|
||||
// their destination rectangle. The Vulkan viewport covers the native image.
|
||||
const vec2 corners[6] = vec2[](
|
||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
||||
vec2 uv = corners[gl_VertexIndex];
|
||||
vec2 p = (rect.dstRect.xy + uv * rect.dstRect.zw) * 2.0 - 1.0;
|
||||
// Same GL-bottom-left to native-image mapping as the monolithic blit.
|
||||
p.y = -p.y;
|
||||
if (rect.surfaceTransform == 1) p = vec2(-p.y, p.x);
|
||||
else if (rect.surfaceTransform == 2) p = -p;
|
||||
else if (rect.surfaceTransform == 3) p = vec2(p.y, -p.x);
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
texCoord = rect.srcRect.xy + uv * rect.srcRect.zw;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlitFilter.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
// P7 g3-blit (g3det, gate 3): WHICH SAMPLER THE WIRE ARM'S SHADER COLOUR BLIT READS ITS SOURCE
|
||||
// THROUGH.
|
||||
//
|
||||
// GL applies a blit's `filter` only where the image is STRETCHED (GL 4.6 core 18.3.1). A blit whose
|
||||
// source and destination rectangles are the same size is a copy, whatever `filter` says, and so is
|
||||
// a mirrored one. The wire arm's shader blit (WireColorBlit.inc, BlitWireColorImage) draws a quad
|
||||
// and samples `textureLod(sourceImage, texCoord, 0)` at interpolated coordinates, and it used the
|
||||
// LINEAR sampler for every GL_LINEAR blit. At 1:1 every fragment centre maps to a texel centre, so
|
||||
// in exact arithmetic linear filtering reads the same single texel. Adreno 830 quantises the
|
||||
// coordinate to sub-texel precision: a fragment that lands a fraction off the centre blends in a
|
||||
// small weight of a neighbour. minecraft-1.21.1-neoforge-create-instancing-in-world ends with a
|
||||
// 1:1 GL_LINEAR glBlitFramebuffer into FB 0, and on the wire arm 712 high-contrast pixels moved by
|
||||
// 1 LSB toward a neighbour. The monolith's vkCmdBlitImage copied them exactly
|
||||
// (~/w7/logs/g3det/VERDICT.md section 3). NEAREST within half a texel of the centre is exact, so an
|
||||
// unscaled blit takes NEAREST. Lavapipe samples texel centres exactly, so no host picture can see
|
||||
// the difference; this pure half is what the unit test pins
|
||||
// (MG_Test/Pipeline/PipelineQuirkTest.cpp, WireColorBlitSamples*).
|
||||
//
|
||||
// THE SURFACE TRANSFORM DOES NOT TURN A COPY INTO A STRETCH. BlitWireColorImage applies the
|
||||
// quarter and half turns in NDC, over a viewport of the native image, and for a quarter turn it
|
||||
// takes the logical extent as the native extent swapped. A native pixel then has exactly the pitch
|
||||
// of a logical one, and the rotation maps pixel centres onto pixel centres. So the scale test is on
|
||||
// GL's own (logical) rectangles for identity and all three rotations. BlitWireColorImage declines
|
||||
// any other transform before it chooses a sampler; for one, this answers with the filter's own
|
||||
// mapping, as before this package.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A glBlitFramebuffer rectangle as GL names it: two corners, either order.
|
||||
struct WireBlitRect {
|
||||
GLint x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||
};
|
||||
|
||||
// Same extent on both axes, orientation aside. The extents are taken in 64 bits: GL accepts any
|
||||
// GLint corner, and INT_MAX - INT_MIN does not fit in one.
|
||||
inline Bool WireBlitIsUnscaled(const WireBlitRect& source, const WireBlitRect& destination) {
|
||||
const auto extent = [](GLint a, GLint b) {
|
||||
return std::abs(static_cast<Int64>(b) - static_cast<Int64>(a));
|
||||
};
|
||||
return extent(source.x0, source.x1) == extent(destination.x0, destination.x1) &&
|
||||
extent(source.y0, source.y1) == extent(destination.y0, destination.y1);
|
||||
}
|
||||
|
||||
inline VkFilter WireColorBlitFilterFor(const WireBlitRect& source, const WireBlitRect& destination,
|
||||
GLenum filter, VkSurfaceTransformFlagBitsKHR transform) {
|
||||
if (filter != GL_LINEAR) return VK_FILTER_NEAREST;
|
||||
const Bool expressed = transform == VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR ||
|
||||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
|
||||
if (expressed && WireBlitIsUnscaled(source, destination)) return VK_FILTER_NEAREST;
|
||||
return VK_FILTER_LINEAR;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,132 +0,0 @@
|
||||
// Generated from WireColorBlit.vert / WireColorBlit.frag (Vulkan 1.0 / SPIR-V 1.0) by
|
||||
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the
|
||||
// IN-TREE glslang and splices the words it emits into the arrays below. Do not edit the words
|
||||
// by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles every
|
||||
// source in its table and reds on the first word that disagrees.
|
||||
//
|
||||
// These words were hand-run through glslangValidator before that gate existed, and the gate
|
||||
// reproduces them exactly - which is the one thing nobody could have known beforehand.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireColorBlitVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000070u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0008000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000016u, 0x0000005du, 0x00000066u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00030005u,
|
||||
0x00000009u, 0x00007675u, 0x00060005u, 0x00000016u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x00000019u, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030005u, 0x0000001cu, 0x00000070u,
|
||||
0x00050005u, 0x0000001eu, 0x74696c42u, 0x74636552u, 0x00000000u, 0x00050006u, 0x0000001eu, 0x00000000u,
|
||||
0x52637273u, 0x00746365u, 0x00050006u, 0x0000001eu, 0x00000001u, 0x52747364u, 0x00746365u, 0x00080006u,
|
||||
0x0000001eu, 0x00000002u, 0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u, 0x00040005u,
|
||||
0x00000020u, 0x74636572u, 0x00000000u, 0x00060005u, 0x0000005bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||
0x00000000u, 0x00060006u, 0x0000005bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u,
|
||||
0x0000005bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000005bu,
|
||||
0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000005bu, 0x00000003u,
|
||||
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000005du, 0x00000000u, 0x00050005u,
|
||||
0x00000066u, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000016u, 0x0000000bu, 0x0000002au,
|
||||
0x00030047u, 0x0000001eu, 0x00000002u, 0x00050048u, 0x0000001eu, 0x00000000u, 0x00000023u, 0x00000000u,
|
||||
0x00050048u, 0x0000001eu, 0x00000001u, 0x00000023u, 0x00000010u, 0x00050048u, 0x0000001eu, 0x00000002u,
|
||||
0x00000023u, 0x00000020u, 0x00030047u, 0x0000005bu, 0x00000002u, 0x00050048u, 0x0000005bu, 0x00000000u,
|
||||
0x0000000bu, 0x00000000u, 0x00050048u, 0x0000005bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u,
|
||||
0x0000005bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000005bu, 0x00000003u, 0x0000000bu,
|
||||
0x00000004u, 0x00040047u, 0x00000066u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u,
|
||||
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
|
||||
0x00000002u, 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x00040015u, 0x0000000au, 0x00000020u,
|
||||
0x00000000u, 0x0004002bu, 0x0000000au, 0x0000000bu, 0x00000006u, 0x0004001cu, 0x0000000cu, 0x00000007u,
|
||||
0x0000000bu, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x0005002cu, 0x00000007u, 0x0000000eu,
|
||||
0x0000000du, 0x0000000du, 0x0004002bu, 0x00000006u, 0x0000000fu, 0x3f800000u, 0x0005002cu, 0x00000007u,
|
||||
0x00000010u, 0x0000000fu, 0x0000000du, 0x0005002cu, 0x00000007u, 0x00000011u, 0x0000000du, 0x0000000fu,
|
||||
0x0005002cu, 0x00000007u, 0x00000012u, 0x0000000fu, 0x0000000fu, 0x0009002cu, 0x0000000cu, 0x00000013u,
|
||||
0x0000000eu, 0x00000010u, 0x00000011u, 0x00000011u, 0x00000010u, 0x00000012u, 0x00040015u, 0x00000014u,
|
||||
0x00000020u, 0x00000001u, 0x00040020u, 0x00000015u, 0x00000001u, 0x00000014u, 0x0004003bu, 0x00000015u,
|
||||
0x00000016u, 0x00000001u, 0x00040020u, 0x00000018u, 0x00000007u, 0x0000000cu, 0x00040017u, 0x0000001du,
|
||||
0x00000006u, 0x00000004u, 0x0005001eu, 0x0000001eu, 0x0000001du, 0x0000001du, 0x00000014u, 0x00040020u,
|
||||
0x0000001fu, 0x00000009u, 0x0000001eu, 0x0004003bu, 0x0000001fu, 0x00000020u, 0x00000009u, 0x0004002bu,
|
||||
0x00000014u, 0x00000021u, 0x00000001u, 0x00040020u, 0x00000022u, 0x00000009u, 0x0000001du, 0x0004002bu,
|
||||
0x00000006u, 0x0000002cu, 0x40000000u, 0x0004002bu, 0x0000000au, 0x00000030u, 0x00000001u, 0x00040020u,
|
||||
0x00000031u, 0x00000007u, 0x00000006u, 0x0004002bu, 0x00000014u, 0x00000036u, 0x00000002u, 0x00040020u,
|
||||
0x00000037u, 0x00000009u, 0x00000014u, 0x00020014u, 0x0000003au, 0x0004002bu, 0x0000000au, 0x00000041u,
|
||||
0x00000000u, 0x0004002bu, 0x00000014u, 0x00000050u, 0x00000003u, 0x0004001cu, 0x0000005au, 0x00000006u,
|
||||
0x00000030u, 0x0006001eu, 0x0000005bu, 0x0000001du, 0x00000006u, 0x0000005au, 0x0000005au, 0x00040020u,
|
||||
0x0000005cu, 0x00000003u, 0x0000005bu, 0x0004003bu, 0x0000005cu, 0x0000005du, 0x00000003u, 0x0004002bu,
|
||||
0x00000014u, 0x0000005eu, 0x00000000u, 0x00040020u, 0x00000063u, 0x00000003u, 0x0000001du, 0x00040020u,
|
||||
0x00000065u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000065u, 0x00000066u, 0x00000003u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
|
||||
0x00000009u, 0x00000007u, 0x0004003bu, 0x00000018u, 0x00000019u, 0x00000007u, 0x0004003bu, 0x00000008u,
|
||||
0x0000001cu, 0x00000007u, 0x0004003du, 0x00000014u, 0x00000017u, 0x00000016u, 0x0003003eu, 0x00000019u,
|
||||
0x00000013u, 0x00050041u, 0x00000008u, 0x0000001au, 0x00000019u, 0x00000017u, 0x0004003du, 0x00000007u,
|
||||
0x0000001bu, 0x0000001au, 0x0003003eu, 0x00000009u, 0x0000001bu, 0x00050041u, 0x00000022u, 0x00000023u,
|
||||
0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du, 0x00000024u, 0x00000023u, 0x0007004fu, 0x00000007u,
|
||||
0x00000025u, 0x00000024u, 0x00000024u, 0x00000000u, 0x00000001u, 0x0004003du, 0x00000007u, 0x00000026u,
|
||||
0x00000009u, 0x00050041u, 0x00000022u, 0x00000027u, 0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du,
|
||||
0x00000028u, 0x00000027u, 0x0007004fu, 0x00000007u, 0x00000029u, 0x00000028u, 0x00000028u, 0x00000002u,
|
||||
0x00000003u, 0x00050085u, 0x00000007u, 0x0000002au, 0x00000026u, 0x00000029u, 0x00050081u, 0x00000007u,
|
||||
0x0000002bu, 0x00000025u, 0x0000002au, 0x0005008eu, 0x00000007u, 0x0000002du, 0x0000002bu, 0x0000002cu,
|
||||
0x00050050u, 0x00000007u, 0x0000002eu, 0x0000000fu, 0x0000000fu, 0x00050083u, 0x00000007u, 0x0000002fu,
|
||||
0x0000002du, 0x0000002eu, 0x0003003eu, 0x0000001cu, 0x0000002fu, 0x00050041u, 0x00000031u, 0x00000032u,
|
||||
0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000033u, 0x00000032u, 0x0004007fu, 0x00000006u,
|
||||
0x00000034u, 0x00000033u, 0x00050041u, 0x00000031u, 0x00000035u, 0x0000001cu, 0x00000030u, 0x0003003eu,
|
||||
0x00000035u, 0x00000034u, 0x00050041u, 0x00000037u, 0x00000038u, 0x00000020u, 0x00000036u, 0x0004003du,
|
||||
0x00000014u, 0x00000039u, 0x00000038u, 0x000500aau, 0x0000003au, 0x0000003bu, 0x00000039u, 0x00000021u,
|
||||
0x000300f7u, 0x0000003du, 0x00000000u, 0x000400fau, 0x0000003bu, 0x0000003cu, 0x00000045u, 0x000200f8u,
|
||||
0x0000003cu, 0x00050041u, 0x00000031u, 0x0000003eu, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u,
|
||||
0x0000003fu, 0x0000003eu, 0x0004007fu, 0x00000006u, 0x00000040u, 0x0000003fu, 0x00050041u, 0x00000031u,
|
||||
0x00000042u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u, 0x00000043u, 0x00000042u, 0x00050050u,
|
||||
0x00000007u, 0x00000044u, 0x00000040u, 0x00000043u, 0x0003003eu, 0x0000001cu, 0x00000044u, 0x000200f9u,
|
||||
0x0000003du, 0x000200f8u, 0x00000045u, 0x00050041u, 0x00000037u, 0x00000046u, 0x00000020u, 0x00000036u,
|
||||
0x0004003du, 0x00000014u, 0x00000047u, 0x00000046u, 0x000500aau, 0x0000003au, 0x00000048u, 0x00000047u,
|
||||
0x00000036u, 0x000300f7u, 0x0000004au, 0x00000000u, 0x000400fau, 0x00000048u, 0x00000049u, 0x0000004du,
|
||||
0x000200f8u, 0x00000049u, 0x0004003du, 0x00000007u, 0x0000004bu, 0x0000001cu, 0x0004007fu, 0x00000007u,
|
||||
0x0000004cu, 0x0000004bu, 0x0003003eu, 0x0000001cu, 0x0000004cu, 0x000200f9u, 0x0000004au, 0x000200f8u,
|
||||
0x0000004du, 0x00050041u, 0x00000037u, 0x0000004eu, 0x00000020u, 0x00000036u, 0x0004003du, 0x00000014u,
|
||||
0x0000004fu, 0x0000004eu, 0x000500aau, 0x0000003au, 0x00000051u, 0x0000004fu, 0x00000050u, 0x000300f7u,
|
||||
0x00000053u, 0x00000000u, 0x000400fau, 0x00000051u, 0x00000052u, 0x00000053u, 0x000200f8u, 0x00000052u,
|
||||
0x00050041u, 0x00000031u, 0x00000054u, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000055u,
|
||||
0x00000054u, 0x00050041u, 0x00000031u, 0x00000056u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u,
|
||||
0x00000057u, 0x00000056u, 0x0004007fu, 0x00000006u, 0x00000058u, 0x00000057u, 0x00050050u, 0x00000007u,
|
||||
0x00000059u, 0x00000055u, 0x00000058u, 0x0003003eu, 0x0000001cu, 0x00000059u, 0x000200f9u, 0x00000053u,
|
||||
0x000200f8u, 0x00000053u, 0x000200f9u, 0x0000004au, 0x000200f8u, 0x0000004au, 0x000200f9u, 0x0000003du,
|
||||
0x000200f8u, 0x0000003du, 0x0004003du, 0x00000007u, 0x0000005fu, 0x0000001cu, 0x00050051u, 0x00000006u,
|
||||
0x00000060u, 0x0000005fu, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000061u, 0x0000005fu, 0x00000001u,
|
||||
0x00070050u, 0x0000001du, 0x00000062u, 0x00000060u, 0x00000061u, 0x0000000du, 0x0000000fu, 0x00050041u,
|
||||
0x00000063u, 0x00000064u, 0x0000005du, 0x0000005eu, 0x0003003eu, 0x00000064u, 0x00000062u, 0x00050041u,
|
||||
0x00000022u, 0x00000067u, 0x00000020u, 0x0000005eu, 0x0004003du, 0x0000001du, 0x00000068u, 0x00000067u,
|
||||
0x0007004fu, 0x00000007u, 0x00000069u, 0x00000068u, 0x00000068u, 0x00000000u, 0x00000001u, 0x0004003du,
|
||||
0x00000007u, 0x0000006au, 0x00000009u, 0x00050041u, 0x00000022u, 0x0000006bu, 0x00000020u, 0x0000005eu,
|
||||
0x0004003du, 0x0000001du, 0x0000006cu, 0x0000006bu, 0x0007004fu, 0x00000007u, 0x0000006du, 0x0000006cu,
|
||||
0x0000006cu, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000006eu, 0x0000006au, 0x0000006du,
|
||||
0x00050081u, 0x00000007u, 0x0000006fu, 0x00000069u, 0x0000006eu, 0x0003003eu, 0x00000066u, 0x0000006fu,
|
||||
0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireColorBlitFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000027u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000au, 0x0000001fu, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
|
||||
0x00000000u, 0x00050005u, 0x0000000au, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00050005u, 0x0000001fu,
|
||||
0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00050005u, 0x00000023u, 0x72756f73u, 0x6d496563u, 0x00656761u,
|
||||
0x00040047u, 0x0000000au, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000001fu, 0x0000001eu, 0x00000000u,
|
||||
0x00040047u, 0x00000023u, 0x00000021u, 0x00000000u, 0x00040047u, 0x00000023u, 0x00000022u, 0x00000000u,
|
||||
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00020014u, 0x00000006u, 0x00030016u,
|
||||
0x00000007u, 0x00000020u, 0x00040017u, 0x00000008u, 0x00000007u, 0x00000002u, 0x00040020u, 0x00000009u,
|
||||
0x00000001u, 0x00000008u, 0x0004003bu, 0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000007u,
|
||||
0x0000000cu, 0x00000000u, 0x0005002cu, 0x00000008u, 0x0000000du, 0x0000000cu, 0x0000000cu, 0x00040017u,
|
||||
0x0000000eu, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000007u, 0x00000015u, 0x3f800000u, 0x0005002cu,
|
||||
0x00000008u, 0x00000016u, 0x00000015u, 0x00000015u, 0x00040017u, 0x0000001du, 0x00000007u, 0x00000004u,
|
||||
0x00040020u, 0x0000001eu, 0x00000003u, 0x0000001du, 0x0004003bu, 0x0000001eu, 0x0000001fu, 0x00000003u,
|
||||
0x00090019u, 0x00000020u, 0x00000007u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u,
|
||||
0x00000000u, 0x0003001bu, 0x00000021u, 0x00000020u, 0x00040020u, 0x00000022u, 0x00000000u, 0x00000021u,
|
||||
0x0004003bu, 0x00000022u, 0x00000023u, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000008u, 0x0000000bu, 0x0000000au, 0x000500b8u,
|
||||
0x0000000eu, 0x0000000fu, 0x0000000bu, 0x0000000du, 0x0004009au, 0x00000006u, 0x00000010u, 0x0000000fu,
|
||||
0x000400a8u, 0x00000006u, 0x00000011u, 0x00000010u, 0x000300f7u, 0x00000013u, 0x00000000u, 0x000400fau,
|
||||
0x00000011u, 0x00000012u, 0x00000013u, 0x000200f8u, 0x00000012u, 0x0004003du, 0x00000008u, 0x00000014u,
|
||||
0x0000000au, 0x000500bau, 0x0000000eu, 0x00000017u, 0x00000014u, 0x00000016u, 0x0004009au, 0x00000006u,
|
||||
0x00000018u, 0x00000017u, 0x000200f9u, 0x00000013u, 0x000200f8u, 0x00000013u, 0x000700f5u, 0x00000006u,
|
||||
0x00000019u, 0x00000010u, 0x00000005u, 0x00000018u, 0x00000012u, 0x000300f7u, 0x0000001bu, 0x00000000u,
|
||||
0x000400fau, 0x00000019u, 0x0000001au, 0x0000001bu, 0x000200f8u, 0x0000001au, 0x000100fcu, 0x000200f8u,
|
||||
0x0000001bu, 0x0004003du, 0x00000021u, 0x00000024u, 0x00000023u, 0x0004003du, 0x00000008u, 0x00000025u,
|
||||
0x0000000au, 0x00070058u, 0x0000001du, 0x00000026u, 0x00000024u, 0x00000025u, 0x00000002u, 0x0000000cu,
|
||||
0x0003003eu, 0x0000001fu, 0x00000026u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
@@ -1,172 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// P7 wave 2 package B3: the per-site tally behind CONTRACT-P7 §0 rule I.
|
||||
//
|
||||
// Shape is PipeApplier.h's ServerVerbSink tallies, and for the same reason (R-16: a probe may
|
||||
// not arm against a stub). "The retrace passed" and "the retrace silently dropped nine draws"
|
||||
// produce the same exit code and the same log, so the lane has to assert the number that only
|
||||
// this path can move.
|
||||
//
|
||||
// Counters are process-wide and monotone. On the spawn arm they live in the SERVER process,
|
||||
// which is where the draw path runs - a client-side reader would always see zero, so the
|
||||
// two-process case asserts pixels and reads these from the server's own dump.
|
||||
enum class WireDeclineSite : Uint32 {
|
||||
#define MGL_WIRE_DECLINE(name) name,
|
||||
#include "WireDeclines.def"
|
||||
#undef MGL_WIRE_DECLINE
|
||||
Count
|
||||
};
|
||||
|
||||
inline const char* WireDeclineSiteName(WireDeclineSite site) {
|
||||
switch (site) {
|
||||
#define MGL_WIRE_DECLINE(name) \
|
||||
case WireDeclineSite::name: \
|
||||
return #name;
|
||||
#include "WireDeclines.def"
|
||||
#undef MGL_WIRE_DECLINE
|
||||
case WireDeclineSite::Count:
|
||||
break;
|
||||
}
|
||||
return "Unnamed";
|
||||
}
|
||||
|
||||
class WireDeclineTally {
|
||||
public:
|
||||
static constexpr SizeT kSiteCount = static_cast<SizeT>(WireDeclineSite::Count);
|
||||
|
||||
static void Count(WireDeclineSite site) {
|
||||
Slots()[static_cast<SizeT>(site)].fetch_add(1, std::memory_order_relaxed);
|
||||
Total().fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static Uint64 Get(WireDeclineSite site) {
|
||||
return Slots()[static_cast<SizeT>(site)].load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static Uint64 TotalDeclines() { return Total().load(std::memory_order_relaxed); }
|
||||
|
||||
// The memory half (package M1's thread): a refused upload does not consume its
|
||||
// PendingUpload entry, so the entry and the level shadow behind it stay alive. These are
|
||||
// GAUGES, not counters - the last observed live depth, sampled where the refusal happens -
|
||||
// so a monotone climb across frames is the signature that a refusal is also a leak.
|
||||
static void SetPendingGauge(Uint64 entries, Uint64 bytes) {
|
||||
PendingEntries().store(entries, std::memory_order_relaxed);
|
||||
PendingBytes().store(bytes, std::memory_order_relaxed);
|
||||
Uint64 peak = PeakPendingBytes().load(std::memory_order_relaxed);
|
||||
while (bytes > peak && !PeakPendingBytes().compare_exchange_weak(peak, bytes, std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
// The frame-serial floor's own counter. An "unsound serial complete" is an advance of
|
||||
// VkBufferManager's completed-frame-serial floor to a serial that ANOTHER submission still
|
||||
// in flight also carries - i.e. the renderer telling the buffer manager "frame N-1 is done"
|
||||
// while the submission holding frame N-1's buffer copies is still executing. It is what
|
||||
// lets a streamed glBufferSubData take the unordered host path and tear one draw's vertex
|
||||
// range. With a provable floor it is unreachable, so this counter is the red-once's reading:
|
||||
// non-zero on the wire arms before the fix, zero after, zero on monolith either way.
|
||||
static void CountUnsoundSerialComplete() {
|
||||
UnsoundSerial().fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
static Uint64 UnsoundSerialCompleteEvents() { return UnsoundSerial().load(std::memory_order_relaxed); }
|
||||
|
||||
static Uint64 PendingUploadEntries() { return PendingEntries().load(std::memory_order_relaxed); }
|
||||
static Uint64 PendingUploadBytes() { return PendingBytes().load(std::memory_order_relaxed); }
|
||||
static Uint64 PeakPendingUploadBytes() { return PeakPendingBytes().load(std::memory_order_relaxed); }
|
||||
|
||||
static void Reset() {
|
||||
for (SizeT i = 0; i < kSiteCount; ++i) Slots()[i].store(0, std::memory_order_relaxed);
|
||||
Total().store(0, std::memory_order_relaxed);
|
||||
UnsoundSerial().store(0, std::memory_order_relaxed);
|
||||
PendingEntries().store(0, std::memory_order_relaxed);
|
||||
PendingBytes().store(0, std::memory_order_relaxed);
|
||||
PeakPendingBytes().store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// One line per site that moved, plus the pending gauges. Called at session teardown and
|
||||
// from the forced-condition harness; silent when nothing declined, so a green lane stays
|
||||
// quiet and a lane that dropped a draw cannot.
|
||||
static void Dump(const char* tag) {
|
||||
// The floor counter prints unconditionally: "zero unsound advances" is the reading the
|
||||
// red-once needs, and an absent line cannot be told from a lane that never ran.
|
||||
MGLOG_I("MGWIRE-FLOOR[%s] unsoundSerialComplete=%llu", tag,
|
||||
static_cast<unsigned long long>(UnsoundSerialCompleteEvents()));
|
||||
if (TotalDeclines() == 0 && PeakPendingUploadBytes() == 0) return;
|
||||
MGLOG_I("MGWIRE-DECLINES[%s] total=%llu pendingEntries=%llu pendingBytes=%llu peakPendingBytes=%llu", tag,
|
||||
static_cast<unsigned long long>(TotalDeclines()),
|
||||
static_cast<unsigned long long>(PendingUploadEntries()),
|
||||
static_cast<unsigned long long>(PendingUploadBytes()),
|
||||
static_cast<unsigned long long>(PeakPendingUploadBytes()));
|
||||
for (SizeT i = 0; i < kSiteCount; ++i) {
|
||||
const Uint64 value = Slots()[i].load(std::memory_order_relaxed);
|
||||
if (!value) continue;
|
||||
MGLOG_I("MGWIRE-DECLINES[%s] %s=%llu", tag, WireDeclineSiteName(static_cast<WireDeclineSite>(i)),
|
||||
static_cast<unsigned long long>(value));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static std::atomic<Uint64>* Slots() {
|
||||
static std::atomic<Uint64> slots[kSiteCount]{};
|
||||
return slots;
|
||||
}
|
||||
static std::atomic<Uint64>& Total() {
|
||||
static std::atomic<Uint64> total{0};
|
||||
return total;
|
||||
}
|
||||
static std::atomic<Uint64>& PendingEntries() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& PendingBytes() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& PeakPendingBytes() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
static std::atomic<Uint64>& UnsoundSerial() {
|
||||
static std::atomic<Uint64> value{0};
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Every decline goes through this: the tally moves ALWAYS, the log line is once per site.
|
||||
// `MGL_WIRE_DECLINE_AT(Site, "why", ...); return false;` is the statement pair that replaces
|
||||
// a bare `return false;` (the few sites that call WireDeclineTally::Count directly stand
|
||||
// under an MGLOG_W/E of their own; scripts/ci/wire_declines_audit.py holds them to that).
|
||||
//
|
||||
// A DECLINE IS NOT ALWAYS A SKIPPED DRAW, so the line does not say "draw". The Tex*, Shape* and
|
||||
// Upload* rows are counted per texture SYNC: SyncTextureResourceByHandle is reached from the
|
||||
// draw/dispatch texture preparation, but also from the descriptor resolver, framebuffer
|
||||
// attachments, CopyTexSubImage, mipmap generation and CopyImageSubData - each of which Fatals
|
||||
// right after a failed sync - and from texture readback, which falls back to the unbacked
|
||||
// shadow. A draw that samples three unsyncable textures counts three; a failed attachment
|
||||
// sync counts once and is then a Fatal, not a skipped draw.
|
||||
#define MGL_WIRE_DECLINE_AT(site, fmt, ...) \
|
||||
do { \
|
||||
::MobileGL::MG_Backend::DirectVulkan::WireDeclineTally::Count( \
|
||||
::MobileGL::MG_Backend::DirectVulkan::WireDeclineSite::site); \
|
||||
MGLOG_E_ONCE("Magma wire decline [" #site "]: " fmt, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -1,106 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDeclines.def
|
||||
// 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
|
||||
|
||||
// P7 wave 2 package B3. THE NAMES OF EVERY WAY THE MAGMA WIRE ARM CAN DECLINE.
|
||||
//
|
||||
// A DECLINE IS NOT ALWAYS A SKIPPED DRAW. The SetupWireDraw / DispatchWireCompute rows are one
|
||||
// per refused draw or dispatch; the Tex* / Shape* / Upload* rows are counted per texture SYNC,
|
||||
// which the draw path reaches once per sampled texture and the descriptor resolver, framebuffer
|
||||
// attachments, CopyTexSubImage, mipmap generation, CopyImageSubData and texture readback reach
|
||||
// on their own (WireDeclineTally.h, above MGL_WIRE_DECLINE_AT).
|
||||
//
|
||||
// CONTRACT-P7 §0 rule I says a refusal on the wire arm is either a named decline or a named
|
||||
// Fatal. Before B3 the draw path had a third shape the rule does not admit: a bare
|
||||
// `return false` with no log, no counter and no census entry - roughly twenty of them in
|
||||
// SetupWireDraw alone, plus the texture-sync chain behind PrepareWireTextureResources. A draw
|
||||
// that took one of those exits left NO trace anywhere, which is precisely why the OpenRA
|
||||
// device divergence survived four device windows with client and server logs that were
|
||||
// LINE-IDENTICAL between a passing and a failing run.
|
||||
//
|
||||
// Each row below is one such exit. The name is what the decline logs and what the tally
|
||||
// counts, so "which exit did this frame take" is answerable from a counter dump instead of
|
||||
// from a bisect. Rows are append-only: the integration lane asserts specific counters moved.
|
||||
//
|
||||
// EVERY ROW HAS A SITE AND EVERY SITE HAS A LINE. A row with no site is a name that can never
|
||||
// be counted - it reads as "this cannot happen here" when it only means "nobody wired it up" -
|
||||
// and a site with no line puts the reader back on a bisect. Sites reached through a macro
|
||||
// (MGL_WIRE_DECLINE_AT) get both at once; the few that use WireDeclineTally::Count directly do
|
||||
// so ONLY because the statement above them already carries the same reason as an _ONCE log.
|
||||
// scripts/ci/wire_declines_audit.py holds both halves.
|
||||
//
|
||||
// MGL_WIRE_DECLINE(Name)
|
||||
|
||||
// ---- SetupWireDraw, in statement order -------------------------------------------------
|
||||
MGL_WIRE_DECLINE(ProgramLinkOrSpirv)
|
||||
MGL_WIRE_DECLINE(PointSizeCapability)
|
||||
MGL_WIRE_DECLINE(TextureResourcesUnprepared)
|
||||
MGL_WIRE_DECLINE(VertexFormatUnsupported)
|
||||
MGL_WIRE_DECLINE(VertexAttributeWindow)
|
||||
MGL_WIRE_DECLINE(VertexStreamCountZero)
|
||||
MGL_WIRE_DECLINE(VertexStreamRead)
|
||||
MGL_WIRE_DECLINE(VertexStreamConverter)
|
||||
MGL_WIRE_DECLINE(VertexTransientUpload)
|
||||
MGL_WIRE_DECLINE(IndexViewMissing)
|
||||
MGL_WIRE_DECLINE(IndexTypeWidth)
|
||||
MGL_WIRE_DECLINE(IndexWindow)
|
||||
MGL_WIRE_DECLINE(IndexStreamRead)
|
||||
MGL_WIRE_DECLINE(IndexTransientUpload)
|
||||
MGL_WIRE_DECLINE(IndexSliceInvalid)
|
||||
MGL_WIRE_DECLINE(UniformBufferBinding)
|
||||
MGL_WIRE_DECLINE(NoAttachments)
|
||||
MGL_WIRE_DECLINE(PipelineUnavailable)
|
||||
MGL_WIRE_DECLINE(CurrentAttributePayload)
|
||||
MGL_WIRE_DECLINE(CurrentAttributeUpload)
|
||||
MGL_WIRE_DECLINE(BeginRenderPass)
|
||||
|
||||
// ---- DispatchWireCompute ----------------------------------------------------------------
|
||||
MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv)
|
||||
MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared)
|
||||
MGL_WIRE_DECLINE(ComputePipelineUnavailable)
|
||||
MGL_WIRE_DECLINE(ComputeUniformBufferBinding)
|
||||
|
||||
// ---- UniformManager::PrepareWireTextureResources ----------------------------------------
|
||||
MGL_WIRE_DECLINE(DescriptorDeclined)
|
||||
MGL_WIRE_DECLINE(SamplerLocationAlias)
|
||||
MGL_WIRE_DECLINE(SamplerUnitRange)
|
||||
|
||||
// ---- VkTextureManager::SyncTextureResourceByHandle --------------------------------------
|
||||
MGL_WIRE_DECLINE(TexResolveStorage)
|
||||
MGL_WIRE_DECLINE(TexViewFormat)
|
||||
MGL_WIRE_DECLINE(TexNoApplierRecord)
|
||||
MGL_WIRE_DECLINE(TexRecordDeadOrStale)
|
||||
MGL_WIRE_DECLINE(TexShapeSync)
|
||||
MGL_WIRE_DECLINE(TexUploadPendingLevels)
|
||||
MGL_WIRE_DECLINE(TexStorageUsage)
|
||||
|
||||
// ---- VkTextureManager::UploadPendingWireLevels ------------------------------------------
|
||||
// The five `incomplete`/false exits the investigation named, plus the two that leave an
|
||||
// entry pending WITHOUT failing (a `continue`) - those are the ones that can starve a draw
|
||||
// of its texels while still reporting success, so they are counted separately.
|
||||
MGL_WIRE_DECLINE(UploadLevelBeyondChain)
|
||||
MGL_WIRE_DECLINE(UploadLevelExtentMismatch)
|
||||
MGL_WIRE_DECLINE(UploadEmptyLevel)
|
||||
MGL_WIRE_DECLINE(UploadConversion)
|
||||
MGL_WIRE_DECLINE(UploadTexelArithmetic)
|
||||
MGL_WIRE_DECLINE(UploadRegionOutOfBounds)
|
||||
MGL_WIRE_DECLINE(UploadIncomplete)
|
||||
MGL_WIRE_DECLINE(UploadFlushFailed)
|
||||
|
||||
// ---- VkTextureManager::SyncWireTextureShape ---------------------------------------------
|
||||
MGL_WIRE_DECLINE(ShapeBufferBacked)
|
||||
MGL_WIRE_DECLINE(ShapeViewOfStorage)
|
||||
MGL_WIRE_DECLINE(ShapeNoLevels)
|
||||
MGL_WIRE_DECLINE(ShapeNoVulkanShape)
|
||||
MGL_WIRE_DECLINE(ShapeNoVkFormat)
|
||||
MGL_WIRE_DECLINE(ShapeSampleCount)
|
||||
MGL_WIRE_DECLINE(ShapePreserveFlushFailed)
|
||||
MGL_WIRE_DECLINE(ShapeImageFlagsUnsupported)
|
||||
MGL_WIRE_DECLINE(ShapeCreateImageFailed)
|
||||
// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in
|
||||
// SyncWireTextureShape still leaving no trace at all.
|
||||
MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
|
||||
@@ -1,28 +0,0 @@
|
||||
#version 450
|
||||
// The monolith arm's kDepthMipmapFragmentShaderSource (VulkanRenderer.cpp), ported to the
|
||||
// baked pipeline: the same 2x2 box of texelFetches, the same clamp, the same average, the
|
||||
// same gl_FragDepth. It is a PORT and not a rewrite on purpose - CONTRACT-P7 §3.2 retires a
|
||||
// refusal by making the wire arm observably the monolith's, so the two arms have to agree
|
||||
// texel for texel, including where they read.
|
||||
//
|
||||
// uSrcTexelSize arrives on the push constant rather than in a uniform block: the wire arm
|
||||
// owns no GL program, so there is no default-block uniform to write and no global UBO to
|
||||
// bind. That is the whole of OQ-13's narrow half for this program.
|
||||
layout(push_constant) uniform DepthMipmapRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
ivec2 srcTexelSize;
|
||||
} rect;
|
||||
layout(set = 0, binding = 0) uniform sampler2D sourceImage;
|
||||
layout(location = 0) in vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
ivec2 srcBase = ivec2(texCoord * vec2(rect.srcTexelSize));
|
||||
ivec2 srcMax = rect.srcTexelSize - ivec2(1);
|
||||
float depth0 = texelFetch(sourceImage, clamp(srcBase, ivec2(0), srcMax), 0).r;
|
||||
float depth1 = texelFetch(sourceImage, clamp(srcBase + ivec2(1, 0), ivec2(0), srcMax), 0).r;
|
||||
float depth2 = texelFetch(sourceImage, clamp(srcBase + ivec2(0, 1), ivec2(0), srcMax), 0).r;
|
||||
float depth3 = texelFetch(sourceImage, clamp(srcBase + ivec2(1, 1), ivec2(0), srcMax), 0).r;
|
||||
gl_FragDepth = 0.25 * (depth0 + depth1 + depth2 + depth3);
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireColorBlit.inc.
|
||||
// The native depth mip owns no frontend object and never invokes the GL shader compiler.
|
||||
//
|
||||
// WHY THIS FILE EXISTS AT ALL. vkCmdBlitImage is the ordinary way to halve a mip, and for a
|
||||
// depth format it is often not available: VK_FORMAT_FEATURE_BLIT_DST is optional for depth,
|
||||
// and the monolith arm has carried a shader fallback for exactly that case since before the
|
||||
// split (VulkanRenderer.cpp's GenerateDepthMipmapWithShader). The wire arm had no such
|
||||
// fallback, so every depth mip the device could not blit died as
|
||||
// `Magma:mipmap-shader-format-or-shape`, and a DEPTH|STENCIL texture died one guard earlier
|
||||
// as `Magma:depth-stencil-mipmap@P7` whether or not it could be blitted.
|
||||
//
|
||||
// The monolith fallback is not reusable here and that is the whole of CONTRACT-P7 §5.1 (A):
|
||||
// it builds a GL ProgramObject, two GL ShaderObjects and a GL sampler, binds through the
|
||||
// uniform manager's global UBO, and writes uSrcTexelSize as a default-block uniform - four
|
||||
// frontend objects and a GL compiler on the server's apply thread. The baked program below is
|
||||
// the same shader with the same arithmetic, driven by a push constant and its own descriptor
|
||||
// set, which is what lets the server generate a depth mip while owning nothing of the client.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "WireDepthMipmapSpirv.h"
|
||||
|
||||
struct VulkanRenderer::WireDepthMipmapResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireDepthMipmapResources() {
|
||||
if (!m_wireDepthMipmapResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
|
||||
// descriptor pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
vkDestroySampler(m_device, cache.nearest, nullptr);
|
||||
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
|
||||
vkDestroyShaderModule(m_device, cache.fragment, nullptr);
|
||||
delete m_wireDepthMipmapResources;
|
||||
m_wireDepthMipmapResources = nullptr;
|
||||
}
|
||||
|
||||
// ONE LEVEL, source -> destination, both depth subresources of the same image. The shape
|
||||
// mirrors BlitWireColorImage deliberately: the same lazily built module pair, the same
|
||||
// per-format render pass/pipeline cache, the same per-submit descriptor pool, and the
|
||||
// same GENERAL-on-both-sides layout rule for a same-image pass, so the two native passes
|
||||
// cannot disagree about how an image is handed between them.
|
||||
Bool VulkanRenderer::GenerateWireDepthMipLevel(WireImage source, WireImage destination) {
|
||||
const Bool sameImage = source.image == destination.image;
|
||||
if (!source.image || !destination.image) return false;
|
||||
if (sameImage && source.level == destination.level && source.layer == destination.layer)
|
||||
return false;
|
||||
if (source.isDefault || destination.isDefault) return false;
|
||||
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
|
||||
return false;
|
||||
if (source.is3D || destination.is3D) return false;
|
||||
if (source.format != destination.format) return false;
|
||||
// DEPTH ONLY. A combined depth/stencil image can be sampled for its depth and
|
||||
// rendered as a depth attachment, but this pass writes gl_FragDepth and NOTHING
|
||||
// else - the stencil aspect would come out of the render pass undefined, which is
|
||||
// not what GL's glGenerateMipmap promises for the level it just produced. The
|
||||
// caller declines that shape rather than producing half a level.
|
||||
if (source.aspect != VK_IMAGE_ASPECT_DEPTH_BIT ||
|
||||
destination.aspect != VK_IMAGE_ASPECT_DEPTH_BIT) return false;
|
||||
if (!source.extent.width || !source.extent.height ||
|
||||
!destination.extent.width || !destination.extent.height) return false;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
if ((properties.optimalTilingFeatures & required) != required) return false;
|
||||
|
||||
// The baked block's layout, and the offsets are not a guess: the generated header
|
||||
// decorates its members at 0 / 16 / 32 / 40, which is where a vec4, a vec4, an int
|
||||
// and an 8-byte-aligned ivec2 land. The pad is what makes the C++ agree.
|
||||
struct Constants {
|
||||
float srcRect[4];
|
||||
float dstRect[4];
|
||||
Int32 surfaceTransform;
|
||||
Int32 reservedForIvec2Alignment;
|
||||
Int32 srcTexelSize[2];
|
||||
};
|
||||
static_assert(sizeof(Constants) == 48);
|
||||
static_assert(offsetof(Constants, srcTexelSize) == 40);
|
||||
|
||||
if (!m_wireDepthMipmapResources) {
|
||||
m_wireDepthMipmapResources = new WireDepthMipmapResources;
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
|
||||
};
|
||||
shader(kWireDepthMipmapVertexSpirv, sizeof(kWireDepthMipmapVertexSpirv), cache.vertex);
|
||||
shader(kWireDepthMipmapFragmentSpirv, sizeof(kWireDepthMipmapFragmentSpirv), cache.fragment);
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
|
||||
// BOTH STAGES ON ONE RANGE: the block is declared identically in the vertex and
|
||||
// fragment sources (a push-constant block must be), the vertex stage reads the
|
||||
// rects and the fragment stage reads srcTexelSize.
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(Constants)};
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
|
||||
pipelineLayout.pushConstantRangeCount = 1;
|
||||
pipelineLayout.pPushConstantRanges = &push;
|
||||
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
|
||||
// NEAREST, and it is not a default: the fragment shader does its own 2x2 box with
|
||||
// texelFetch, so any filtering the sampler added would be applied twice. It also
|
||||
// keeps the sampler legal on a device that does not advertise linear filtering for
|
||||
// depth formats, which is most of them.
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW =
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
|
||||
}
|
||||
auto& cache = *m_wireDepthMipmapResources;
|
||||
|
||||
const VkImageLayout sourceLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
const VkImageLayout destinationLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL
|
||||
: VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
WireDepthMipmapResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets) {
|
||||
if (entry.format == destination.format && entry.layout == destinationLayout) {
|
||||
target = &entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = destination.format;
|
||||
target->layout = destinationLayout;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = destination.format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// DONT_CARE, not LOAD: the pass writes gl_FragDepth at every texel of the level
|
||||
// it is producing, so there is nothing in the destination worth loading, and a
|
||||
// level that has just been grown has no defined contents to load anyway.
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = attachment.finalLayout = destinationLayout;
|
||||
const VkAttachmentReference depth{0, destinationLayout};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &depth;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1;
|
||||
pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1;
|
||||
pass.pSubpasses = &subpass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = cache.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = cache.fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// ALWAYS + write enabled: the fragment shader's gl_FragDepth is the answer, and a
|
||||
// comparison against whatever DONT_CARE left behind would discard some of it.
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = VK_TRUE;
|
||||
depthStencil.depthWriteEnable = VK_TRUE;
|
||||
depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||
depthStencil.minDepthBounds = 0.0f;
|
||||
depthStencil.maxDepthBounds = 1.0f;
|
||||
// No colour attachment in this render pass, so no attachment states either.
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
pipeline.stageCount = 2;
|
||||
pipeline.pStages = stages;
|
||||
pipeline.pVertexInputState = &vertex;
|
||||
pipeline.pInputAssemblyState = &assembly;
|
||||
pipeline.pViewportState = &viewport;
|
||||
pipeline.pRasterizationState = &raster;
|
||||
pipeline.pMultisampleState = &samples;
|
||||
pipeline.pDepthStencilState = &depthStencil;
|
||||
pipeline.pColorBlendState = &blend;
|
||||
pipeline.pDynamicState = &dynamic;
|
||||
pipeline.layout = cache.pipelineLayout;
|
||||
pipeline.renderPass = target->renderPass;
|
||||
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, &target->pipeline));
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
// Distinct mip views of the same image are legal shader input and output at once.
|
||||
// GENERAL keeps the whole-image layout tracker truthful; the transition also orders
|
||||
// the previous level's attachment write against this level's sample of it.
|
||||
TransitionWireImage(source, sourceLayout);
|
||||
if (sameImage) destination.layout = source.layout;
|
||||
else TransitionWireImage(destination, destinationLayout);
|
||||
const auto makeView = [&](const WireImage& image) {
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image;
|
||||
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
view.format = image.format;
|
||||
view.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, image.level, 1, image.layer, 1};
|
||||
VkImageView handle = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
|
||||
return handle;
|
||||
};
|
||||
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &destinationView;
|
||||
framebufferInfo.width = destination.extent.width;
|
||||
framebufferInfo.height = destination.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
// Each in-flight pass owns its descriptor set, for the colour blit's reason: updating
|
||||
// a cached set would rewrite the source view of an older queued pass.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
const VkDescriptorImageInfo sampled{cache.nearest, sourceView, sourceLayout};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
// The destination rectangle is flipped exactly as BlitWireColorImage flips it for an
|
||||
// offscreen target, because it shares that file's vertex shader and therefore its
|
||||
// window-Y flip. With the flip undone, destination row r samples texCoord.y
|
||||
// (r + 0.5) / dstHeight, and the fragment's ivec2(texCoord * srcTexelSize) lands on
|
||||
// the same source texels the monolith arm's fullscreen triangle lands on - which is
|
||||
// the point: §3.2 retires a refusal by making the wire arm observably the monolith's,
|
||||
// half-texel offset included.
|
||||
const Constants constants{
|
||||
{0.0f, 0.0f, 1.0f, 1.0f},
|
||||
{0.0f, 1.0f, 1.0f, -1.0f},
|
||||
0, 0,
|
||||
{static_cast<Int32>(source.extent.width), static_cast<Int32>(source.extent.height)}};
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
static_cast<float>(destination.extent.height), 0.0f, 1.0f};
|
||||
const VkRect2D scissor{{0, 0}, destination.extent};
|
||||
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
|
||||
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
|
||||
vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(constants), &constants);
|
||||
vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0);
|
||||
vkCmdEndRenderPass(frame.commandBuffer);
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the application's
|
||||
// descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, destinationView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,33 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan depth mip generation: no GL program, registry entry, or runtime transpilation.
|
||||
//
|
||||
// The geometry is WireColorBlit.vert's, unchanged - the same quad, the same push-constant
|
||||
// rect, the same GL-bottom-left to native-image mapping - so a depth mip and a colour blit
|
||||
// place their fragments identically and only the fragment stage differs. The block carries
|
||||
// one member more than the colour blit's, and the fragment stage is the member's only
|
||||
// reader: a push-constant block must be declared identically in every stage of one pipeline,
|
||||
// so it is declared here too rather than split into a second range.
|
||||
layout(push_constant) uniform DepthMipmapRect {
|
||||
vec4 srcRect;
|
||||
vec4 dstRect;
|
||||
int surfaceTransform;
|
||||
ivec2 srcTexelSize;
|
||||
} rect;
|
||||
layout(location = 0) out vec2 texCoord;
|
||||
|
||||
void main() {
|
||||
// A quad, rather than an oversized triangle, also clips partial blits to
|
||||
// their destination rectangle. The Vulkan viewport covers the native image.
|
||||
const vec2 corners[6] = vec2[](
|
||||
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
|
||||
vec2 uv = corners[gl_VertexIndex];
|
||||
vec2 p = (rect.dstRect.xy + uv * rect.dstRect.zw) * 2.0 - 1.0;
|
||||
// Same GL-bottom-left to native-image mapping as the monolithic blit.
|
||||
p.y = -p.y;
|
||||
if (rect.surfaceTransform == 1) p = vec2(-p.y, p.x);
|
||||
else if (rect.surfaceTransform == 2) p = -p;
|
||||
else if (rect.surfaceTransform == 3) p = vec2(p.y, -p.x);
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
texCoord = rect.srcRect.xy + uv * rect.srcRect.zw;
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Generated from WireDepthMipmap.vert / WireDepthMipmap.frag by
|
||||
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the
|
||||
// IN-TREE glslang and splices the words it emits into the arrays below. Do not edit the
|
||||
// words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles
|
||||
// every source in its table and reds on the first word that disagrees.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireDepthMipmapVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000071u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0008000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000016u, 0x0000005eu, 0x00000067u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00030005u,
|
||||
0x00000009u, 0x00007675u, 0x00060005u, 0x00000016u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x00000019u, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030005u, 0x0000001cu, 0x00000070u,
|
||||
0x00060005u, 0x0000001fu, 0x74706544u, 0x70694d68u, 0x5270616du, 0x00746365u, 0x00050006u, 0x0000001fu,
|
||||
0x00000000u, 0x52637273u, 0x00746365u, 0x00050006u, 0x0000001fu, 0x00000001u, 0x52747364u, 0x00746365u,
|
||||
0x00080006u, 0x0000001fu, 0x00000002u, 0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u,
|
||||
0x00070006u, 0x0000001fu, 0x00000003u, 0x54637273u, 0x6c657865u, 0x657a6953u, 0x00000000u, 0x00040005u,
|
||||
0x00000021u, 0x74636572u, 0x00000000u, 0x00060005u, 0x0000005cu, 0x505f6c67u, 0x65567265u, 0x78657472u,
|
||||
0x00000000u, 0x00060006u, 0x0000005cu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u,
|
||||
0x0000005cu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000005cu,
|
||||
0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000005cu, 0x00000003u,
|
||||
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000005eu, 0x00000000u, 0x00050005u,
|
||||
0x00000067u, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000016u, 0x0000000bu, 0x0000002au,
|
||||
0x00030047u, 0x0000001fu, 0x00000002u, 0x00050048u, 0x0000001fu, 0x00000000u, 0x00000023u, 0x00000000u,
|
||||
0x00050048u, 0x0000001fu, 0x00000001u, 0x00000023u, 0x00000010u, 0x00050048u, 0x0000001fu, 0x00000002u,
|
||||
0x00000023u, 0x00000020u, 0x00050048u, 0x0000001fu, 0x00000003u, 0x00000023u, 0x00000028u, 0x00030047u,
|
||||
0x0000005cu, 0x00000002u, 0x00050048u, 0x0000005cu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000005cu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000005cu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000005cu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000067u,
|
||||
0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u,
|
||||
0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000002u, 0x00040020u, 0x00000008u,
|
||||
0x00000007u, 0x00000007u, 0x00040015u, 0x0000000au, 0x00000020u, 0x00000000u, 0x0004002bu, 0x0000000au,
|
||||
0x0000000bu, 0x00000006u, 0x0004001cu, 0x0000000cu, 0x00000007u, 0x0000000bu, 0x0004002bu, 0x00000006u,
|
||||
0x0000000du, 0x00000000u, 0x0005002cu, 0x00000007u, 0x0000000eu, 0x0000000du, 0x0000000du, 0x0004002bu,
|
||||
0x00000006u, 0x0000000fu, 0x3f800000u, 0x0005002cu, 0x00000007u, 0x00000010u, 0x0000000fu, 0x0000000du,
|
||||
0x0005002cu, 0x00000007u, 0x00000011u, 0x0000000du, 0x0000000fu, 0x0005002cu, 0x00000007u, 0x00000012u,
|
||||
0x0000000fu, 0x0000000fu, 0x0009002cu, 0x0000000cu, 0x00000013u, 0x0000000eu, 0x00000010u, 0x00000011u,
|
||||
0x00000011u, 0x00000010u, 0x00000012u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040020u,
|
||||
0x00000015u, 0x00000001u, 0x00000014u, 0x0004003bu, 0x00000015u, 0x00000016u, 0x00000001u, 0x00040020u,
|
||||
0x00000018u, 0x00000007u, 0x0000000cu, 0x00040017u, 0x0000001du, 0x00000006u, 0x00000004u, 0x00040017u,
|
||||
0x0000001eu, 0x00000014u, 0x00000002u, 0x0006001eu, 0x0000001fu, 0x0000001du, 0x0000001du, 0x00000014u,
|
||||
0x0000001eu, 0x00040020u, 0x00000020u, 0x00000009u, 0x0000001fu, 0x0004003bu, 0x00000020u, 0x00000021u,
|
||||
0x00000009u, 0x0004002bu, 0x00000014u, 0x00000022u, 0x00000001u, 0x00040020u, 0x00000023u, 0x00000009u,
|
||||
0x0000001du, 0x0004002bu, 0x00000006u, 0x0000002du, 0x40000000u, 0x0004002bu, 0x0000000au, 0x00000031u,
|
||||
0x00000001u, 0x00040020u, 0x00000032u, 0x00000007u, 0x00000006u, 0x0004002bu, 0x00000014u, 0x00000037u,
|
||||
0x00000002u, 0x00040020u, 0x00000038u, 0x00000009u, 0x00000014u, 0x00020014u, 0x0000003bu, 0x0004002bu,
|
||||
0x0000000au, 0x00000042u, 0x00000000u, 0x0004002bu, 0x00000014u, 0x00000051u, 0x00000003u, 0x0004001cu,
|
||||
0x0000005bu, 0x00000006u, 0x00000031u, 0x0006001eu, 0x0000005cu, 0x0000001du, 0x00000006u, 0x0000005bu,
|
||||
0x0000005bu, 0x00040020u, 0x0000005du, 0x00000003u, 0x0000005cu, 0x0004003bu, 0x0000005du, 0x0000005eu,
|
||||
0x00000003u, 0x0004002bu, 0x00000014u, 0x0000005fu, 0x00000000u, 0x00040020u, 0x00000064u, 0x00000003u,
|
||||
0x0000001du, 0x00040020u, 0x00000066u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000066u, 0x00000067u,
|
||||
0x00000003u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u,
|
||||
0x0004003bu, 0x00000008u, 0x00000009u, 0x00000007u, 0x0004003bu, 0x00000018u, 0x00000019u, 0x00000007u,
|
||||
0x0004003bu, 0x00000008u, 0x0000001cu, 0x00000007u, 0x0004003du, 0x00000014u, 0x00000017u, 0x00000016u,
|
||||
0x0003003eu, 0x00000019u, 0x00000013u, 0x00050041u, 0x00000008u, 0x0000001au, 0x00000019u, 0x00000017u,
|
||||
0x0004003du, 0x00000007u, 0x0000001bu, 0x0000001au, 0x0003003eu, 0x00000009u, 0x0000001bu, 0x00050041u,
|
||||
0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x0004003du, 0x0000001du, 0x00000025u, 0x00000024u,
|
||||
0x0007004fu, 0x00000007u, 0x00000026u, 0x00000025u, 0x00000025u, 0x00000000u, 0x00000001u, 0x0004003du,
|
||||
0x00000007u, 0x00000027u, 0x00000009u, 0x00050041u, 0x00000023u, 0x00000028u, 0x00000021u, 0x00000022u,
|
||||
0x0004003du, 0x0000001du, 0x00000029u, 0x00000028u, 0x0007004fu, 0x00000007u, 0x0000002au, 0x00000029u,
|
||||
0x00000029u, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000002bu, 0x00000027u, 0x0000002au,
|
||||
0x00050081u, 0x00000007u, 0x0000002cu, 0x00000026u, 0x0000002bu, 0x0005008eu, 0x00000007u, 0x0000002eu,
|
||||
0x0000002cu, 0x0000002du, 0x00050050u, 0x00000007u, 0x0000002fu, 0x0000000fu, 0x0000000fu, 0x00050083u,
|
||||
0x00000007u, 0x00000030u, 0x0000002eu, 0x0000002fu, 0x0003003eu, 0x0000001cu, 0x00000030u, 0x00050041u,
|
||||
0x00000032u, 0x00000033u, 0x0000001cu, 0x00000031u, 0x0004003du, 0x00000006u, 0x00000034u, 0x00000033u,
|
||||
0x0004007fu, 0x00000006u, 0x00000035u, 0x00000034u, 0x00050041u, 0x00000032u, 0x00000036u, 0x0000001cu,
|
||||
0x00000031u, 0x0003003eu, 0x00000036u, 0x00000035u, 0x00050041u, 0x00000038u, 0x00000039u, 0x00000021u,
|
||||
0x00000037u, 0x0004003du, 0x00000014u, 0x0000003au, 0x00000039u, 0x000500aau, 0x0000003bu, 0x0000003cu,
|
||||
0x0000003au, 0x00000022u, 0x000300f7u, 0x0000003eu, 0x00000000u, 0x000400fau, 0x0000003cu, 0x0000003du,
|
||||
0x00000046u, 0x000200f8u, 0x0000003du, 0x00050041u, 0x00000032u, 0x0000003fu, 0x0000001cu, 0x00000031u,
|
||||
0x0004003du, 0x00000006u, 0x00000040u, 0x0000003fu, 0x0004007fu, 0x00000006u, 0x00000041u, 0x00000040u,
|
||||
0x00050041u, 0x00000032u, 0x00000043u, 0x0000001cu, 0x00000042u, 0x0004003du, 0x00000006u, 0x00000044u,
|
||||
0x00000043u, 0x00050050u, 0x00000007u, 0x00000045u, 0x00000041u, 0x00000044u, 0x0003003eu, 0x0000001cu,
|
||||
0x00000045u, 0x000200f9u, 0x0000003eu, 0x000200f8u, 0x00000046u, 0x00050041u, 0x00000038u, 0x00000047u,
|
||||
0x00000021u, 0x00000037u, 0x0004003du, 0x00000014u, 0x00000048u, 0x00000047u, 0x000500aau, 0x0000003bu,
|
||||
0x00000049u, 0x00000048u, 0x00000037u, 0x000300f7u, 0x0000004bu, 0x00000000u, 0x000400fau, 0x00000049u,
|
||||
0x0000004au, 0x0000004eu, 0x000200f8u, 0x0000004au, 0x0004003du, 0x00000007u, 0x0000004cu, 0x0000001cu,
|
||||
0x0004007fu, 0x00000007u, 0x0000004du, 0x0000004cu, 0x0003003eu, 0x0000001cu, 0x0000004du, 0x000200f9u,
|
||||
0x0000004bu, 0x000200f8u, 0x0000004eu, 0x00050041u, 0x00000038u, 0x0000004fu, 0x00000021u, 0x00000037u,
|
||||
0x0004003du, 0x00000014u, 0x00000050u, 0x0000004fu, 0x000500aau, 0x0000003bu, 0x00000052u, 0x00000050u,
|
||||
0x00000051u, 0x000300f7u, 0x00000054u, 0x00000000u, 0x000400fau, 0x00000052u, 0x00000053u, 0x00000054u,
|
||||
0x000200f8u, 0x00000053u, 0x00050041u, 0x00000032u, 0x00000055u, 0x0000001cu, 0x00000031u, 0x0004003du,
|
||||
0x00000006u, 0x00000056u, 0x00000055u, 0x00050041u, 0x00000032u, 0x00000057u, 0x0000001cu, 0x00000042u,
|
||||
0x0004003du, 0x00000006u, 0x00000058u, 0x00000057u, 0x0004007fu, 0x00000006u, 0x00000059u, 0x00000058u,
|
||||
0x00050050u, 0x00000007u, 0x0000005au, 0x00000056u, 0x00000059u, 0x0003003eu, 0x0000001cu, 0x0000005au,
|
||||
0x000200f9u, 0x00000054u, 0x000200f8u, 0x00000054u, 0x000200f9u, 0x0000004bu, 0x000200f8u, 0x0000004bu,
|
||||
0x000200f9u, 0x0000003eu, 0x000200f8u, 0x0000003eu, 0x0004003du, 0x00000007u, 0x00000060u, 0x0000001cu,
|
||||
0x00050051u, 0x00000006u, 0x00000061u, 0x00000060u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000062u,
|
||||
0x00000060u, 0x00000001u, 0x00070050u, 0x0000001du, 0x00000063u, 0x00000061u, 0x00000062u, 0x0000000du,
|
||||
0x0000000fu, 0x00050041u, 0x00000064u, 0x00000065u, 0x0000005eu, 0x0000005fu, 0x0003003eu, 0x00000065u,
|
||||
0x00000063u, 0x00050041u, 0x00000023u, 0x00000068u, 0x00000021u, 0x0000005fu, 0x0004003du, 0x0000001du,
|
||||
0x00000069u, 0x00000068u, 0x0007004fu, 0x00000007u, 0x0000006au, 0x00000069u, 0x00000069u, 0x00000000u,
|
||||
0x00000001u, 0x0004003du, 0x00000007u, 0x0000006bu, 0x00000009u, 0x00050041u, 0x00000023u, 0x0000006cu,
|
||||
0x00000021u, 0x0000005fu, 0x0004003du, 0x0000001du, 0x0000006du, 0x0000006cu, 0x0007004fu, 0x00000007u,
|
||||
0x0000006eu, 0x0000006du, 0x0000006du, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000006fu,
|
||||
0x0000006bu, 0x0000006eu, 0x00050081u, 0x00000007u, 0x00000070u, 0x0000006au, 0x0000006fu, 0x0003003eu,
|
||||
0x00000067u, 0x00000070u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireDepthMipmapFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000059u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000004fu, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u,
|
||||
0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00040005u, 0x00000009u, 0x42637273u, 0x00657361u,
|
||||
0x00050005u, 0x0000000du, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00060005u, 0x00000010u, 0x74706544u,
|
||||
0x70694d68u, 0x5270616du, 0x00746365u, 0x00050006u, 0x00000010u, 0x00000000u, 0x52637273u, 0x00746365u,
|
||||
0x00050006u, 0x00000010u, 0x00000001u, 0x52747364u, 0x00746365u, 0x00080006u, 0x00000010u, 0x00000002u,
|
||||
0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u, 0x00070006u, 0x00000010u, 0x00000003u,
|
||||
0x54637273u, 0x6c657865u, 0x657a6953u, 0x00000000u, 0x00040005u, 0x00000012u, 0x74636572u, 0x00000000u,
|
||||
0x00040005u, 0x0000001au, 0x4d637273u, 0x00007861u, 0x00040005u, 0x00000021u, 0x74706564u, 0x00003068u,
|
||||
0x00050005u, 0x00000025u, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00040005u, 0x00000031u, 0x74706564u,
|
||||
0x00003168u, 0x00040005u, 0x0000003bu, 0x74706564u, 0x00003268u, 0x00040005u, 0x00000045u, 0x74706564u,
|
||||
0x00003368u, 0x00060005u, 0x0000004fu, 0x465f6c67u, 0x44676172u, 0x68747065u, 0x00000000u, 0x00040047u,
|
||||
0x0000000du, 0x0000001eu, 0x00000000u, 0x00030047u, 0x00000010u, 0x00000002u, 0x00050048u, 0x00000010u,
|
||||
0x00000000u, 0x00000023u, 0x00000000u, 0x00050048u, 0x00000010u, 0x00000001u, 0x00000023u, 0x00000010u,
|
||||
0x00050048u, 0x00000010u, 0x00000002u, 0x00000023u, 0x00000020u, 0x00050048u, 0x00000010u, 0x00000003u,
|
||||
0x00000023u, 0x00000028u, 0x00040047u, 0x00000025u, 0x00000021u, 0x00000000u, 0x00040047u, 0x00000025u,
|
||||
0x00000022u, 0x00000000u, 0x00040047u, 0x0000004fu, 0x0000000bu, 0x00000016u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040017u,
|
||||
0x00000007u, 0x00000006u, 0x00000002u, 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x00030016u,
|
||||
0x0000000au, 0x00000020u, 0x00040017u, 0x0000000bu, 0x0000000au, 0x00000002u, 0x00040020u, 0x0000000cu,
|
||||
0x00000001u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000001u, 0x00040017u, 0x0000000fu,
|
||||
0x0000000au, 0x00000004u, 0x0006001eu, 0x00000010u, 0x0000000fu, 0x0000000fu, 0x00000006u, 0x00000007u,
|
||||
0x00040020u, 0x00000011u, 0x00000009u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000009u,
|
||||
0x0004002bu, 0x00000006u, 0x00000013u, 0x00000003u, 0x00040020u, 0x00000014u, 0x00000009u, 0x00000007u,
|
||||
0x0004002bu, 0x00000006u, 0x0000001du, 0x00000001u, 0x0005002cu, 0x00000007u, 0x0000001eu, 0x0000001du,
|
||||
0x0000001du, 0x00040020u, 0x00000020u, 0x00000007u, 0x0000000au, 0x00090019u, 0x00000022u, 0x0000000au,
|
||||
0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x00000023u,
|
||||
0x00000022u, 0x00040020u, 0x00000024u, 0x00000000u, 0x00000023u, 0x0004003bu, 0x00000024u, 0x00000025u,
|
||||
0x00000000u, 0x0004002bu, 0x00000006u, 0x00000028u, 0x00000000u, 0x0005002cu, 0x00000007u, 0x00000029u,
|
||||
0x00000028u, 0x00000028u, 0x00040015u, 0x0000002eu, 0x00000020u, 0x00000000u, 0x0004002bu, 0x0000002eu,
|
||||
0x0000002fu, 0x00000000u, 0x0005002cu, 0x00000007u, 0x00000034u, 0x0000001du, 0x00000028u, 0x0005002cu,
|
||||
0x00000007u, 0x0000003eu, 0x00000028u, 0x0000001du, 0x00040020u, 0x0000004eu, 0x00000003u, 0x0000000au,
|
||||
0x0004003bu, 0x0000004eu, 0x0000004fu, 0x00000003u, 0x0004002bu, 0x0000000au, 0x00000050u, 0x3e800000u,
|
||||
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu,
|
||||
0x00000008u, 0x00000009u, 0x00000007u, 0x0004003bu, 0x00000008u, 0x0000001au, 0x00000007u, 0x0004003bu,
|
||||
0x00000020u, 0x00000021u, 0x00000007u, 0x0004003bu, 0x00000020u, 0x00000031u, 0x00000007u, 0x0004003bu,
|
||||
0x00000020u, 0x0000003bu, 0x00000007u, 0x0004003bu, 0x00000020u, 0x00000045u, 0x00000007u, 0x0004003du,
|
||||
0x0000000bu, 0x0000000eu, 0x0000000du, 0x00050041u, 0x00000014u, 0x00000015u, 0x00000012u, 0x00000013u,
|
||||
0x0004003du, 0x00000007u, 0x00000016u, 0x00000015u, 0x0004006fu, 0x0000000bu, 0x00000017u, 0x00000016u,
|
||||
0x00050085u, 0x0000000bu, 0x00000018u, 0x0000000eu, 0x00000017u, 0x0004006eu, 0x00000007u, 0x00000019u,
|
||||
0x00000018u, 0x0003003eu, 0x00000009u, 0x00000019u, 0x00050041u, 0x00000014u, 0x0000001bu, 0x00000012u,
|
||||
0x00000013u, 0x0004003du, 0x00000007u, 0x0000001cu, 0x0000001bu, 0x00050082u, 0x00000007u, 0x0000001fu,
|
||||
0x0000001cu, 0x0000001eu, 0x0003003eu, 0x0000001au, 0x0000001fu, 0x0004003du, 0x00000023u, 0x00000026u,
|
||||
0x00000025u, 0x0004003du, 0x00000007u, 0x00000027u, 0x00000009u, 0x0004003du, 0x00000007u, 0x0000002au,
|
||||
0x0000001au, 0x0008000cu, 0x00000007u, 0x0000002bu, 0x00000001u, 0x0000002du, 0x00000027u, 0x00000029u,
|
||||
0x0000002au, 0x00040064u, 0x00000022u, 0x0000002cu, 0x00000026u, 0x0007005fu, 0x0000000fu, 0x0000002du,
|
||||
0x0000002cu, 0x0000002bu, 0x00000002u, 0x00000028u, 0x00050051u, 0x0000000au, 0x00000030u, 0x0000002du,
|
||||
0x00000000u, 0x0003003eu, 0x00000021u, 0x00000030u, 0x0004003du, 0x00000023u, 0x00000032u, 0x00000025u,
|
||||
0x0004003du, 0x00000007u, 0x00000033u, 0x00000009u, 0x00050080u, 0x00000007u, 0x00000035u, 0x00000033u,
|
||||
0x00000034u, 0x0004003du, 0x00000007u, 0x00000036u, 0x0000001au, 0x0008000cu, 0x00000007u, 0x00000037u,
|
||||
0x00000001u, 0x0000002du, 0x00000035u, 0x00000029u, 0x00000036u, 0x00040064u, 0x00000022u, 0x00000038u,
|
||||
0x00000032u, 0x0007005fu, 0x0000000fu, 0x00000039u, 0x00000038u, 0x00000037u, 0x00000002u, 0x00000028u,
|
||||
0x00050051u, 0x0000000au, 0x0000003au, 0x00000039u, 0x00000000u, 0x0003003eu, 0x00000031u, 0x0000003au,
|
||||
0x0004003du, 0x00000023u, 0x0000003cu, 0x00000025u, 0x0004003du, 0x00000007u, 0x0000003du, 0x00000009u,
|
||||
0x00050080u, 0x00000007u, 0x0000003fu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000007u, 0x00000040u,
|
||||
0x0000001au, 0x0008000cu, 0x00000007u, 0x00000041u, 0x00000001u, 0x0000002du, 0x0000003fu, 0x00000029u,
|
||||
0x00000040u, 0x00040064u, 0x00000022u, 0x00000042u, 0x0000003cu, 0x0007005fu, 0x0000000fu, 0x00000043u,
|
||||
0x00000042u, 0x00000041u, 0x00000002u, 0x00000028u, 0x00050051u, 0x0000000au, 0x00000044u, 0x00000043u,
|
||||
0x00000000u, 0x0003003eu, 0x0000003bu, 0x00000044u, 0x0004003du, 0x00000023u, 0x00000046u, 0x00000025u,
|
||||
0x0004003du, 0x00000007u, 0x00000047u, 0x00000009u, 0x00050080u, 0x00000007u, 0x00000048u, 0x00000047u,
|
||||
0x0000001eu, 0x0004003du, 0x00000007u, 0x00000049u, 0x0000001au, 0x0008000cu, 0x00000007u, 0x0000004au,
|
||||
0x00000001u, 0x0000002du, 0x00000048u, 0x00000029u, 0x00000049u, 0x00040064u, 0x00000022u, 0x0000004bu,
|
||||
0x00000046u, 0x0007005fu, 0x0000000fu, 0x0000004cu, 0x0000004bu, 0x0000004au, 0x00000002u, 0x00000028u,
|
||||
0x00050051u, 0x0000000au, 0x0000004du, 0x0000004cu, 0x00000000u, 0x0003003eu, 0x00000045u, 0x0000004du,
|
||||
0x0004003du, 0x0000000au, 0x00000051u, 0x00000021u, 0x0004003du, 0x0000000au, 0x00000052u, 0x00000031u,
|
||||
0x00050081u, 0x0000000au, 0x00000053u, 0x00000051u, 0x00000052u, 0x0004003du, 0x0000000au, 0x00000054u,
|
||||
0x0000003bu, 0x00050081u, 0x0000000au, 0x00000055u, 0x00000053u, 0x00000054u, 0x0004003du, 0x0000000au,
|
||||
0x00000056u, 0x00000045u, 0x00050081u, 0x0000000au, 0x00000057u, 0x00000055u, 0x00000056u, 0x00050085u,
|
||||
0x0000000au, 0x00000058u, 0x00000050u, 0x00000057u, 0x0003003eu, 0x0000004fu, 0x00000058u, 0x000100fdu,
|
||||
0x00010038u,
|
||||
};
|
||||
@@ -1,329 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.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 <cstring>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
// P7 gate 5 (g5-msrbo, g5-msprobe): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST.
|
||||
//
|
||||
// The wire arm has two (WireFramebuffer.inc, ResolveWireDepthStencil): a render pass that carries
|
||||
// a VK_KHR_depth_stencil_resolve attachment and no draw, and the baked shader pass of
|
||||
// WireMultisampleResolve.inc, which draws. P7 wave 2-B2 took the render pass wherever the
|
||||
// extension exists. On the Redmi (Adreno 830, Vulkan 1.3.284, driver 512.800.71) that pass left
|
||||
// its resolve target unwritten - KHR-GL46.direct_state_access.renderbuffers_storage_multisample
|
||||
// read every resolved depth and stencil value back as 0 on the inproc arm while the monolith arm
|
||||
// passed, and the same case passed on the device with the shader arm forced
|
||||
// (MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1).
|
||||
//
|
||||
// A DRIVER THAT CLAIMS WHAT IT CANNOT DO IS A POST PROBE, NOT A VENDOR QUIRK (standing rule,
|
||||
// 2026-08-22). The first fix keyed the order on Qualcomm's vendor id; it is replaced by a probe
|
||||
// that reproduces the production resolve on the actual VkDevice (WireDepthResolveProbe.cpp) and
|
||||
// asks the one question the order depends on: does the no-draw render pass write the resolve
|
||||
// target? A driver that fixes the defect gets the fast arm back, and a driver of any vendor that
|
||||
// has it gets the shader arm - neither needs a table edit.
|
||||
//
|
||||
// EVERY BUG PROBE CARRIES A CONTROL (MG_Util/SelfTest/DriverBugProbes.h). The control is the
|
||||
// production SHADER arm fed the same inputs: when it does not resolve them either, the probe has
|
||||
// measured its own setup (or a device that cannot resolve depth at all) rather than the render
|
||||
// pass, the verdict is INCONCLUSIVE, and the default order stands.
|
||||
//
|
||||
// This header is the pure half - the measurement, the verdict, the test knob and the choice - so
|
||||
// a unit test pins every mapping with a fake result source (MG_Test/Pipeline/PipelineQuirkTest.cpp).
|
||||
// The Vulkan half that fills the measurement is WireDepthResolveProbe.{h,cpp}.
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// One aspect of one probed format, both arms. Values are the RAW aspect words the
|
||||
// buffer copy returns (D16: 16 bits; D24: the low 24 bits of the 32-bit word; D32F: the float's
|
||||
// bits; stencil: 8 bits), compared within `tolerance` (1 LSB for a UNORM depth, 0 otherwise).
|
||||
struct WireDepthResolveAspectReading {
|
||||
// The format has this aspect and the render-pass arm's copy of it was read back.
|
||||
Bool measured = false;
|
||||
Uint32 texels = 0;
|
||||
Uint32 expected = 0; // the multisample source's cleared value
|
||||
Uint32 sentinel = 0; // what the resolve target was pre-filled with
|
||||
Uint32 tolerance = 0;
|
||||
// THE SUBJECT: VK_KHR_depth_stencil_resolve render pass, no draw.
|
||||
Uint32 renderPassMatches = 0; // texels within tolerance of `expected`
|
||||
Uint32 renderPassSentinels = 0; // texels still holding the sentinel
|
||||
Uint32 renderPassFirst = 0; // texel (0,0)
|
||||
// THE CONTROL: the production shader pass, same inputs. False where it cannot run for this
|
||||
// aspect (a stencil aspect without VK_EXT_shader_stencil_export, a failed pipeline).
|
||||
Bool shaderRan = false;
|
||||
Uint32 shaderMatches = 0;
|
||||
Uint32 shaderFirst = 0;
|
||||
};
|
||||
|
||||
struct WireDepthResolveFormatReading {
|
||||
String name; // e.g. "D24_UNORM_S8_UINT"
|
||||
Int32 format = 0; // the VkFormat
|
||||
Uint32 samples = 0;
|
||||
// False when the format was not probed; `skipReason` says why (unsupported, the render-pass
|
||||
// arm would not be taken for it, an object the probe needed could not be created).
|
||||
Bool ran = false;
|
||||
String skipReason;
|
||||
WireDepthResolveAspectReading depth, stencil;
|
||||
};
|
||||
|
||||
struct WireDepthResolveProbeMeasurement {
|
||||
// At least one format was recorded, submitted and read back.
|
||||
Bool ran = false;
|
||||
// The bounded fence wait expired: every object the probe created was deliberately leaked
|
||||
// (the queue may still be executing it), and nothing was read back.
|
||||
Bool fenceWaitTimedOut = false;
|
||||
String failureReason;
|
||||
// Came from MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean, not from the device.
|
||||
Bool fromKnob = false;
|
||||
// Measured on the device, but with MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject: the probe
|
||||
// recorded no render-pass resolve at all, so the subject's target kept its sentinel.
|
||||
Bool subjectElided = false;
|
||||
Vector<WireDepthResolveFormatReading> formats;
|
||||
};
|
||||
|
||||
enum class WireDepthResolveProbeVerdict : Uint8 {
|
||||
// Nothing was measured: the render-pass arm is not available (nothing to choose between),
|
||||
// or the probe could not set itself up. The default order stands.
|
||||
NotRun,
|
||||
// Measured, but no format's CONTROL resolved: the probe cannot tell the render pass's
|
||||
// defect from its own. The default order stands, and the report must never call it a bug.
|
||||
Inconclusive,
|
||||
// Every format whose control resolved was resolved by the render pass too.
|
||||
Clean,
|
||||
// The defect: on at least one format the shader control resolved the inputs and the
|
||||
// render pass did not (wrote nothing - the sentinel survived - or wrote wrong values).
|
||||
RenderPassResolveBroken,
|
||||
};
|
||||
|
||||
inline const char* WireDepthResolveProbeVerdictName(WireDepthResolveProbeVerdict verdict) {
|
||||
switch (verdict) {
|
||||
case WireDepthResolveProbeVerdict::NotRun: return "not-run";
|
||||
case WireDepthResolveProbeVerdict::Inconclusive: return "inconclusive";
|
||||
case WireDepthResolveProbeVerdict::Clean: return "clean";
|
||||
case WireDepthResolveProbeVerdict::RenderPassResolveBroken: return "render-pass-resolve-broken";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
inline Bool WireDepthResolveControlHolds(const WireDepthResolveAspectReading& aspect) {
|
||||
return aspect.measured && aspect.shaderRan && aspect.texels > 0 && aspect.shaderMatches == aspect.texels;
|
||||
}
|
||||
|
||||
inline Bool WireDepthResolveSubjectHolds(const WireDepthResolveAspectReading& aspect) {
|
||||
return aspect.texels > 0 && aspect.renderPassMatches == aspect.texels;
|
||||
}
|
||||
|
||||
// An aspect only counts where its control holds; a format is conclusive when any of its
|
||||
// aspects is. The render pass resolves every aspect of the format in one pass, so one aspect
|
||||
// it fails is the defect for the whole arm.
|
||||
inline WireDepthResolveProbeVerdict EvaluateWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) {
|
||||
if (!measurement.ran || measurement.fenceWaitTimedOut) return WireDepthResolveProbeVerdict::NotRun;
|
||||
Bool conclusive = false, broken = false;
|
||||
for (const WireDepthResolveFormatReading& reading : measurement.formats) {
|
||||
if (!reading.ran) continue;
|
||||
for (const WireDepthResolveAspectReading* aspect : {&reading.depth, &reading.stencil}) {
|
||||
if (!WireDepthResolveControlHolds(*aspect)) continue;
|
||||
conclusive = true;
|
||||
if (!WireDepthResolveSubjectHolds(*aspect)) broken = true;
|
||||
}
|
||||
}
|
||||
if (broken) return WireDepthResolveProbeVerdict::RenderPassResolveBroken;
|
||||
return conclusive ? WireDepthResolveProbeVerdict::Clean : WireDepthResolveProbeVerdict::Inconclusive;
|
||||
}
|
||||
|
||||
// THE TEST KNOB, read by the SERVER (split and spawn arms only; the tcp server is a lane fixture
|
||||
// whose environment no entry reaches - spawn_lane_parity.py's MAGMA_SERVER_ENV_KNOB_NO_TCP).
|
||||
// Lavapipe resolves correctly through both arms, so no host lane can reach the verdict that
|
||||
// matters on the Redmi. MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=bug|clean replaces the MEASUREMENT
|
||||
// with a canned one that says exactly that, and nothing else: the canned measurement still goes
|
||||
// through EvaluateWireDepthResolveProbe and the same member, so an entry that forces `bug` is red
|
||||
// when the evaluation stops detecting the defect.
|
||||
//
|
||||
// A canned measurement cannot see the REAL probe - its recording, readback and tally - stop
|
||||
// detecting the defect, and on lavapipe the real subject is clean, so nothing would. The third
|
||||
// value, MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject, is that negative control: the REAL
|
||||
// probe runs (ChooseWireDepthResolveArm treats it as Measure) and the caller passes it on as
|
||||
// WireDepthResolveProbeContext::elideSubject, so the probe records no render-pass resolve and
|
||||
// the real readback keeps the sentinel while the shader control still runs. The
|
||||
// DirectVulkan.{Split,Spawn}.MsResolveElide. entries then require the defect verdict from the
|
||||
// measurement itself.
|
||||
enum class WireDepthResolveProbeKnob : Uint8 { Measure, ForceBug, ForceClean, ElideSubject, Unrecognised };
|
||||
|
||||
inline WireDepthResolveProbeKnob ParseWireDepthResolveProbeKnob(const char* value) {
|
||||
if (value == nullptr || *value == '\0') return WireDepthResolveProbeKnob::Measure;
|
||||
if (std::strcmp(value, "bug") == 0) return WireDepthResolveProbeKnob::ForceBug;
|
||||
if (std::strcmp(value, "clean") == 0) return WireDepthResolveProbeKnob::ForceClean;
|
||||
if (std::strcmp(value, "elide-subject") == 0) return WireDepthResolveProbeKnob::ElideSubject;
|
||||
return WireDepthResolveProbeKnob::Unrecognised;
|
||||
}
|
||||
|
||||
// One packed format's worth of readings: the control resolved both aspects; the render pass
|
||||
// either did too (`renderPassWrites`) or left the sentinel everywhere.
|
||||
inline WireDepthResolveProbeMeasurement CannedWireDepthResolveProbeMeasurement(Bool renderPassWrites) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
measurement.ran = true;
|
||||
measurement.fromKnob = true;
|
||||
WireDepthResolveFormatReading reading;
|
||||
reading.name = "MGITEST_MAGMA_DEPTH_RESOLVE_PROBE";
|
||||
reading.samples = 4;
|
||||
reading.ran = true;
|
||||
const auto fill = [&](WireDepthResolveAspectReading& aspect, Uint32 expected, Uint32 sentinel) {
|
||||
aspect.measured = true;
|
||||
aspect.texels = 16;
|
||||
aspect.expected = expected;
|
||||
aspect.sentinel = sentinel;
|
||||
aspect.renderPassMatches = renderPassWrites ? 16 : 0;
|
||||
aspect.renderPassSentinels = renderPassWrites ? 0 : 16;
|
||||
aspect.renderPassFirst = renderPassWrites ? expected : sentinel;
|
||||
aspect.shaderRan = true;
|
||||
aspect.shaderMatches = 16;
|
||||
aspect.shaderFirst = expected;
|
||||
};
|
||||
fill(reading.depth, 0x400000u, 0xBFFFFFu);
|
||||
fill(reading.stencil, 0x5Au, 0xA5u);
|
||||
measurement.formats.push_back(Move(reading));
|
||||
return measurement;
|
||||
}
|
||||
|
||||
struct WireDepthResolveArmChoice {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
WireDepthResolveProbeVerdict verdict = WireDepthResolveProbeVerdict::NotRun;
|
||||
// m_wirePreferShaderDepthResolve: the shader pass goes first, the render pass is the
|
||||
// fallback for what the shader cannot write.
|
||||
Bool preferShader = false;
|
||||
};
|
||||
|
||||
// The whole decision, with the Vulkan half behind `measure` (called at most once, and only when
|
||||
// the knob does not decide and the render-pass arm exists at all: without it the shader pass
|
||||
// is the only arm and there is no order to choose). ElideSubject does not decide: it is
|
||||
// measured like Measure, and the elision is the measuring callable's to apply.
|
||||
template <typename MeasureFn>
|
||||
WireDepthResolveArmChoice ChooseWireDepthResolveArm(WireDepthResolveProbeKnob knob, Bool renderPassArmAvailable,
|
||||
MeasureFn&& measure) {
|
||||
WireDepthResolveArmChoice choice;
|
||||
if (knob == WireDepthResolveProbeKnob::ForceBug || knob == WireDepthResolveProbeKnob::ForceClean) {
|
||||
choice.measurement = CannedWireDepthResolveProbeMeasurement(knob == WireDepthResolveProbeKnob::ForceClean);
|
||||
} else if (renderPassArmAvailable) {
|
||||
choice.measurement = measure();
|
||||
}
|
||||
choice.verdict = EvaluateWireDepthResolveProbe(choice.measurement);
|
||||
choice.preferShader = choice.verdict == WireDepthResolveProbeVerdict::RenderPassResolveBroken;
|
||||
return choice;
|
||||
}
|
||||
|
||||
// WHOSE VERDICT IT IS (codex closeout finding 7). The verdict is a property of ONE device and
|
||||
// driver, and a process can hold more than one: inproc tears a renderer down and initializes
|
||||
// another, possibly on a different physical device or under a different driver. The choice was
|
||||
// a function-static - the first renderer's answer for every renderer after it - so a second
|
||||
// device skipped its own probe and took an order measured elsewhere. It is now memoized per
|
||||
// IDENTITY: the device (vendor, device, driver version, pipeline-cache UUID - the last changes
|
||||
// with the driver build even where the version number does not) plus the two inputs the choice
|
||||
// itself depends on (whether a render-pass arm exists to judge, and the test knob). The same
|
||||
// identity is still probed once per process; a different one is probed for itself.
|
||||
struct WireDepthResolveDeviceIdentity {
|
||||
Uint32 vendorID = 0;
|
||||
Uint32 deviceID = 0;
|
||||
Uint32 driverVersion = 0;
|
||||
Uint8 pipelineCacheUUID[16] = {};
|
||||
Bool renderPassArmAvailable = false;
|
||||
WireDepthResolveProbeKnob knob = WireDepthResolveProbeKnob::Measure;
|
||||
|
||||
Bool operator==(const WireDepthResolveDeviceIdentity& other) const {
|
||||
return vendorID == other.vendorID && deviceID == other.deviceID && driverVersion == other.driverVersion &&
|
||||
std::memcmp(pipelineCacheUUID, other.pipelineCacheUUID, sizeof(pipelineCacheUUID)) == 0 &&
|
||||
renderPassArmAvailable == other.renderPassArmAvailable && knob == other.knob;
|
||||
}
|
||||
};
|
||||
|
||||
// The renderer's identity from its device's VkPhysicalDeviceProperties - the ONE place the fields
|
||||
// are filled (VulkanRenderer::ArmWireDepthResolveOrder calls it), so the unit test proves that each
|
||||
// property reaches the key: with only the cache tested, a field left out here stayed green.
|
||||
inline WireDepthResolveDeviceIdentity MakeWireDepthResolveDeviceIdentity(const VkPhysicalDeviceProperties& properties,
|
||||
Bool renderPassArmAvailable,
|
||||
WireDepthResolveProbeKnob knob) {
|
||||
WireDepthResolveDeviceIdentity identity;
|
||||
identity.vendorID = properties.vendorID;
|
||||
identity.deviceID = properties.deviceID;
|
||||
identity.driverVersion = properties.driverVersion;
|
||||
static_assert(sizeof(identity.pipelineCacheUUID) == sizeof(properties.pipelineCacheUUID));
|
||||
std::memcpy(identity.pipelineCacheUUID, properties.pipelineCacheUUID, sizeof(identity.pipelineCacheUUID));
|
||||
identity.renderPassArmAvailable = renderPassArmAvailable;
|
||||
identity.knob = knob;
|
||||
return identity;
|
||||
}
|
||||
|
||||
class WireDepthResolveArmCache {
|
||||
public:
|
||||
// The identity's memoized choice, or `decide()`'s - called at most once per identity, under
|
||||
// the cache's lock (a probe records on the device's queue; two renderers of one identity
|
||||
// must not both run it). `decided`, when given, says whether this call ran `decide`.
|
||||
template <typename DecideFn>
|
||||
WireDepthResolveArmChoice Resolve(const WireDepthResolveDeviceIdentity& identity, DecideFn&& decide,
|
||||
Bool* decided = nullptr) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& entry : m_entries) {
|
||||
if (entry.first == identity) {
|
||||
if (decided) *decided = false;
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
WireDepthResolveArmChoice choice = decide();
|
||||
m_entries.emplace_back(identity, choice);
|
||||
if (decided) *decided = true;
|
||||
return choice;
|
||||
}
|
||||
|
||||
SizeT Size() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_entries.size();
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
Vector<std::pair<WireDepthResolveDeviceIdentity, WireDepthResolveArmChoice>> m_entries;
|
||||
};
|
||||
|
||||
// One format's readings, both arms: "D24_UNORM_S8_UINT x4: depth expected 0x400000 sentinel
|
||||
// 0xbfffff: render pass 0/16 (first 0xbfffff, sentinel 16), shader control 16/16 (first
|
||||
// 0x400000) / stencil ...".
|
||||
inline String DescribeWireDepthResolveFormat(const WireDepthResolveFormatReading& reading) {
|
||||
const auto aspectText = [](const char* name, const WireDepthResolveAspectReading& aspect) {
|
||||
String text = format("{} expected 0x{:x} sentinel 0x{:x}: render pass {}/{} (first 0x{:x}, sentinel {}), ",
|
||||
name, aspect.expected, aspect.sentinel, aspect.renderPassMatches, aspect.texels,
|
||||
aspect.renderPassFirst, aspect.renderPassSentinels);
|
||||
if (aspect.shaderRan) {
|
||||
text += format("shader control {}/{} (first 0x{:x})", aspect.shaderMatches, aspect.texels,
|
||||
aspect.shaderFirst);
|
||||
} else {
|
||||
text += "no shader control";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
String out = reading.name;
|
||||
if (!reading.ran) return out + " skipped (" + reading.skipReason + ")";
|
||||
out += format(" x{}: ", reading.samples);
|
||||
if (reading.depth.measured) out += aspectText("depth", reading.depth);
|
||||
if (reading.depth.measured && reading.stencil.measured) out += " / ";
|
||||
if (reading.stencil.measured) out += aspectText("stencil", reading.stencil);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every format, "; "-separated, for the POST row.
|
||||
inline String DescribeWireDepthResolveProbe(const WireDepthResolveProbeMeasurement& measurement) {
|
||||
if (!measurement.ran) {
|
||||
return measurement.failureReason.empty() ? String("not run") : "not run (" + measurement.failureReason + ")";
|
||||
}
|
||||
String out;
|
||||
for (const WireDepthResolveFormatReading& reading : measurement.formats) {
|
||||
if (!out.empty()) out += "; ";
|
||||
out += DescribeWireDepthResolveFormat(reading);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,992 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
// Copyright (c) 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
|
||||
//
|
||||
// See WireDepthResolveProbe.h for the arms and the probe's shape, WireDepthResolveArm.h for the
|
||||
// verdict. The plumbing is the PRIMITIVES_GENERATED probe's (MG_Util/SelfTest/
|
||||
// PrimitivesGeneratedNoXfbProbe.cpp): one throwaway command buffer, a bounded fence wait that
|
||||
// leaks rather than idle-waits a hung GPU, teardown on every other path.
|
||||
|
||||
#include "WireDepthResolveProbe.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#include "WireMultisampleResolveSpirv.h"
|
||||
|
||||
VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format) {
|
||||
if (format == VK_FORMAT_S8_UINT) return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
if (format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT ||
|
||||
format == VK_FORMAT_D32_SFLOAT_S8_UINT)
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects,
|
||||
Uint32 level, Uint32 layer) {
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = image;
|
||||
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
info.format = format;
|
||||
info.subresourceRange = {aspects, level, 1, layer, 1};
|
||||
return info;
|
||||
}
|
||||
|
||||
VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device,
|
||||
VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass) {
|
||||
const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(format);
|
||||
VkAttachmentDescription2 attachments[2]{};
|
||||
for (Uint32 i = 0; i < 2; ++i) {
|
||||
auto& attachment = attachments[i];
|
||||
attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2;
|
||||
attachment.format = format;
|
||||
attachment.samples = i == 0 ? samples : VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = attachment.stencilLoadOp =
|
||||
i == 0 ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
}
|
||||
VkAttachmentReference2 sourceReference{VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2};
|
||||
sourceReference.attachment = 0;
|
||||
sourceReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
sourceReference.aspectMask = formatAspects;
|
||||
VkAttachmentReference2 resolveReference = sourceReference;
|
||||
resolveReference.attachment = 1;
|
||||
VkSubpassDescriptionDepthStencilResolve resolve{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE};
|
||||
resolve.depthResolveMode =
|
||||
(formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE;
|
||||
resolve.stencilResolveMode =
|
||||
(formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ? VK_RESOLVE_MODE_SAMPLE_ZERO_BIT : VK_RESOLVE_MODE_NONE;
|
||||
resolve.pDepthStencilResolveAttachment = &resolveReference;
|
||||
VkSubpassDescription2 subpass{VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2};
|
||||
subpass.pNext = &resolve;
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &sourceReference;
|
||||
VkRenderPassCreateInfo2 passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2};
|
||||
passInfo.attachmentCount = 2;
|
||||
passInfo.pAttachments = attachments;
|
||||
passInfo.subpassCount = 1;
|
||||
passInfo.pSubpasses = &subpass;
|
||||
return createRenderPass2(device, &passInfo, nullptr, pass);
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit) {
|
||||
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = bytes;
|
||||
info.pCode = code;
|
||||
return vkCreateShaderModule(device, &info, nullptr, &module);
|
||||
};
|
||||
VkResult result =
|
||||
shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), kit.vertex);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
result = shader(kWireMultisampleDepthResolveFragmentSpirv, sizeof(kWireMultisampleDepthResolveFragmentSpirv),
|
||||
kit.depthFragment);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the
|
||||
// StencilExportEXT capability, and handing a module with an unsupported capability to
|
||||
// vkCreateShaderModule is undefined behaviour - not an error this could catch.
|
||||
if (shaderStencilExport) {
|
||||
result = shader(kWireMultisampleStencilResolveFragmentSpirv,
|
||||
sizeof(kWireMultisampleStencilResolveFragmentSpirv), kit.stencilFragment);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
}
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
layout.bindingCount = 1;
|
||||
layout.pBindings = &binding;
|
||||
result = vkCreateDescriptorSetLayout(device, &layout, nullptr, &kit.descriptorLayout);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and its
|
||||
// rectangle from the scissor, so neither stage takes an input.
|
||||
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
pipelineLayout.setLayoutCount = 1;
|
||||
pipelineLayout.pSetLayouts = &kit.descriptorLayout;
|
||||
result = vkCreatePipelineLayout(device, &pipelineLayout, nullptr, &kit.pipelineLayout);
|
||||
if (result != VK_SUCCESS) return result;
|
||||
// NEAREST: texelFetch ignores the sampler's filter, and most devices do not advertise
|
||||
// linear filtering for a depth format anyway. The sampler exists only because GLSL's
|
||||
// sampler2DMS is a COMBINED image sampler.
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.maxLod = 0.0f;
|
||||
return vkCreateSampler(device, &sampler, nullptr, &kit.nearest);
|
||||
}
|
||||
|
||||
void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit) {
|
||||
vkDestroySampler(device, kit.nearest, nullptr);
|
||||
vkDestroyPipelineLayout(device, kit.pipelineLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(device, kit.descriptorLayout, nullptr);
|
||||
vkDestroyShaderModule(device, kit.vertex, nullptr);
|
||||
vkDestroyShaderModule(device, kit.depthFragment, nullptr);
|
||||
vkDestroyShaderModule(device, kit.stencilFragment, nullptr);
|
||||
kit = {};
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass) {
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for the
|
||||
// aspect it owns, and the caller copies only that aspect out. The other aspect of a packed
|
||||
// format is left undefined on purpose - it is never read, and LOADing it would order this
|
||||
// pass behind a write nobody is waiting for.
|
||||
attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &depthStencil;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = 1;
|
||||
info.pAttachments = &attachment;
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
return vkCreateRenderPass(device, &info, nullptr, pass);
|
||||
}
|
||||
|
||||
VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass,
|
||||
VkImageAspectFlags aspect, VkPipeline* pipeline) {
|
||||
const VkShaderModule fragment =
|
||||
aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? kit.stencilFragment : kit.depthFragment;
|
||||
if (fragment == VK_NULL_HANDLE || kit.vertex == VK_NULL_HANDLE) return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||
for (auto& stage : stages) {
|
||||
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
stage.pName = "main";
|
||||
}
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = kit.vertex;
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = fragment;
|
||||
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
raster.cullMode = VK_CULL_MODE_NONE;
|
||||
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
raster.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencilState{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencilState.minDepthBounds = 0.0f;
|
||||
depthStencilState.maxDepthBounds = 1.0f;
|
||||
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
// ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever
|
||||
// DONT_CARE left behind would discard some of it.
|
||||
depthStencilState.depthTestEnable = VK_TRUE;
|
||||
depthStencilState.depthWriteEnable = VK_TRUE;
|
||||
depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS;
|
||||
} else {
|
||||
// REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes the
|
||||
// shader-exported reference the value that lands. The static `reference` is overridden
|
||||
// by gl_FragStencilRefARB, which is the whole point of the extension.
|
||||
depthStencilState.stencilTestEnable = VK_TRUE;
|
||||
VkStencilOpState op{};
|
||||
op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE;
|
||||
op.compareOp = VK_COMPARE_OP_ALWAYS;
|
||||
op.compareMask = op.writeMask = 0xFFu;
|
||||
op.reference = 0;
|
||||
depthStencilState.front = depthStencilState.back = op;
|
||||
}
|
||||
// No colour attachment in this render pass, so no attachment states either.
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
info.stageCount = 2;
|
||||
info.pStages = stages;
|
||||
info.pVertexInputState = &vertex;
|
||||
info.pInputAssemblyState = &assembly;
|
||||
info.pViewportState = &viewport;
|
||||
info.pRasterizationState = &raster;
|
||||
info.pMultisampleState = &samples;
|
||||
info.pDepthStencilState = &depthStencilState;
|
||||
info.pColorBlendState = &blend;
|
||||
info.pDynamicState = &dynamic;
|
||||
info.layout = kit.pipelineLayout;
|
||||
info.renderPass = pass;
|
||||
return vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, pipeline);
|
||||
}
|
||||
|
||||
void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit,
|
||||
VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer,
|
||||
VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = pass;
|
||||
begin.framebuffer = framebuffer;
|
||||
// The RECTANGLE is the render area, exactly as the render-pass arm's renderArea is - the
|
||||
// scratch outside it is never read.
|
||||
begin.renderArea = rect;
|
||||
vkCmdBeginRenderPass(commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
// THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not
|
||||
// interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the
|
||||
// clip-space-to-attachment mapping has to be the identity for every rectangle. A viewport
|
||||
// shrunk onto the rect would slide the whole image instead of moving a window over it.
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(attachmentExtent.width),
|
||||
static_cast<float>(attachmentExtent.height), 0.0f, 1.0f};
|
||||
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
|
||||
vkCmdSetScissor(commandBuffer, 0, 1, &rect);
|
||||
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, kit.pipelineLayout, 0, 1,
|
||||
&descriptorSet, 0, nullptr);
|
||||
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr Uint32 kProbeExtent = 4;
|
||||
constexpr Uint32 kProbeTexels = kProbeExtent * kProbeExtent;
|
||||
// Every Vulkan device must support 4 samples on a depth/stencil attachment
|
||||
// (framebufferDepthSampleCounts), and the CTS case the probe stands for fails at 2 and 4.
|
||||
constexpr VkSampleCountFlagBits kProbeSamples = VK_SAMPLE_COUNT_4_BIT;
|
||||
constexpr Float kClearDepth = 0.25f;
|
||||
constexpr Float kSentinelDepth = 0.75f;
|
||||
constexpr Uint32 kClearStencil = 0x5Au;
|
||||
constexpr Uint32 kSentinelStencil = 0xA5u;
|
||||
constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull;
|
||||
|
||||
// The formats the wire maps GL's depth/stencil internal formats onto (VkTextureManager),
|
||||
// packed first: the case that set all this off failed on every one of them.
|
||||
struct ProbeFormat {
|
||||
VkFormat format;
|
||||
const char* name;
|
||||
};
|
||||
constexpr ProbeFormat kProbeFormats[] = {
|
||||
{VK_FORMAT_D24_UNORM_S8_UINT, "D24_UNORM_S8_UINT"},
|
||||
{VK_FORMAT_D32_SFLOAT_S8_UINT, "D32_SFLOAT_S8_UINT"},
|
||||
{VK_FORMAT_D16_UNORM, "D16_UNORM"},
|
||||
{VK_FORMAT_X8_D24_UNORM_PACK32, "X8_D24_UNORM_PACK32"},
|
||||
{VK_FORMAT_D32_SFLOAT, "D32_SFLOAT"},
|
||||
{VK_FORMAT_S8_UINT, "S8_UINT"},
|
||||
};
|
||||
|
||||
// The usages production gives the two images: a wire multisample depth/stencil attachment
|
||||
// (VkTextureManager: SAMPLED | DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC | TRANSFER_DST) and
|
||||
// ResolveWireDepthStencil's resolve scratch (DEPTH_STENCIL_ATTACHMENT | TRANSFER_SRC).
|
||||
constexpr VkImageUsageFlags kSourceUsage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
constexpr VkImageUsageFlags kScratchUsage =
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
Bool IsFloatDepth(VkFormat format) {
|
||||
return format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
}
|
||||
|
||||
Uint32 DepthCopyBytes(VkFormat format) { return format == VK_FORMAT_D16_UNORM ? 2u : 4u; }
|
||||
|
||||
Uint32 EncodeDepth(VkFormat format, Float depth) {
|
||||
if (IsFloatDepth(format)) return std::bit_cast<Uint32>(depth);
|
||||
const double scale = format == VK_FORMAT_D16_UNORM ? 65535.0 : 16777215.0;
|
||||
return static_cast<Uint32>(std::lround(static_cast<double>(depth) * scale));
|
||||
}
|
||||
|
||||
Uint32 ReadDepthWord(VkFormat format, const Uint8* texel) {
|
||||
if (format == VK_FORMAT_D16_UNORM) {
|
||||
Uint16 word = 0;
|
||||
std::memcpy(&word, texel, sizeof(word));
|
||||
return word;
|
||||
}
|
||||
Uint32 word = 0;
|
||||
std::memcpy(&word, texel, sizeof(word));
|
||||
// A D24 aspect copies out as a 32-bit word whose top byte is undefined.
|
||||
return IsFloatDepth(format) ? word : (word & 0x00FFFFFFu);
|
||||
}
|
||||
|
||||
Uint32 WordDistance(Uint32 a, Uint32 b) { return a > b ? a - b : b - a; }
|
||||
|
||||
struct ProbeImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
struct ControlPass {
|
||||
Bool ready = false;
|
||||
ProbeImage target;
|
||||
VkRenderPass pass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkImageView sampledView = VK_NULL_HANDLE;
|
||||
VkImageView targetView = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0;
|
||||
};
|
||||
|
||||
struct FormatWork {
|
||||
SizeT reading = 0;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
ProbeImage subjectSource, controlSource, subjectTarget;
|
||||
VkRenderPass resolvePass = VK_NULL_HANDLE;
|
||||
VkImageView resolveViews[2]{};
|
||||
VkFramebuffer resolveFramebuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize subjectDepthOffset = 0, subjectStencilOffset = 0;
|
||||
ControlPass depthControl, stencilControl;
|
||||
};
|
||||
|
||||
// Everything the probe creates, destroyed together (or leaked together on a timeout).
|
||||
struct ProbeObjects {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
Vector<VkImage> images;
|
||||
Vector<VkDeviceMemory> memories;
|
||||
Vector<VkImageView> views;
|
||||
Vector<VkRenderPass> passes;
|
||||
Vector<VkFramebuffer> framebuffers;
|
||||
Vector<VkPipeline> pipelines;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
WireShaderResolveKit kit;
|
||||
|
||||
void Destroy() {
|
||||
for (VkPipeline pipeline : pipelines) vkDestroyPipeline(device, pipeline, nullptr);
|
||||
for (VkFramebuffer framebuffer : framebuffers) vkDestroyFramebuffer(device, framebuffer, nullptr);
|
||||
for (VkRenderPass pass : passes) vkDestroyRenderPass(device, pass, nullptr);
|
||||
for (VkImageView view : views) vkDestroyImageView(device, view, nullptr);
|
||||
for (VkImage image : images) vkDestroyImage(device, image, nullptr);
|
||||
vkDestroyBuffer(device, buffer, nullptr);
|
||||
for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr);
|
||||
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
|
||||
vkDestroyCommandPool(device, commandPool, nullptr);
|
||||
vkDestroyFence(device, fence, nullptr);
|
||||
DestroyWireShaderResolveKit(device, kit);
|
||||
}
|
||||
};
|
||||
|
||||
Optional<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
|
||||
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
|
||||
Optional<Uint32> fallback;
|
||||
for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) {
|
||||
if (!(allowed & (1u << i))) continue;
|
||||
const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags;
|
||||
if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue;
|
||||
if ((flags & required) != required) continue;
|
||||
if ((flags & wanted) == wanted) return i;
|
||||
if (!fallback) fallback = i;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void ImageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageAspectFlags aspects,
|
||||
VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStage,
|
||||
VkAccessFlags srcAccess, VkPipelineStageFlags dstStage, VkAccessFlags dstAccess) {
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = srcAccess;
|
||||
barrier.dstAccessMask = dstAccess;
|
||||
barrier.oldLayout = oldLayout;
|
||||
barrier.newLayout = newLayout;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = image;
|
||||
barrier.subresourceRange = {aspects, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
measurement.subjectElided = context.elideSubject;
|
||||
const VkDevice device = context.device;
|
||||
if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE || context.createRenderPass2 == nullptr) {
|
||||
measurement.failureReason = "no device, queue or vkCreateRenderPass2";
|
||||
return measurement;
|
||||
}
|
||||
ProbeObjects objects;
|
||||
objects.device = device;
|
||||
Bool leak = false;
|
||||
struct Teardown {
|
||||
ProbeObjects& objects;
|
||||
Bool& leak;
|
||||
~Teardown() {
|
||||
if (!leak) objects.Destroy();
|
||||
}
|
||||
} teardown{objects, leak};
|
||||
const auto fail = [&](String reason) {
|
||||
measurement.ran = false;
|
||||
measurement.failureReason = Move(reason);
|
||||
return measurement;
|
||||
};
|
||||
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties);
|
||||
const auto makeImage = [&](VkFormat format, VkSampleCountFlagBits samples, VkImageUsageFlags usage,
|
||||
ProbeImage& out) -> Bool {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format;
|
||||
info.extent = {kProbeExtent, kProbeExtent, 1};
|
||||
info.mipLevels = info.arrayLayers = 1;
|
||||
info.samples = samples;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = usage;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
if (vkCreateImage(device, &info, nullptr, &out.image) != VK_SUCCESS) return false;
|
||||
objects.images.push_back(out.image);
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(device, out.image, &requirements);
|
||||
const Optional<Uint32> type = PickMemoryType(memoryProperties, requirements.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0);
|
||||
if (!type) return false;
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size;
|
||||
allocate.memoryTypeIndex = *type;
|
||||
if (vkAllocateMemory(device, &allocate, nullptr, &out.memory) != VK_SUCCESS) return false;
|
||||
objects.memories.push_back(out.memory);
|
||||
return vkBindImageMemory(device, out.image, out.memory, 0) == VK_SUCCESS;
|
||||
};
|
||||
const auto makeView = [&](const VkImageViewCreateInfo& info, VkImageView& out) -> Bool {
|
||||
if (vkCreateImageView(device, &info, nullptr, &out) != VK_SUCCESS) return false;
|
||||
objects.views.push_back(out);
|
||||
return true;
|
||||
};
|
||||
const auto makeFramebuffer = [&](VkRenderPass pass, const VkImageView* views, Uint32 count,
|
||||
VkFramebuffer& out) -> Bool {
|
||||
VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
info.renderPass = pass;
|
||||
info.attachmentCount = count;
|
||||
info.pAttachments = views;
|
||||
info.width = info.height = kProbeExtent;
|
||||
info.layers = 1;
|
||||
if (vkCreateFramebuffer(device, &info, nullptr, &out) != VK_SUCCESS) return false;
|
||||
objects.framebuffers.push_back(out);
|
||||
return true;
|
||||
};
|
||||
// The wire's clear (WireFramebuffer.inc, ClearWireFramebuffer): a LOAD/STORE pass over every
|
||||
// aspect of the format with vkCmdClearAttachments inside it. Recorded later; built now.
|
||||
struct ClearPass {
|
||||
VkRenderPass pass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
Float depth = 0.0f;
|
||||
Uint32 stencil = 0;
|
||||
};
|
||||
Vector<ClearPass> clears;
|
||||
const auto makeClear = [&](const ProbeImage& image, VkFormat format, VkSampleCountFlagBits samples,
|
||||
Float depth, Uint32 stencil) -> Bool {
|
||||
const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format);
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
if (!makeView(WireDepthStencilViewInfo(image.image, format, aspects, 0, 0), view)) return false;
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = samples;
|
||||
attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.pDepthStencilAttachment = &reference;
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = 1;
|
||||
info.pAttachments = &attachment;
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
ClearPass clear;
|
||||
if (vkCreateRenderPass(device, &info, nullptr, &clear.pass) != VK_SUCCESS) return false;
|
||||
objects.passes.push_back(clear.pass);
|
||||
if (!makeFramebuffer(clear.pass, &view, 1, clear.framebuffer)) return false;
|
||||
clear.aspects = aspects;
|
||||
clear.depth = depth;
|
||||
clear.stencil = stencil;
|
||||
clears.push_back(clear);
|
||||
return true;
|
||||
};
|
||||
|
||||
// THE CONTROL'S FORMAT-INDEPENDENT HALF. A kit that cannot be made leaves every control
|
||||
// un-run, which the verdict reads as INCONCLUSIVE - never as the bug.
|
||||
String kitFailure;
|
||||
if (const VkResult result = CreateWireShaderResolveKit(device, context.shaderStencilExport, objects.kit);
|
||||
result != VK_SUCCESS)
|
||||
kitFailure = format("shader resolve kit creation failed (VkResult {})", static_cast<Int>(result));
|
||||
|
||||
// Readback layout: every region on a 16-byte boundary (a depth/stencil buffer copy wants a
|
||||
// multiple of 4).
|
||||
VkDeviceSize bufferBytes = 0;
|
||||
const auto reserve = [&](VkDeviceSize bytes) {
|
||||
const VkDeviceSize offset = bufferBytes;
|
||||
bufferBytes += (bytes + 15u) & ~VkDeviceSize{15u};
|
||||
return offset;
|
||||
};
|
||||
|
||||
Vector<FormatWork> work;
|
||||
for (const ProbeFormat& probeFormat : kProbeFormats) {
|
||||
WireDepthResolveFormatReading reading;
|
||||
reading.name = probeFormat.name;
|
||||
reading.format = static_cast<Int32>(probeFormat.format);
|
||||
reading.samples = static_cast<Uint32>(kProbeSamples);
|
||||
const SizeT readingIndex = measurement.formats.size();
|
||||
measurement.formats.push_back(reading);
|
||||
auto& slot = measurement.formats.back();
|
||||
// A skipped format records nothing: the clears it had already built are dropped with it
|
||||
// (their images stay in `objects` and are only destroyed).
|
||||
const SizeT clearsBefore = clears.size();
|
||||
const auto skip = [&](String reason) {
|
||||
slot.skipReason = Move(reason);
|
||||
clears.resize(clearsBefore);
|
||||
};
|
||||
|
||||
const VkFormat format = probeFormat.format;
|
||||
const VkImageAspectFlags aspects = WireDepthStencilFormatAspects(format);
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(context.physicalDevice, format, &formatProperties);
|
||||
constexpr VkFormatFeatureFlags kRequired =
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
if ((formatProperties.optimalTilingFeatures & kRequired) != kRequired) {
|
||||
skip("not a sampled depth/stencil attachment format here");
|
||||
continue;
|
||||
}
|
||||
VkImageFormatProperties imageProperties{};
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, kSourceUsage, 0,
|
||||
&imageProperties) != VK_SUCCESS ||
|
||||
!(imageProperties.sampleCounts & kProbeSamples)) {
|
||||
skip("no 4x multisample image with the wire's attachment usage");
|
||||
continue;
|
||||
}
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(context.physicalDevice, format, VK_IMAGE_TYPE_2D,
|
||||
VK_IMAGE_TILING_OPTIMAL, kScratchUsage, 0,
|
||||
&imageProperties) != VK_SUCCESS) {
|
||||
skip("no single-sample image with the resolve scratch's usage");
|
||||
continue;
|
||||
}
|
||||
// ResolveWireDepthStencil's own test for the render-pass arm, per format.
|
||||
if (((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) &&
|
||||
!(context.depthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) ||
|
||||
((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) &&
|
||||
!(context.stencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT))) {
|
||||
skip("the render-pass arm is not taken for this format (no SAMPLE_ZERO resolve mode)");
|
||||
continue;
|
||||
}
|
||||
|
||||
FormatWork item;
|
||||
item.reading = readingIndex;
|
||||
item.format = format;
|
||||
item.aspects = aspects;
|
||||
if (!makeImage(format, kProbeSamples, kSourceUsage, item.subjectSource) ||
|
||||
!makeImage(format, kProbeSamples, kSourceUsage, item.controlSource) ||
|
||||
!makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, item.subjectTarget)) {
|
||||
skip("image creation failed");
|
||||
continue;
|
||||
}
|
||||
if (CreateWireDepthStencilResolvePass(context.createRenderPass2, device, format, kProbeSamples,
|
||||
&item.resolvePass) != VK_SUCCESS) {
|
||||
skip("the render-pass arm's vkCreateRenderPass2 failed");
|
||||
continue;
|
||||
}
|
||||
objects.passes.push_back(item.resolvePass);
|
||||
if (!makeView(WireDepthStencilViewInfo(item.subjectSource.image, format, aspects, 0, 0),
|
||||
item.resolveViews[0]) ||
|
||||
!makeView(WireDepthStencilViewInfo(item.subjectTarget.image, format, aspects, 0, 0),
|
||||
item.resolveViews[1]) ||
|
||||
!makeFramebuffer(item.resolvePass, item.resolveViews, 2, item.resolveFramebuffer)) {
|
||||
skip("the render-pass arm's views or framebuffer could not be created");
|
||||
continue;
|
||||
}
|
||||
if (!makeClear(item.subjectSource, format, kProbeSamples, kClearDepth, kClearStencil) ||
|
||||
!makeClear(item.controlSource, format, kProbeSamples, kClearDepth, kClearStencil) ||
|
||||
!makeClear(item.subjectTarget, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil)) {
|
||||
skip("the clear passes could not be created");
|
||||
continue;
|
||||
}
|
||||
// The control, one target per aspect: the shader pass's attachment is DONT_CARE on both
|
||||
// aspects, so a second aspect's pass into the same target would discard the first.
|
||||
const auto makeControl = [&](VkImageAspectFlags aspect, ControlPass& control) {
|
||||
if (!kitFailure.empty()) return;
|
||||
if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && objects.kit.stencilFragment == VK_NULL_HANDLE) return;
|
||||
if (!makeImage(format, VK_SAMPLE_COUNT_1_BIT, kScratchUsage, control.target)) return;
|
||||
if (CreateWireShaderResolveRenderPass(device, format, &control.pass) != VK_SUCCESS) return;
|
||||
objects.passes.push_back(control.pass);
|
||||
if (CreateWireShaderResolvePipeline(device, objects.kit, control.pass, aspect, &control.pipeline) !=
|
||||
VK_SUCCESS)
|
||||
return;
|
||||
objects.pipelines.push_back(control.pipeline);
|
||||
if (!makeView(WireDepthStencilViewInfo(item.controlSource.image, format, aspect, 0, 0),
|
||||
control.sampledView) ||
|
||||
!makeView(WireDepthStencilViewInfo(control.target.image, format, aspects, 0, 0),
|
||||
control.targetView) ||
|
||||
!makeFramebuffer(control.pass, &control.targetView, 1, control.framebuffer))
|
||||
return;
|
||||
if (!makeClear(control.target, format, VK_SAMPLE_COUNT_1_BIT, kSentinelDepth, kSentinelStencil))
|
||||
return;
|
||||
control.ready = true;
|
||||
};
|
||||
if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) makeControl(VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl);
|
||||
if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) makeControl(VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl);
|
||||
|
||||
if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
item.subjectDepthOffset = reserve(kProbeTexels * DepthCopyBytes(format));
|
||||
if (item.depthControl.ready) item.depthControl.offset = reserve(kProbeTexels * DepthCopyBytes(format));
|
||||
}
|
||||
if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
||||
item.subjectStencilOffset = reserve(kProbeTexels);
|
||||
if (item.stencilControl.ready) item.stencilControl.offset = reserve(kProbeTexels);
|
||||
}
|
||||
slot.ran = true;
|
||||
work.push_back(item);
|
||||
}
|
||||
if (work.empty()) return fail("no format could be probed");
|
||||
|
||||
// Descriptor sets for the controls, one per draw (the production shader arm's own rule).
|
||||
Uint32 controlDraws = 0;
|
||||
for (const FormatWork& item : work)
|
||||
controlDraws += (item.depthControl.ready ? 1u : 0u) + (item.stencilControl.ready ? 1u : 0u);
|
||||
if (controlDraws > 0) {
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, controlDraws};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = controlDraws;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
if (vkCreateDescriptorPool(device, &pool, nullptr, &objects.descriptorPool) != VK_SUCCESS)
|
||||
return fail("vkCreateDescriptorPool failed");
|
||||
for (FormatWork& item : work) {
|
||||
for (ControlPass* control : {&item.depthControl, &item.stencilControl}) {
|
||||
if (!control->ready) continue;
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = objects.descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &objects.kit.descriptorLayout;
|
||||
if (vkAllocateDescriptorSets(device, &alloc, &control->set) != VK_SUCCESS)
|
||||
return fail("vkAllocateDescriptorSets failed");
|
||||
const VkDescriptorImageInfo sampled{objects.kit.nearest, control->sampledView,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = control->set;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The host-visible readback buffer.
|
||||
VkDeviceMemory bufferMemory = VK_NULL_HANDLE;
|
||||
Bool bufferCoherent = false;
|
||||
{
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = bufferBytes;
|
||||
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateBuffer(device, &info, nullptr, &objects.buffer) != VK_SUCCESS)
|
||||
return fail("vkCreateBuffer failed");
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetBufferMemoryRequirements(device, objects.buffer, &requirements);
|
||||
const Optional<Uint32> type =
|
||||
PickMemoryType(memoryProperties, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
|
||||
if (!type) return fail("no host-visible memory type for the readback buffer");
|
||||
bufferCoherent = (memoryProperties.memoryTypes[*type].propertyFlags &
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size;
|
||||
allocate.memoryTypeIndex = *type;
|
||||
if (vkAllocateMemory(device, &allocate, nullptr, &bufferMemory) != VK_SUCCESS)
|
||||
return fail("vkAllocateMemory (readback) failed");
|
||||
objects.memories.push_back(bufferMemory);
|
||||
if (vkBindBufferMemory(device, objects.buffer, bufferMemory, 0) != VK_SUCCESS)
|
||||
return fail("vkBindBufferMemory failed");
|
||||
}
|
||||
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
|
||||
if (vkCreateCommandPool(device, &poolInfo, nullptr, &objects.commandPool) != VK_SUCCESS)
|
||||
return fail("vkCreateCommandPool failed");
|
||||
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
commandInfo.commandPool = objects.commandPool;
|
||||
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
commandInfo.commandBufferCount = 1;
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS)
|
||||
return fail("vkAllocateCommandBuffers failed");
|
||||
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed");
|
||||
|
||||
const VkRect2D fullRect{{0, 0}, {kProbeExtent, kProbeExtent}};
|
||||
// (1) Every image UNDEFINED -> DEPTH_STENCIL_ATTACHMENT_OPTIMAL, the way TransitionWireImage
|
||||
// moves a fresh image (source state from UNDEFINED, destination ALL_COMMANDS).
|
||||
for (const FormatWork& item : work) {
|
||||
const VkImage images[] = {item.subjectSource.image, item.controlSource.image, item.subjectTarget.image,
|
||||
item.depthControl.ready ? item.depthControl.target.image : VK_NULL_HANDLE,
|
||||
item.stencilControl.ready ? item.stencilControl.target.image : VK_NULL_HANDLE};
|
||||
for (const VkImage image : images) {
|
||||
if (image == VK_NULL_HANDLE) continue;
|
||||
ImageBarrier(cmd, image, item.aspects, VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
}
|
||||
}
|
||||
// (2) The inputs and the sentinels, cleared the wire's way.
|
||||
for (const ClearPass& clear : clears) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = clear.pass;
|
||||
begin.framebuffer = clear.framebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
VkClearAttachment attachment{};
|
||||
attachment.aspectMask = clear.aspects;
|
||||
attachment.clearValue.depthStencil = {clear.depth, clear.stencil};
|
||||
const VkClearRect rect{fullRect, 0, 1};
|
||||
vkCmdClearAttachments(cmd, 1, &attachment, 1, &rect);
|
||||
vkCmdEndRenderPass(cmd);
|
||||
}
|
||||
// (3) What TransitionWireImage records when the layout already matches - the case of a
|
||||
// source the application cleared and then resolves.
|
||||
{
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
|
||||
&memory, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
// (4) THE SUBJECT: the render-pass arm, begun and ended with no draw. Under the test-only
|
||||
// elideSubject it is not recorded at all: its target keeps the sentinel cleared in (2) and
|
||||
// the layout it was left in, which is what (6) expects either way.
|
||||
if (!context.elideSubject) {
|
||||
for (const FormatWork& item : work) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = item.resolvePass;
|
||||
begin.framebuffer = item.resolveFramebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(cmd);
|
||||
}
|
||||
}
|
||||
// (5) THE CONTROL: the shader arm on the twin source, which TransitionWireImage moves to
|
||||
// SHADER_READ_ONLY_OPTIMAL first (both aspects of a packed format).
|
||||
for (const FormatWork& item : work) {
|
||||
if (!item.depthControl.ready && !item.stencilControl.ready) continue;
|
||||
ImageBarrier(cmd, item.controlSource.image, item.aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
for (const ControlPass* control : {&item.depthControl, &item.stencilControl}) {
|
||||
if (!control->ready) continue;
|
||||
RecordWireShaderResolveDraw(cmd, objects.kit, control->pass, control->pipeline, control->framebuffer,
|
||||
control->set, {kProbeExtent, kProbeExtent}, fullRect);
|
||||
}
|
||||
}
|
||||
// (6) Every target to TRANSFER_SRC with the barrier ResolveWireDepthStencil puts after its
|
||||
// resolve (a depth/stencil resolve writes in COLOR_ATTACHMENT_OUTPUT), then one copy per aspect.
|
||||
const auto copyOut = [&](VkImage image, VkImageAspectFlags aspect, VkDeviceSize offset) {
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = offset;
|
||||
copy.imageSubresource = {aspect, 0, 0, 1};
|
||||
copy.imageExtent = {kProbeExtent, kProbeExtent, 1};
|
||||
vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, objects.buffer, 1, ©);
|
||||
};
|
||||
const auto toTransfer = [&](VkImage image, VkImageAspectFlags aspects) {
|
||||
ImageBarrier(cmd, image, aspects, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
};
|
||||
for (const FormatWork& item : work) {
|
||||
toTransfer(item.subjectTarget.image, item.aspects);
|
||||
if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT)
|
||||
copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.subjectDepthOffset);
|
||||
if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT)
|
||||
copyOut(item.subjectTarget.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.subjectStencilOffset);
|
||||
if (item.depthControl.ready) {
|
||||
toTransfer(item.depthControl.target.image, item.aspects);
|
||||
copyOut(item.depthControl.target.image, VK_IMAGE_ASPECT_DEPTH_BIT, item.depthControl.offset);
|
||||
}
|
||||
if (item.stencilControl.ready) {
|
||||
toTransfer(item.stencilControl.target.image, item.aspects);
|
||||
copyOut(item.stencilControl.target.image, VK_IMAGE_ASPECT_STENCIL_BIT, item.stencilControl.offset);
|
||||
}
|
||||
}
|
||||
{
|
||||
VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.buffer = objects.buffer;
|
||||
barrier.size = VK_WHOLE_SIZE;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1,
|
||||
&barrier, 0, nullptr);
|
||||
}
|
||||
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed");
|
||||
|
||||
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS)
|
||||
return fail("vkCreateFence failed");
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &cmd;
|
||||
if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS)
|
||||
return fail(format("vkQueueSubmit failed (VkResult {})", static_cast<Int>(result)));
|
||||
const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs);
|
||||
if (waited == VK_TIMEOUT) {
|
||||
// The submission may still be executing: destroying anything it names, or idle-waiting
|
||||
// the device, is the hang the bound exists to prevent. Everything leaks.
|
||||
leak = true;
|
||||
measurement.fenceWaitTimedOut = true;
|
||||
return fail("the probe's fence did not signal within 5 s");
|
||||
}
|
||||
if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast<Int>(waited)));
|
||||
|
||||
void* mapped = nullptr;
|
||||
if (vkMapMemory(device, bufferMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr)
|
||||
return fail("vkMapMemory failed");
|
||||
if (!bufferCoherent) {
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = bufferMemory;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
vkInvalidateMappedMemoryRanges(device, 1, &range);
|
||||
}
|
||||
const Uint8* bytes = static_cast<const Uint8*>(mapped);
|
||||
const auto tally = [&](WireDepthResolveAspectReading& aspect, VkDeviceSize offset, Uint32 texelBytes,
|
||||
Bool depth, VkFormat format, Bool subject) {
|
||||
Uint32 matches = 0, sentinels = 0, first = 0;
|
||||
for (Uint32 i = 0; i < kProbeTexels; ++i) {
|
||||
const Uint8* texel = bytes + offset + static_cast<VkDeviceSize>(i) * texelBytes;
|
||||
const Uint32 word = depth ? ReadDepthWord(format, texel) : static_cast<Uint32>(*texel);
|
||||
if (i == 0) first = word;
|
||||
if (WordDistance(word, aspect.expected) <= aspect.tolerance) ++matches;
|
||||
if (WordDistance(word, aspect.sentinel) <= aspect.tolerance) ++sentinels;
|
||||
}
|
||||
if (subject) {
|
||||
aspect.renderPassMatches = matches;
|
||||
aspect.renderPassSentinels = sentinels;
|
||||
aspect.renderPassFirst = first;
|
||||
} else {
|
||||
aspect.shaderRan = true;
|
||||
aspect.shaderMatches = matches;
|
||||
aspect.shaderFirst = first;
|
||||
}
|
||||
};
|
||||
for (const FormatWork& item : work) {
|
||||
WireDepthResolveFormatReading& reading = measurement.formats[item.reading];
|
||||
if (item.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
auto& aspect = reading.depth;
|
||||
aspect.measured = true;
|
||||
aspect.texels = kProbeTexels;
|
||||
aspect.expected = EncodeDepth(item.format, kClearDepth);
|
||||
aspect.sentinel = EncodeDepth(item.format, kSentinelDepth);
|
||||
aspect.tolerance = IsFloatDepth(item.format) ? 0u : 1u;
|
||||
tally(aspect, item.subjectDepthOffset, DepthCopyBytes(item.format), true, item.format, true);
|
||||
if (item.depthControl.ready)
|
||||
tally(aspect, item.depthControl.offset, DepthCopyBytes(item.format), true, item.format, false);
|
||||
}
|
||||
if (item.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
||||
auto& aspect = reading.stencil;
|
||||
aspect.measured = true;
|
||||
aspect.texels = kProbeTexels;
|
||||
aspect.expected = kClearStencil;
|
||||
aspect.sentinel = kSentinelStencil;
|
||||
aspect.tolerance = 0;
|
||||
tally(aspect, item.subjectStencilOffset, 1, false, item.format, true);
|
||||
if (item.stencilControl.ready)
|
||||
tally(aspect, item.stencilControl.offset, 1, false, item.format, false);
|
||||
}
|
||||
}
|
||||
vkUnmapMemory(device, bufferMemory);
|
||||
measurement.ran = true;
|
||||
if (!kitFailure.empty()) measurement.failureReason = kitFailure;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice(
|
||||
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
Bool& renderPassArmAvailable, Bool& shaderStencilExport) {
|
||||
WireDepthResolveProbeMeasurement measurement;
|
||||
const auto has = [&](const char* name) {
|
||||
for (const VkExtensionProperties& extension : deviceExtensions)
|
||||
if (std::strcmp(extension.extensionName, name) == 0) return true;
|
||||
return false;
|
||||
};
|
||||
renderPassArmAvailable = has(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) &&
|
||||
has(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME);
|
||||
shaderStencilExport = has(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
if (!renderPassArmAvailable) {
|
||||
measurement.failureReason = "no VK_KHR_create_renderpass2 + VK_KHR_depth_stencil_resolve";
|
||||
return measurement;
|
||||
}
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
|
||||
auto getProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2"));
|
||||
if (getProperties2 == nullptr)
|
||||
getProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
|
||||
getInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties2KHR"));
|
||||
const auto createDevice =
|
||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||
if (properties.apiVersion < VK_API_VERSION_1_1 || getProperties2 == nullptr || createDevice == nullptr) {
|
||||
renderPassArmAvailable = false;
|
||||
measurement.failureReason = "a pre-1.1 device, or no vkGetPhysicalDeviceProperties2 / vkCreateDevice";
|
||||
return measurement;
|
||||
}
|
||||
VkPhysicalDeviceDepthStencilResolveProperties resolveProperties{
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 properties2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
||||
properties2.pNext = &resolveProperties;
|
||||
getProperties2(physicalDevice, &properties2);
|
||||
|
||||
// The renderer's own extension set for the two arms (VulkanRenderer.cpp,
|
||||
// CreateLogicalDeviceAndQueues), nothing more.
|
||||
const Float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
queueInfo.queueCount = 1;
|
||||
queueInfo.pQueuePriorities = &priority;
|
||||
Vector<const char*> extensions{VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME,
|
||||
VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME};
|
||||
if (shaderStencilExport) extensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||
deviceInfo.enabledExtensionCount = static_cast<Uint32>(extensions.size());
|
||||
deviceInfo.ppEnabledExtensionNames = extensions.data();
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device);
|
||||
result != VK_SUCCESS || device == VK_NULL_HANDLE) {
|
||||
measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast<Int>(result));
|
||||
return measurement;
|
||||
}
|
||||
WireDepthResolveProbeContext context;
|
||||
context.physicalDevice = physicalDevice;
|
||||
context.device = device;
|
||||
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue);
|
||||
context.queueFamilyIndex = graphicsQueueFamilyIndex;
|
||||
context.createRenderPass2 =
|
||||
reinterpret_cast<PFN_vkCreateRenderPass2>(vkGetDeviceProcAddr(device, "vkCreateRenderPass2KHR"));
|
||||
context.depthResolveModes = resolveProperties.supportedDepthResolveModes;
|
||||
context.stencilResolveModes = resolveProperties.supportedStencilResolveModes;
|
||||
context.shaderStencilExport = shaderStencilExport;
|
||||
measurement = RunWireDepthResolveProbe(context);
|
||||
// A timed-out probe leaked its children on purpose; destroying their device would block
|
||||
// exactly where the bound says not to.
|
||||
if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr);
|
||||
return measurement;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,121 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h
|
||||
// Copyright (c) 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 "WireDepthResolveArm.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <Includes.h>
|
||||
|
||||
// P7 gate 5 (g5-msprobe): the Vulkan half of the multisample depth/stencil resolve probe, and the
|
||||
// two resolve arms it measures.
|
||||
//
|
||||
// THE ARMS LIVE HERE, NOT ONLY IN THE RENDERER. A probe that rebuilt "something like" the wire's
|
||||
// render pass would measure its own copy; the render-pass description, the shader pass's modules,
|
||||
// pipeline and draw, and the view shapes both arms attach are defined once below and called by
|
||||
// ResolveWireDepthStencil (WireFramebuffer.inc), ResolveWireDepthStencilWithShader
|
||||
// (WireMultisampleResolve.inc) and the probe alike.
|
||||
//
|
||||
// Two callers: the Magma renderer at device bring-up (server side, once per process, only where
|
||||
// the render-pass arm exists at all - VulkanRenderer::ArmWireDepthResolveOrder) and the POST's
|
||||
// "Known Driver Bugs" section (MG_Util/SelfTest/DriverBugProbes.cpp) on a throwaway device.
|
||||
//
|
||||
// Disaggregated builds only: the monolith never records either arm, and the pull build's image
|
||||
// must not change (P7 gate G1).
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Every aspect a wire depth/stencil format has.
|
||||
VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format);
|
||||
|
||||
// A 2D view of one level/layer of a depth/stencil image naming `aspects`. The render-pass arm
|
||||
// attaches both images with every aspect of the format; the shader arm SAMPLES its source
|
||||
// through a view of exactly one aspect (a depth/stencil image can only be sampled that way) and
|
||||
// attaches its target with every aspect.
|
||||
VkImageViewCreateInfo WireDepthStencilViewInfo(VkImage image, VkFormat format, VkImageAspectFlags aspects,
|
||||
Uint32 level, Uint32 layer);
|
||||
|
||||
// THE RENDER-PASS ARM: attachment 0 the multisample source (LOAD/STORE), attachment 1 the
|
||||
// single-sample resolve target (DONT_CARE/STORE), both in DEPTH_STENCIL_ATTACHMENT_OPTIMAL, one
|
||||
// subpass whose VkSubpassDescriptionDepthStencilResolve resolves every aspect of the format
|
||||
// with VK_RESOLVE_MODE_SAMPLE_ZERO_BIT. The caller begins and ends it with NO draw.
|
||||
VkResult CreateWireDepthStencilResolvePass(PFN_vkCreateRenderPass2 createRenderPass2, VkDevice device,
|
||||
VkFormat format, VkSampleCountFlagBits samples, VkRenderPass* pass);
|
||||
|
||||
// THE SHADER ARM's device objects that do not depend on the format.
|
||||
struct WireShaderResolveKit {
|
||||
VkShaderModule vertex = VK_NULL_HANDLE;
|
||||
VkShaderModule depthFragment = VK_NULL_HANDLE;
|
||||
// Only where VK_EXT_shader_stencil_export is enabled: its SPIR-V declares StencilExportEXT.
|
||||
VkShaderModule stencilFragment = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE;
|
||||
};
|
||||
// Creates every member; the first failure is returned and the members made so far stay set,
|
||||
// for DestroyWireShaderResolveKit.
|
||||
VkResult CreateWireShaderResolveKit(VkDevice device, Bool shaderStencilExport, WireShaderResolveKit& kit);
|
||||
void DestroyWireShaderResolveKit(VkDevice device, WireShaderResolveKit& kit);
|
||||
// One (format, aspect)'s single-attachment render pass: DONT_CARE/STORE on both aspects,
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL throughout.
|
||||
VkResult CreateWireShaderResolveRenderPass(VkDevice device, VkFormat format, VkRenderPass* pass);
|
||||
// Its pipeline: full-screen triangle, gl_FragDepth (ALWAYS + write) or gl_FragStencilRefARB
|
||||
// (REPLACE everywhere), viewport and scissor dynamic. VK_ERROR_FEATURE_NOT_PRESENT when the kit
|
||||
// has no fragment module for `aspect`.
|
||||
VkResult CreateWireShaderResolvePipeline(VkDevice device, const WireShaderResolveKit& kit, VkRenderPass pass,
|
||||
VkImageAspectFlags aspect, VkPipeline* pipeline);
|
||||
// Records the draw: the render area and the scissor are `rect`, the viewport is the WHOLE
|
||||
// attachment (gl_FragCoord is what the fragment stage fetches by), one triangle.
|
||||
void RecordWireShaderResolveDraw(VkCommandBuffer commandBuffer, const WireShaderResolveKit& kit,
|
||||
VkRenderPass pass, VkPipeline pipeline, VkFramebuffer framebuffer,
|
||||
VkDescriptorSet descriptorSet, VkExtent2D attachmentExtent, VkRect2D rect);
|
||||
|
||||
// What the probe runs on. The device must have been created with VK_KHR_create_renderpass2 and
|
||||
// VK_KHR_depth_stencil_resolve (or Vulkan 1.2 core), and with VK_EXT_shader_stencil_export where
|
||||
// `shaderStencilExport` says so. The queue must be otherwise idle for the probe's duration.
|
||||
struct WireDepthResolveProbeContext {
|
||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
Uint32 queueFamilyIndex = 0;
|
||||
PFN_vkCreateRenderPass2 createRenderPass2 = nullptr;
|
||||
VkResolveModeFlags depthResolveModes = 0;
|
||||
VkResolveModeFlags stencilResolveModes = 0;
|
||||
Bool shaderStencilExport = false;
|
||||
// TEST ONLY (MGITEST_MAGMA_DEPTH_RESOLVE_PROBE=elide-subject, WireDepthResolveArm.h): record
|
||||
// NO render-pass resolve, so every subject target keeps its sentinel - the Adreno 830's
|
||||
// reading - while the shader control runs as always. Everything else is the real probe.
|
||||
Bool elideSubject = false;
|
||||
};
|
||||
|
||||
// Per format the device can host as a 4x multisample depth/stencil attachment AND for which
|
||||
// ResolveWireDepthStencil would take the render-pass arm (D16, X8_D24, D32F, D24S8, D32FS8, S8):
|
||||
// - two identical 4x4 4x multisample sources (production usage), each cleared to depth 0.25 /
|
||||
// stencil 0x5A the way the wire clears (a LOAD pass with vkCmdClearAttachments);
|
||||
// - single-sample resolve targets (the production scratch usage) pre-filled with the sentinel
|
||||
// depth 0.75 / stencil 0xA5;
|
||||
// - SUBJECT: the render-pass arm's exact pass, begun and ended with no draw;
|
||||
// - CONTROL: the shader arm's exact pass on the other source, one target per aspect (stencil
|
||||
// only with VK_EXT_shader_stencil_export);
|
||||
// - every target copied aspect by aspect into a host-visible buffer and compared texel by texel.
|
||||
// One submission, a bounded fence wait (on timeout every object is leaked and the measurement
|
||||
// says so - the caller must then not destroy or idle-wait the device), teardown on every other
|
||||
// path. Never touches renderer state.
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbe(const WireDepthResolveProbeContext& context);
|
||||
|
||||
// The POST's entry: creates a throwaway device on `physicalDevice` with exactly the extensions
|
||||
// the renderer enables for the two arms, runs the probe on its graphics queue and destroys the
|
||||
// device (leaks it on a fence timeout). `renderPassArmAvailable` is false - and nothing is run -
|
||||
// when the device lacks VK_KHR_create_renderpass2 / VK_KHR_depth_stencil_resolve; the shader
|
||||
// pass is then the only arm and the probe has nothing to choose between.
|
||||
// `shaderStencilExport` reports whether VK_EXT_shader_stencil_export was available.
|
||||
WireDepthResolveProbeMeasurement RunWireDepthResolveProbeOnThrowawayDevice(
|
||||
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
Bool& renderPassArmAvailable, Bool& shaderStencilExport);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,631 +0,0 @@
|
||||
// Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) {
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const Bool composite = handle.Slot >= MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
|
||||
const auto& records = composite ? state.CompositeShaderCsos : state.ShaderCsos;
|
||||
const SizeT index = composite ? handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase : handle.Slot;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle) || index >= records.size() ||
|
||||
!records[index].Live || records[index].Gen != handle.Gen || !records[index].Archive)
|
||||
MagmaWireFatal("program-record");
|
||||
return MagmaProgramSource(handle, records[index]);
|
||||
}
|
||||
|
||||
// Through the same seam as MagmaWireFatal, and for the same reason (P7 wave 0). The family
|
||||
// stays RoleViolation: reaching the legacy buffer arm inside a Magma server is a role
|
||||
// invariant the two halves must hold, not a verb the wire failed to migrate.
|
||||
[[noreturn]] static void WireBufferLegacyFatal() {
|
||||
MG_Pipe::MGPipeSessionFail(
|
||||
MG_Pipe::MGPipeFatalFamily::RoleViolation,
|
||||
"MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)");
|
||||
}
|
||||
|
||||
static const MG_Pipe::MGPStreamOutputBegin& WireXfbSpan() {
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId);
|
||||
if (found == state.StreamOutputSpans.end()) MagmaWireFatal("xfb-span-record");
|
||||
return found->second;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::BeginWireXfbCaptureForDraw(FrameContext::FrameData& frame) {
|
||||
const auto& span = WireXfbSpan();
|
||||
const auto program = WireProgramSource(span.CaptureProgram);
|
||||
const Uint32 count = static_cast<Uint32>(std::min<SizeT>(program.GetTransformFeedbackBufferCount(), 4));
|
||||
if (!count || !program.GetTransformFeedbackVaryingCount()) return false;
|
||||
VkBuffer buffers[4]{};
|
||||
VkDeviceSize offsets[4]{};
|
||||
VkDeviceSize sizes[4]{};
|
||||
Bool hasTarget = false;
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
// A leading gl_NextBuffer leaves a hole in the binding indices. No
|
||||
// output names that zero-stride target, and GL need not bind a buffer.
|
||||
if (!program.GetTransformFeedbackStride(i)) continue;
|
||||
const auto& target = span.Targets[i];
|
||||
BufferSlice slice{};
|
||||
if (MG_Pipe::MGPipeHandleIsNull(target.Res) ||
|
||||
!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, target.Res, slice))
|
||||
MagmaWireFatal("xfb-buffer-resource");
|
||||
if (target.Offset >= slice.size) return false;
|
||||
const Uint64 size = target.Size == 0 || target.Size == MG_Pipe::kMGPipeWholeBuffer
|
||||
? slice.size - target.Offset : target.Size;
|
||||
if (!size || size > slice.size - target.Offset) return false;
|
||||
buffers[i] = slice.buffer;
|
||||
offsets[i] = slice.offset + target.Offset;
|
||||
sizes[i] = size;
|
||||
hasTarget = true;
|
||||
// The server owns the storage. Publish dirtiness through the existing
|
||||
// reverse channel so CPU readback does not reuse the client's old shadow.
|
||||
m_bufferManager.MarkWireBufferGpuWritten(target.Res, target.Offset, size);
|
||||
}
|
||||
if (!hasTarget) return false;
|
||||
return BeginXfbCaptureWithBuffers(frame, count, buffers, offsets, sizes);
|
||||
}
|
||||
|
||||
static Bool ConvertWireVertexStream(const MGPVertexAttribWire& attr,
|
||||
VertexInputStateFactory::VertexStreamConversion conversion, const Uint8* source,
|
||||
SizeT stride, SizeT count, Vector<Uint8>& output) {
|
||||
using Conversion = VertexInputStateFactory::VertexStreamConversion;
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
const SizeT elementBytes = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (conversion == Conversion::Repack) {
|
||||
output.resize(count * elementBytes);
|
||||
for (SizeT i = 0; i < count; ++i)
|
||||
Memcpy(output.data() + i * elementBytes, source + i * stride, elementBytes);
|
||||
return true;
|
||||
}
|
||||
output.resize(count * attr.Size * sizeof(Float));
|
||||
const auto convert = [&]<typename T>() {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
for (SizeT component = 0; component < attr.Size; ++component) {
|
||||
T value{};
|
||||
Memcpy(&value, source + i * stride + component * sizeof(T), sizeof(T));
|
||||
const Float converted = static_cast<Float>(value);
|
||||
Memcpy(output.data() + (i * attr.Size + component) * sizeof(Float), &converted, sizeof(Float));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
switch (type) {
|
||||
case DataType::Float64: return convert.template operator()<Double>();
|
||||
case DataType::Int8: return convert.template operator()<Int8>();
|
||||
case DataType::Uint8: return convert.template operator()<Uint8>();
|
||||
case DataType::Int16: return convert.template operator()<Int16>();
|
||||
case DataType::Uint16: return convert.template operator()<Uint16>();
|
||||
case DataType::Int32: return convert.template operator()<Int32>();
|
||||
case DataType::Uint32: return convert.template operator()<Uint32>();
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::DestroyWireDrawPass() {
|
||||
// Only shutdown/swapchain replacement calls this, after device idle.
|
||||
if (m_wireDrawPass && m_frameContext.GetFrameCount() != 0) {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
m_wireDrawPass.reset();
|
||||
for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr);
|
||||
m_wireDrawViews.clear();
|
||||
m_wireDrawPassKey = {};
|
||||
}
|
||||
|
||||
void VulkanRenderer::RetireWireObjects(WireRetiredObjects objects) {
|
||||
// The current recording is the next graphics submission. Do not use a
|
||||
// frame number: mid-frame flushes and readbacks can split one frame.
|
||||
objects.submitIndex = m_submitCounter + 1;
|
||||
m_wireRetiredObjects.push_back(Move(objects));
|
||||
}
|
||||
|
||||
void VulkanRenderer::RetireWireDrawPass() {
|
||||
if (!m_wireDrawPass) return;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
if (m_wireDrawPassCaches.size() < m_frameContext.GetFrameCount())
|
||||
m_wireDrawPassCaches.resize(m_frameContext.GetFrameCount());
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
// Bound a present-less stream of distinct framebuffer configurations.
|
||||
// Overflow retains the original submit-index retirement path.
|
||||
constexpr SizeT kMaxCachedWirePassesPerFrame = 32;
|
||||
if (cache.size() < kMaxCachedWirePassesPerFrame) {
|
||||
cache.push_back({Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews)});
|
||||
return;
|
||||
}
|
||||
WireRetiredObjects retired{};
|
||||
retired.drawPass = Move(m_wireDrawPass);
|
||||
retired.imageViews = Move(m_wireDrawViews);
|
||||
RetireWireObjects(Move(retired));
|
||||
m_wireDrawPassKey = {};
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::AcquireCachedWireDrawPass(const WireDrawPassKey& key) {
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
if (frameIndex >= m_wireDrawPassCaches.size()) return false;
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
for (auto it = cache.begin(); it != cache.end(); ++it) {
|
||||
// Exact values, not a compatibility hash: two FBOs with the same
|
||||
// formats can name different images, windows or draw-buffer slots.
|
||||
if (!(it->key == key)) continue;
|
||||
m_wireDrawPassKey = Move(it->key);
|
||||
m_wireDrawPass = Move(it->pass);
|
||||
m_wireDrawViews = Move(it->views);
|
||||
cache.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) {
|
||||
if (frameIndex >= m_wireDrawPassCaches.size()) return;
|
||||
auto& cache = m_wireDrawPassCaches[frameIndex];
|
||||
for (auto& entry : cache) {
|
||||
entry.pass.reset();
|
||||
for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr);
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearAllWireDrawPassCaches() {
|
||||
for (Uint32 frameIndex = 0; frameIndex < m_wireDrawPassCaches.size(); ++frameIndex)
|
||||
ClearWireDrawPassCache(frameIndex);
|
||||
}
|
||||
|
||||
void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) {
|
||||
SizeT retained = 0;
|
||||
for (SizeT index = 0; index < m_wireRetiredObjects.size(); ++index) {
|
||||
auto& entry = m_wireRetiredObjects[index];
|
||||
if (!all && entry.submitIndex > completedSubmit) {
|
||||
if (retained != index) m_wireRetiredObjects[retained] = Move(entry);
|
||||
++retained;
|
||||
continue;
|
||||
}
|
||||
// Framebuffers/descriptor sets stop naming their views first.
|
||||
entry.drawPass.reset();
|
||||
for (const auto pool : entry.descriptorPools) vkDestroyDescriptorPool(m_device, pool, nullptr);
|
||||
for (const auto framebuffer : entry.framebuffers) vkDestroyFramebuffer(m_device, framebuffer, nullptr);
|
||||
for (const auto pass : entry.renderPasses) vkDestroyRenderPass(m_device, pass, nullptr);
|
||||
for (const auto view : entry.imageViews) vkDestroyImageView(m_device, view, nullptr);
|
||||
}
|
||||
// Erase once: repeated front erases move the whole remaining cohort for
|
||||
// every draw retired by a fence, turning a batch retirement quadratic.
|
||||
m_wireRetiredObjects.resize(retained);
|
||||
}
|
||||
|
||||
struct WirePreparationScope {
|
||||
Uint32& depth;
|
||||
explicit WirePreparationScope(Uint32& value) : depth(value) { ++depth; }
|
||||
~WirePreparationScope() { --depth; }
|
||||
};
|
||||
|
||||
Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode,
|
||||
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) {
|
||||
auto& state = MG_Pipe::MGPipeApplier();
|
||||
const auto program = WireProgramSource(state.BoundShaderCso);
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
|
||||
return false;
|
||||
}
|
||||
if (aspects & DrawSetupAspect::IndirectDrawBuffer)
|
||||
WireBufferLegacyFatal();
|
||||
MakeXfbWritesVisible();
|
||||
const auto* fbo = state.DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("draw-framebuffer-record");
|
||||
RetireWireDrawPass();
|
||||
// CPU preparation may synchronize GPU-produced vertex/index bytes. Do
|
||||
// not rewind an arena while this draw owns as-yet-unrecorded slices.
|
||||
WirePreparationScope preparing(m_wirePreparationDepth);
|
||||
|
||||
ProgramFactory::CompileOptionFlags flags =
|
||||
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(fbo->IsDefault));
|
||||
if (m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0)
|
||||
flags |= ProgramFactory::CompileOptionBit::XfbCapture;
|
||||
const auto* programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
if (!(aspects & DrawSetupAspect::IndexBuffer) && programObject->readsBaseVertexBuiltin) {
|
||||
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
}
|
||||
if (programObject->pointSizeCapabilityUnsupported) {
|
||||
MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it");
|
||||
return false;
|
||||
}
|
||||
const auto& programObj = *programObject;
|
||||
m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined;
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(TextureResourcesUnprepared, "a sampled/storage image could not be made ready; see the Tex* tally");
|
||||
return false;
|
||||
}
|
||||
// Lazy image uploads/promotions can submit the old recording. Resolve
|
||||
// ALL attachments before descriptor binding captures the current cmd.
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
||||
const Int32 slot = fbo->DrawBuffers[i];
|
||||
if (slot >= 0) (void)ResolveWireImage(*fbo, fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
}
|
||||
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
(void)ResolveWireImage(*fbo, fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
(void)ResolveWireImage(*fbo, fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
m_textureManager->FlushPendingUploads();
|
||||
const auto vaoHandle = state.BoundVertexElements;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record");
|
||||
if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record");
|
||||
const auto& vao = state.VertexElementsCsos[vaoHandle.Slot];
|
||||
if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation");
|
||||
VertexInputStateFactory::BackendVertexInputState input{};
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT.
|
||||
//
|
||||
// A false is now ONE reason and only one - `buffer-window`, an enabled attribute the
|
||||
// program reads whose slot lies outside the window set_vertex_buffers published. That
|
||||
// is a disagreement about what crossed, not about what this device can express, so it
|
||||
// keeps a named Fatal through the hook (MGPipeSessionFail -> Session::Fail, which is
|
||||
// what rule I asks of a refusal that must stay a refusal). It is the check that caught
|
||||
// the Redmi VAO hash collision fixed in 376c04be, which is why it is not softened.
|
||||
// The P7 marker is gone from the detail string with the abort: this is a permanent
|
||||
// protocol error, not a shape the split path has yet to migrate, so it must not keep
|
||||
// counting against the census of unmigrated refusals (scripts/ci/fatal_census.py
|
||||
// counts the marker literally, which is also why no comment in this package spells it).
|
||||
if (!m_vertexInputStateFactory->BuildWireVertexInput(vao, state,
|
||||
programObj.activeVertexInputLocationMask, input))
|
||||
MagmaWireFatal("vertex-layout-buffer-window");
|
||||
// The seven device/format reasons are now MASKED OUT of the vertex input state, and
|
||||
// this is the monolith arm's pre-flight for them, statement for statement
|
||||
// (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An
|
||||
// enabled array with no usable VkFormat never reaches the vertex input state, which
|
||||
// makes it indistinguishable from a DISABLED one - and a disabled one is answered
|
||||
// below by uploading the current attribute value, so without this check the draw would
|
||||
// quietly feed the shader a constant in place of the application's vertex data.
|
||||
//
|
||||
// A DECLINE, NOT A FATAL (rule I (a)): the draw is skipped, the reason is named once,
|
||||
// and the observable is the monolith lane's for the same VAO on the same device.
|
||||
// BuildWireVertexInput only considers attributes the program reads, so a masked bit is
|
||||
// always an active one and this predicate is the whole of the difference.
|
||||
if ((input.unsupportedAttribMask & programObj.activeVertexInputLocationMask) != 0) {
|
||||
MGLOG_E_ONCE("Magma wire draw skipped: program=%u reads vertex attribute location "
|
||||
"mask 0x%x whose enabled array has no supported vertex format",
|
||||
program.GetExternalIndex(),
|
||||
input.unsupportedAttribMask & programObj.activeVertexInputLocationMask);
|
||||
WireDeclineTally::Count(WireDeclineSite::VertexFormatUnsupported);
|
||||
return false;
|
||||
}
|
||||
Vector<BufferSlice> vertexSlices(input.bindings.size());
|
||||
for (SizeT binding = 0; binding < input.bindings.size(); ++binding) {
|
||||
const Uint32 location = input.bindingAttributeLocations[binding];
|
||||
const auto& attr = vao.Attributes[location];
|
||||
const auto& resource = state.VertexBuffers[location];
|
||||
if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8");
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
|
||||
MagmaWireFatal("vertex-buffer-resource");
|
||||
const VkDeviceSize offset = input.bindingBaseOffsets[binding];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (offset > slice.size || elementSize > slice.size - offset) {
|
||||
MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's first element lies outside its buffer");
|
||||
return false;
|
||||
}
|
||||
const auto conversion = input.bindingConversions[binding];
|
||||
if (conversion != VertexInputStateFactory::VertexStreamConversion::None) {
|
||||
const SizeT stride = static_cast<SizeT>(attr.Stride);
|
||||
SizeT count = stride ? 1 + (static_cast<SizeT>(slice.size - offset) - elementSize) / stride : 1;
|
||||
// Direct non-indexed draws bound the conversion; indexed/multi-draw may
|
||||
// name arbitrary vertices, so preserve the entire valid source stream.
|
||||
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor)
|
||||
count = std::min<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
|
||||
if (!count) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> source((count - 1) * stride + elementSize);
|
||||
if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) {
|
||||
MGL_WIRE_DECLINE_AT(VertexStreamRead, "the vertex bytes to convert could not be read back");
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> converted;
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-format-conversion`: DECLINE.
|
||||
// The converter is asked only for a conversion BuildWireVertexInput already
|
||||
// chose, so a refusal here means the two disagree about what is convertible -
|
||||
// a MobileGL-internal defect, not a peer's malformed record and not a verb the
|
||||
// split path has yet to migrate. Killing the session for it told the peer
|
||||
// "protocol corruption" about a bug on this side; skipping the draw and naming
|
||||
// it once is the honest answer and is what the row asks for. The packed
|
||||
// converter that would make the refusal unreachable is the row's optional half
|
||||
// and is not taken here.
|
||||
if (!ConvertWireVertexStream(attr, conversion, source.data(), stride, count, converted)) {
|
||||
MGLOG_E_ONCE("Magma wire draw skipped: no vertex stream converter for "
|
||||
"location=%u type=%u size=%u conversion=%d",
|
||||
location, attr.Type, static_cast<Uint32>(attr.Size),
|
||||
static_cast<Int>(conversion));
|
||||
WireDeclineTally::Count(WireDeclineSite::VertexStreamConverter);
|
||||
return false;
|
||||
}
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
converted.data(), converted.size(), 16, vertexSlices[binding])) {
|
||||
MGL_WIRE_DECLINE_AT(VertexTransientUpload, "the converted vertex stream did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
slice.offset += offset;
|
||||
slice.size -= offset;
|
||||
vertexSlices[binding] = slice;
|
||||
}
|
||||
}
|
||||
|
||||
BufferSlice indexSlice{};
|
||||
VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM;
|
||||
if (aspects & DrawSetupAspect::IndexBuffer) {
|
||||
if (!indices) {
|
||||
MGL_WIRE_DECLINE_AT(IndexViewMissing, "an indexed draw arrived with no index view");
|
||||
return false;
|
||||
}
|
||||
const SizeT width = MG_Util::GetGLTypeSize(indices->indexType);
|
||||
if (width != 1 && width != 2 && width != 4) {
|
||||
MGL_WIRE_DECLINE_AT(IndexTypeWidth, "the index type is not 1, 2 or 4 bytes wide");
|
||||
return false;
|
||||
}
|
||||
indexType = width == 4 ? VK_INDEX_TYPE_UINT32 : width == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8;
|
||||
const auto& restart = MGB_CTX->GetRenderStateParameters();
|
||||
const Uint32 fixed = MG_Util::FixedRestartIndexForGLType(indices->indexType);
|
||||
const Bool replaceRestart = restart.PrimitiveRestartEnabled && !restart.PrimitiveRestartFixedIndexEnabled &&
|
||||
restart.PrimitiveRestartIndex < fixed;
|
||||
const Bool expandByte = width == 1 && !m_indexTypeUint8ExtensionEnabled;
|
||||
const Bool cpuIndices = indices->forceClientMemory || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res);
|
||||
Vector<Uint8> copied;
|
||||
const void* bytes = nullptr;
|
||||
if (cpuIndices) bytes = reinterpret_cast<const void*>(indices->indexByteOffset);
|
||||
else {
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice))
|
||||
MagmaWireFatal("index-buffer-resource");
|
||||
const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset;
|
||||
if (offset < state.IndexBuffer.Offset || offset > indexSlice.size ||
|
||||
indices->indexByteSize > indexSlice.size - offset) {
|
||||
MGL_WIRE_DECLINE_AT(IndexWindow, "the index range lies outside its buffer");
|
||||
return false;
|
||||
}
|
||||
if (replaceRestart || expandByte || offset % width) {
|
||||
copied.resize(indices->indexByteSize);
|
||||
if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) {
|
||||
MGL_WIRE_DECLINE_AT(IndexStreamRead, "the indices to rewrite could not be read back");
|
||||
return false;
|
||||
}
|
||||
bytes = copied.data();
|
||||
}
|
||||
indexSlice.offset += offset;
|
||||
indexSlice.size = indices->indexByteSize;
|
||||
}
|
||||
if (bytes) {
|
||||
Vector<Uint8> rewritten;
|
||||
SizeT uploadSize = indices->indexByteSize;
|
||||
if (replaceRestart || expandByte) {
|
||||
const SizeT outWidth = expandByte ? 2 : width;
|
||||
rewritten.resize(uploadSize / width * outWidth);
|
||||
for (SizeT i = 0; i < uploadSize / width; ++i) {
|
||||
Uint32 value = 0;
|
||||
Memcpy(&value, static_cast<const Uint8*>(bytes) + i * width, width);
|
||||
if (replaceRestart && value == restart.PrimitiveRestartIndex) value = fixed;
|
||||
if (expandByte && ResolvePrimitiveRestartEnable(aspects, indices) && value == fixed) value = 0xffffu;
|
||||
Memcpy(rewritten.data() + i * outWidth, &value, outWidth);
|
||||
}
|
||||
bytes = rewritten.data();
|
||||
uploadSize = rewritten.size();
|
||||
if (expandByte) indexType = VK_INDEX_TYPE_UINT16;
|
||||
}
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
|
||||
bytes, uploadSize, 16, indexSlice)) {
|
||||
MGL_WIRE_DECLINE_AT(IndexTransientUpload, "the rewritten indices did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!indexSlice.IsValid()) {
|
||||
MGL_WIRE_DECLINE_AT(IndexSliceInvalid, "no usable index slice was resolved");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
// Descriptor resolution can upgrade a view's root image to STORAGE usage.
|
||||
// Resolve it before constructing framebuffer views so both roles of that image
|
||||
// name the same allocation even on its first storage-image use.
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) {
|
||||
MGL_WIRE_DECLINE_AT(UniformBufferBinding, "the program's uniform/storage bindings could not be bound");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<VkAttachmentDescription> attachments;
|
||||
Vector<VkImageViewCreateInfo> viewInfos;
|
||||
Vector<VkAttachmentReference> colors;
|
||||
VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
|
||||
VkExtent2D extent{~0u, ~0u};
|
||||
Uint32 layers = ~0u;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
Uint64 passHash = 0;
|
||||
WireDrawPassKey passKey{};
|
||||
passKey.isDefault = fbo->IsDefault;
|
||||
passKey.framebufferSrgb = MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
|
||||
if (fbo->IsDefault) {
|
||||
passKey.swapchain = m_swapchainObject.GetHandle();
|
||||
// A drawing pass on the default framebuffer is what makes the freshly acquired
|
||||
// swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the
|
||||
// attachments below resolve the same index, so this key has to be taken after it.
|
||||
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
|
||||
}
|
||||
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
|
||||
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
|
||||
if (!image.image) return VK_ATTACHMENT_UNUSED;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT)
|
||||
image.format = ResolveSrgbAttachmentWriteFormat(image.format,
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
|
||||
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
|
||||
TransitionWireImage(image, layout);
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = image.image;
|
||||
info.viewType = image.viewType;
|
||||
info.format = image.format;
|
||||
info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers};
|
||||
if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
|
||||
info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
viewInfos.push_back(info);
|
||||
passKey.attachments.push_back({image.storage, static_cast<Uint32>(surface.Kind), image.image,
|
||||
image.viewType, image.format, info.subresourceRange.aspectMask, aspect,
|
||||
image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height,
|
||||
image.samples});
|
||||
VkAttachmentDescription description{};
|
||||
description.format = image.format;
|
||||
description.samples = image.samples;
|
||||
description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
description.initialLayout = description.finalLayout = layout;
|
||||
attachments.push_back(description);
|
||||
extent.width = std::min(extent.width, image.extent.width);
|
||||
extent.height = std::min(extent.height, image.extent.height);
|
||||
layers = std::min(layers, image.layers);
|
||||
samples = image.samples;
|
||||
passHash = XXH64(&description, sizeof(description), passHash);
|
||||
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
||||
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
|
||||
if (image.isDefault) {
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
|
||||
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
|
||||
}
|
||||
return static_cast<Uint32>(attachments.size() - 1);
|
||||
};
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
||||
const Int32 slot = fbo->DrawBuffers[i];
|
||||
const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL});
|
||||
}
|
||||
while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back();
|
||||
if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
if (attachments.empty()) {
|
||||
MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments");
|
||||
return false;
|
||||
}
|
||||
passKey.width = extent.width;
|
||||
passKey.height = extent.height;
|
||||
passKey.layers = layers;
|
||||
passKey.depthReference = depth.attachment;
|
||||
for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment);
|
||||
// A native handle can be recycled after image replacement or death.
|
||||
// Sample epochs after every lazy resolve, then compare them with the key.
|
||||
passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
|
||||
passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch();
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = static_cast<Uint32>(colors.size());
|
||||
subpass.pColorAttachments = colors.data();
|
||||
subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr;
|
||||
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
passInfo.attachmentCount = static_cast<Uint32>(attachments.size());
|
||||
passInfo.pAttachments = attachments.data();
|
||||
passInfo.subpassCount = 1;
|
||||
passInfo.pSubpasses = &subpass;
|
||||
if (!AcquireCachedWireDrawPass(passKey)) {
|
||||
m_wireDrawPassKey = Move(passKey);
|
||||
Vector<VkImageView> views;
|
||||
views.reserve(viewInfos.size());
|
||||
for (const auto& info : viewInfos) {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view));
|
||||
m_wireDrawViews.push_back(view);
|
||||
views.push_back(view);
|
||||
}
|
||||
m_wireDrawPass = MakeUnique<RenderPassEntry>();
|
||||
auto& created = *m_wireDrawPass;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass));
|
||||
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
fbInfo.renderPass = created.renderPass;
|
||||
fbInfo.attachmentCount = static_cast<Uint32>(views.size());
|
||||
fbInfo.pAttachments = views.data();
|
||||
fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer));
|
||||
created.attachmentCount = static_cast<Uint32>(attachments.size());
|
||||
created.colorAttachmentCount = static_cast<Uint32>(colors.size());
|
||||
created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED;
|
||||
created.sampleCount = samples;
|
||||
created.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
|
||||
created.layers = layers;
|
||||
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
|
||||
}
|
||||
auto& pass = *m_wireDrawPass;
|
||||
WireRenderPassCompatibilityKey pipelinePassKey{};
|
||||
for (const auto& attachment : attachments)
|
||||
pipelinePassKey.attachmentFormatsAndSamples.push_back(
|
||||
(static_cast<Uint64>(attachment.format) << 32) | static_cast<Uint32>(attachment.samples));
|
||||
for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment);
|
||||
pipelinePassKey.depthReference = depth.attachment;
|
||||
const Uint64 compatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey);
|
||||
const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass,
|
||||
ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw");
|
||||
return false;
|
||||
}
|
||||
for (Uint32 i = 0; i < vertexSlices.size(); ++i)
|
||||
vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset);
|
||||
Uint32 binding = static_cast<Uint32>(vertexSlices.size());
|
||||
for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) {
|
||||
if (!(programObj.activeVertexInputLocationMask & (1u << location)) ||
|
||||
(input.attributeLocationMask & (1u << location))) continue;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
const void* data = nullptr;
|
||||
VkDeviceSize size = 0;
|
||||
if (!TryGetCurrentVertexAttributeUploadPayload(MGB_CTX->GetCurrentVertexAttribute(location),
|
||||
programObj.vertexInputTypes[location], format, data, size)) {
|
||||
MGL_WIRE_DECLINE_AT(CurrentAttributePayload, "a disabled array's current attribute value has no upload payload");
|
||||
return false;
|
||||
}
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
data, size, 16, slice)) {
|
||||
MGL_WIRE_DECLINE_AT(CurrentAttributeUpload, "the current attribute value did not fit the transient arena");
|
||||
return false;
|
||||
}
|
||||
vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset);
|
||||
}
|
||||
if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType);
|
||||
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) {
|
||||
MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun");
|
||||
return false;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault,
|
||||
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
|
||||
if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
|
||||
RetireWireDrawPass();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram);
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V");
|
||||
return;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
|
||||
return;
|
||||
}
|
||||
const auto pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (!pipeline) {
|
||||
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
|
||||
return;
|
||||
}
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound");
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdDispatch(frame.commandBuffer, x, y, z);
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
#version 450
|
||||
// SAMPLE ZERO, and it is not an approximation: VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is the mode the
|
||||
// VK_KHR_depth_stencil_resolve arm asks for, and GL 4.6 core 18.3.1 says a depth resolve picks
|
||||
// a value "in an implementation-dependent manner where the result will be between the minimum
|
||||
// and maximum depth values in the pixel" - sample 0 is one of the samples, so it qualifies, and
|
||||
// taking the same sample as the extension arm is what makes the two arms interchangeable on one
|
||||
// device.
|
||||
//
|
||||
// AVERAGING WOULD NOT BE: a mean of four depths is a surface that exists nowhere, and GL's
|
||||
// bound is on the per-pixel min/max, which a mean satisfies but a shadow comparison against it
|
||||
// does not survive.
|
||||
layout(set = 0, binding = 0) uniform sampler2DMS sourceImage;
|
||||
|
||||
void main() {
|
||||
gl_FragDepth = texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r;
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// Included by VulkanRenderer.cpp inside the backend namespace, after WireDepthMipmap.inc.
|
||||
// The native multisample depth/stencil resolve owns no frontend object and never invokes the
|
||||
// GL shader compiler.
|
||||
//
|
||||
// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth
|
||||
// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment,
|
||||
// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this
|
||||
// package a device without either killed the session at
|
||||
// `Magma:multisample-depth-resolve-capability` the first time an application resolved a
|
||||
// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a
|
||||
// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both
|
||||
// arms; this is the second one.
|
||||
//
|
||||
// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the
|
||||
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
|
||||
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
|
||||
// requires a SELECTED sample for stencil anyway.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The pass's modules, layouts, render passes and pipeline, and the draw it records, are
|
||||
// WireDepthResolveProbe.cpp's (g5-msprobe): the multisample depth/stencil resolve probe runs
|
||||
// this exact arm as its CONTROL, so it is defined once for both.
|
||||
struct VulkanRenderer::WireMultisampleResolveResources {
|
||||
struct Target {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = 0; // the ONE aspect this pipeline writes
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
WireShaderResolveKit kit;
|
||||
Vector<Target> targets;
|
||||
};
|
||||
|
||||
void VulkanRenderer::DestroyWireMultisampleResolveResources() {
|
||||
if (!m_wireMultisampleResolveResources) return;
|
||||
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit descriptor
|
||||
// pools/framebuffers/views have been collected.
|
||||
auto& cache = *m_wireMultisampleResolveResources;
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
DestroyWireShaderResolveKit(m_device, cache.kit);
|
||||
delete m_wireMultisampleResolveResources;
|
||||
m_wireMultisampleResolveResources = nullptr;
|
||||
}
|
||||
|
||||
// ONE ASPECT, source -> resolved, both 2D images of the same format and extent, the source
|
||||
// multisampled and the destination not. Returns false for every shape it cannot express,
|
||||
// which the caller turns into a decline; it never ends the session.
|
||||
Bool VulkanRenderer::ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
|
||||
VkImageAspectFlags aspect, GLint sx0, GLint sy0, Uint32 width, Uint32 height) {
|
||||
if (!source.image || !resolved.image || !width || !height) return false;
|
||||
if (aspect != VK_IMAGE_ASPECT_DEPTH_BIT && aspect != VK_IMAGE_ASPECT_STENCIL_BIT) return false;
|
||||
// A fragment shader cannot write the stencil aspect at all without
|
||||
// VK_EXT_shader_stencil_export. §3.2: decline stencil-only rather than produce a level
|
||||
// whose stencil is whatever the DONT_CARE left there.
|
||||
if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && !m_wireShaderStencilExport) return false;
|
||||
if (source.samples == VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
if (source.format != resolved.format) return false;
|
||||
// SAMPLED is asked for by every attachment VkTextureManager creates; a device that
|
||||
// declined it would make the descriptor write below undefined, not failed.
|
||||
if (!(source.usageFlags & VK_IMAGE_USAGE_SAMPLED_BIT)) return false;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties);
|
||||
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
if ((properties.optimalTilingFeatures & required) != required) return false;
|
||||
|
||||
const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(source.format);
|
||||
if (!(formatAspects & aspect)) return false;
|
||||
|
||||
if (!m_wireMultisampleResolveResources) {
|
||||
m_wireMultisampleResolveResources = new WireMultisampleResolveResources;
|
||||
VK_VERIFY(CreateWireShaderResolveKit(m_device, m_wireShaderStencilExport,
|
||||
m_wireMultisampleResolveResources->kit));
|
||||
}
|
||||
auto& cache = *m_wireMultisampleResolveResources;
|
||||
const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.kit.stencilFragment
|
||||
: cache.kit.depthFragment;
|
||||
if (fragment == VK_NULL_HANDLE) return false;
|
||||
|
||||
WireMultisampleResolveResources::Target* target = nullptr;
|
||||
for (auto& entry : cache.targets)
|
||||
if (entry.format == resolved.format && entry.aspect == aspect) { target = &entry; break; }
|
||||
if (!target) {
|
||||
cache.targets.emplace_back();
|
||||
target = &cache.targets.back();
|
||||
target->format = resolved.format;
|
||||
target->aspect = aspect;
|
||||
VK_VERIFY(CreateWireShaderResolveRenderPass(m_device, resolved.format, &target->renderPass));
|
||||
if (CreateWireShaderResolvePipeline(m_device, cache.kit, target->renderPass, aspect,
|
||||
&target->pipeline) != VK_SUCCESS) {
|
||||
vkDestroyRenderPass(m_device, target->renderPass, nullptr);
|
||||
cache.targets.pop_back();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_textureManager->FlushPendingUploads();
|
||||
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
||||
|
||||
// ONE ASPECT on the sampled view: a depth/stencil image can only be SAMPLED through a
|
||||
// view that names exactly one of its aspects, which is also what makes the stencil half
|
||||
// a usampler2DMS and the depth half a sampler2DMS.
|
||||
const VkImageViewCreateInfo sourceViewInfo =
|
||||
WireDepthStencilViewInfo(source.image, source.format, aspect, source.level, source.layer);
|
||||
VkImageView sourceView = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &sourceViewInfo, nullptr, &sourceView));
|
||||
// BOTH aspects on the attachment view: a depth/stencil ATTACHMENT names every aspect
|
||||
// its format has, whichever one the pipeline writes.
|
||||
const VkImageViewCreateInfo targetViewInfo =
|
||||
WireDepthStencilViewInfo(resolved.image, resolved.format, formatAspects, 0, 0);
|
||||
VkImageView targetView = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &targetViewInfo, nullptr, &targetView));
|
||||
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &targetView;
|
||||
framebufferInfo.width = resolved.extent.width;
|
||||
framebufferInfo.height = resolved.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
||||
|
||||
// Each in-flight pass owns its descriptor set, for the colour blit's reason: updating a
|
||||
// cached set would rewrite the source view of an older queued pass.
|
||||
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = 1;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
|
||||
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
alloc.descriptorSetCount = 1;
|
||||
alloc.pSetLayouts = &cache.kit.descriptorLayout;
|
||||
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
|
||||
const VkDescriptorImageInfo sampled{cache.kit.nearest, sourceView,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
||||
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
write.dstSet = descriptorSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &sampled;
|
||||
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
RecordWireShaderResolveDraw(frame.commandBuffer, cache.kit, target->renderPass, target->pipeline, framebuffer,
|
||||
descriptorSet, resolved.extent, {{sx0, sy0}, {width, height}});
|
||||
ResetDynamicStateShadow();
|
||||
InvalidatePipelineMemo();
|
||||
// This native pass binds its own set/layout without going through the application's
|
||||
// descriptor binder. Its next fast path must rebind.
|
||||
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
|
||||
WireRetiredObjects retired{};
|
||||
retired.framebuffers.push_back(framebuffer);
|
||||
retired.imageViews = {sourceView, targetView};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,18 +0,0 @@
|
||||
#version 450
|
||||
// Native Vulkan multisample depth/stencil resolve: no GL program, registry entry, or runtime
|
||||
// transpilation.
|
||||
//
|
||||
// THIS STAGE CARRIES NOTHING, and that is the whole design. The scratch this pass writes has
|
||||
// the SAME extent and the SAME coordinate space as the multisample source it reads - both are
|
||||
// native images, neither is flipped against the other - so the fragment stage can take its
|
||||
// coordinate straight out of gl_FragCoord and needs no varying, no push constant and no rect.
|
||||
// The requested rectangle is expressed by the scissor instead, which is also what the
|
||||
// VK_KHR_depth_stencil_resolve arm next door expresses with its renderArea.
|
||||
//
|
||||
// An oversized triangle rather than WireDepthMipmap.vert's quad: there is no destination
|
||||
// rectangle to clip to here, only the scissor, and a triangle rasterizes the same area with
|
||||
// one fewer primitive and no shared edge.
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(corners[gl_VertexIndex], 0.0, 1.0);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Generated from WireMultisampleResolve.vert / WireMultisampleDepthResolve.frag /
|
||||
// WireMultisampleStencilResolve.frag by scripts/bake_internal_shaders.py, which runs MG_Test's
|
||||
// BakedInternalShadersTest with the IN-TREE glslang and splices the words it emits into the
|
||||
// arrays below. Do not edit the words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test,
|
||||
// run as a gate) recompiles every source in its table and reds on the first word that
|
||||
// disagrees.
|
||||
#pragma once
|
||||
|
||||
static constexpr Uint32 kWireMultisampleResolveVertexSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000028u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000001au, 0x00030003u,
|
||||
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x0000000bu,
|
||||
0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u,
|
||||
0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
|
||||
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu,
|
||||
0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u,
|
||||
0x0000000du, 0x00000000u, 0x00060005u, 0x0000001au, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
|
||||
0x00050005u, 0x0000001du, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030047u, 0x0000000bu, 0x00000002u,
|
||||
0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u,
|
||||
0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u,
|
||||
0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x0000001au, 0x0000000bu, 0x0000002au,
|
||||
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u,
|
||||
0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u,
|
||||
0x0004002bu, 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u,
|
||||
0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu,
|
||||
0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu,
|
||||
0x00000020u, 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u,
|
||||
0x00000006u, 0x00000002u, 0x0004002bu, 0x00000008u, 0x00000011u, 0x00000003u, 0x0004001cu, 0x00000012u,
|
||||
0x00000010u, 0x00000011u, 0x0004002bu, 0x00000006u, 0x00000013u, 0xbf800000u, 0x0005002cu, 0x00000010u,
|
||||
0x00000014u, 0x00000013u, 0x00000013u, 0x0004002bu, 0x00000006u, 0x00000015u, 0x40400000u, 0x0005002cu,
|
||||
0x00000010u, 0x00000016u, 0x00000015u, 0x00000013u, 0x0005002cu, 0x00000010u, 0x00000017u, 0x00000013u,
|
||||
0x00000015u, 0x0006002cu, 0x00000012u, 0x00000018u, 0x00000014u, 0x00000016u, 0x00000017u, 0x00040020u,
|
||||
0x00000019u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000019u, 0x0000001au, 0x00000001u, 0x00040020u,
|
||||
0x0000001cu, 0x00000007u, 0x00000012u, 0x00040020u, 0x0000001eu, 0x00000007u, 0x00000010u, 0x0004002bu,
|
||||
0x00000006u, 0x00000021u, 0x00000000u, 0x0004002bu, 0x00000006u, 0x00000022u, 0x3f800000u, 0x00040020u,
|
||||
0x00000026u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
|
||||
0x000200f8u, 0x00000005u, 0x0004003bu, 0x0000001cu, 0x0000001du, 0x00000007u, 0x0004003du, 0x0000000eu,
|
||||
0x0000001bu, 0x0000001au, 0x0003003eu, 0x0000001du, 0x00000018u, 0x00050041u, 0x0000001eu, 0x0000001fu,
|
||||
0x0000001du, 0x0000001bu, 0x0004003du, 0x00000010u, 0x00000020u, 0x0000001fu, 0x00050051u, 0x00000006u,
|
||||
0x00000023u, 0x00000020u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000024u, 0x00000020u, 0x00000001u,
|
||||
0x00070050u, 0x00000007u, 0x00000025u, 0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x00050041u,
|
||||
0x00000026u, 0x00000027u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000027u, 0x00000025u, 0x000100fdu,
|
||||
0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireMultisampleDepthResolveFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001du, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000008u, 0x00000010u, 0x00030010u,
|
||||
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u,
|
||||
0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x00000008u, 0x465f6c67u, 0x44676172u,
|
||||
0x68747065u, 0x00000000u, 0x00050005u, 0x0000000cu, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u,
|
||||
0x00000010u, 0x465f6c67u, 0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu,
|
||||
0x00000016u, 0x00040047u, 0x0000000cu, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000cu, 0x00000022u,
|
||||
0x00000000u, 0x00040047u, 0x00000010u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u,
|
||||
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040020u, 0x00000007u, 0x00000003u,
|
||||
0x00000006u, 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00090019u, 0x00000009u, 0x00000006u,
|
||||
0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x0000000au,
|
||||
0x00000009u, 0x00040020u, 0x0000000bu, 0x00000000u, 0x0000000au, 0x0004003bu, 0x0000000bu, 0x0000000cu,
|
||||
0x00000000u, 0x00040017u, 0x0000000eu, 0x00000006u, 0x00000004u, 0x00040020u, 0x0000000fu, 0x00000001u,
|
||||
0x0000000eu, 0x0004003bu, 0x0000000fu, 0x00000010u, 0x00000001u, 0x00040017u, 0x00000011u, 0x00000006u,
|
||||
0x00000002u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040017u, 0x00000015u, 0x00000014u,
|
||||
0x00000002u, 0x0004002bu, 0x00000014u, 0x00000017u, 0x00000000u, 0x00040015u, 0x0000001au, 0x00000020u,
|
||||
0x00000000u, 0x0004002bu, 0x0000001au, 0x0000001bu, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u,
|
||||
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000au, 0x0000000du, 0x0000000cu,
|
||||
0x0004003du, 0x0000000eu, 0x00000012u, 0x00000010u, 0x0007004fu, 0x00000011u, 0x00000013u, 0x00000012u,
|
||||
0x00000012u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000015u, 0x00000016u, 0x00000013u, 0x00040064u,
|
||||
0x00000009u, 0x00000018u, 0x0000000du, 0x0007005fu, 0x0000000eu, 0x00000019u, 0x00000018u, 0x00000016u,
|
||||
0x00000040u, 0x00000017u, 0x00050051u, 0x00000006u, 0x0000001cu, 0x00000019u, 0x00000000u, 0x0003003eu,
|
||||
0x00000008u, 0x0000001cu, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
static constexpr Uint32 kWireMultisampleStencilResolveFragmentSpirv[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001fu, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
|
||||
0x00001395u, 0x0009000au, 0x5f565053u, 0x5f545845u, 0x64616873u, 0x735f7265u, 0x636e6574u, 0x655f6c69u,
|
||||
0x726f7078u, 0x00000074u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u,
|
||||
0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00000008u, 0x00000012u, 0x00030010u, 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x000013a3u,
|
||||
0x00030003u, 0x00000002u, 0x000001c2u, 0x00090004u, 0x415f4c47u, 0x735f4252u, 0x65646168u, 0x74735f72u,
|
||||
0x69636e65u, 0x78655f6cu, 0x74726f70u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00080005u, 0x00000008u, 0x465f6c67u, 0x53676172u, 0x636e6574u, 0x65526c69u, 0x42524166u, 0x00000000u,
|
||||
0x00050005u, 0x0000000du, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u, 0x00000012u, 0x465f6c67u,
|
||||
0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu, 0x00001396u, 0x00040047u,
|
||||
0x0000000du, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000du, 0x00000022u, 0x00000000u, 0x00040047u,
|
||||
0x00000012u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
|
||||
0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040020u, 0x00000007u, 0x00000003u, 0x00000006u,
|
||||
0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00040015u, 0x00000009u, 0x00000020u, 0x00000000u,
|
||||
0x00090019u, 0x0000000au, 0x00000009u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u,
|
||||
0x00000000u, 0x0003001bu, 0x0000000bu, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000000u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000000u, 0x00030016u, 0x0000000fu, 0x00000020u, 0x00040017u,
|
||||
0x00000010u, 0x0000000fu, 0x00000004u, 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu,
|
||||
0x00000011u, 0x00000012u, 0x00000001u, 0x00040017u, 0x00000013u, 0x0000000fu, 0x00000002u, 0x00040017u,
|
||||
0x00000016u, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000006u, 0x00000018u, 0x00000000u, 0x00040017u,
|
||||
0x0000001au, 0x00000009u, 0x00000004u, 0x0004002bu, 0x00000009u, 0x0000001cu, 0x00000000u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000bu,
|
||||
0x0000000eu, 0x0000000du, 0x0004003du, 0x00000010u, 0x00000014u, 0x00000012u, 0x0007004fu, 0x00000013u,
|
||||
0x00000015u, 0x00000014u, 0x00000014u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000016u, 0x00000017u,
|
||||
0x00000015u, 0x00040064u, 0x0000000au, 0x00000019u, 0x0000000eu, 0x0007005fu, 0x0000001au, 0x0000001bu,
|
||||
0x00000019u, 0x00000017u, 0x00000040u, 0x00000018u, 0x00050051u, 0x00000009u, 0x0000001du, 0x0000001bu,
|
||||
0x00000000u, 0x0004007cu, 0x00000006u, 0x0000001eu, 0x0000001du, 0x0003003eu, 0x00000008u, 0x0000001eu,
|
||||
0x000100fdu, 0x00010038u,
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
#version 450
|
||||
// The stencil half, and it needs an extension to exist at all: a fragment shader cannot write
|
||||
// the stencil aspect without VK_EXT_shader_stencil_export (SPV_EXT_shader_stencil_export, which
|
||||
// this `#extension` line is how glslang is asked for). CONTRACT-P7 §3.2 says so and says what
|
||||
// happens otherwise - a stencil-only resolve DECLINES on a device without it, because there is
|
||||
// no second way to put a value in the stencil aspect from a pass.
|
||||
//
|
||||
// usampler2DMS and not sampler2DMS: the stencil aspect of a depth/stencil image is an unsigned
|
||||
// integer, and a float view of it would round the index.
|
||||
//
|
||||
// Sample zero, for WireMultisampleDepthResolve.frag's reason, and GL is stricter here: 18.3.1
|
||||
// says integer and stencil resolves SELECT one sample rather than combining them, so sample 0
|
||||
// is the answer and not merely an acceptable one.
|
||||
#extension GL_ARB_shader_stencil_export : require
|
||||
layout(set = 0, binding = 0) uniform usampler2DMS sourceImage;
|
||||
|
||||
void main() {
|
||||
gl_FragStencilRefARB = int(texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r);
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// UniformManager-owned native descriptors for wire bindings with no image.
|
||||
// No GL object is constructed or inserted into the client's resource namespace.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void UniformManager::DestroyWirePlaceholderImage(WirePlaceholderImage& image) const {
|
||||
if (m_device != VK_NULL_HANDLE) {
|
||||
if (image.clearFramebuffer) vkDestroyFramebuffer(m_device, image.clearFramebuffer, nullptr);
|
||||
if (image.clearPass) vkDestroyRenderPass(m_device, image.clearPass, nullptr);
|
||||
if (image.sampler) vkDestroySampler(m_device, image.sampler, nullptr);
|
||||
if (image.view) vkDestroyImageView(m_device, image.view, nullptr);
|
||||
if (image.image) vkDestroyImage(m_device, image.image, nullptr);
|
||||
if (image.memory) vkFreeMemory(m_device, image.memory, nullptr);
|
||||
}
|
||||
image = {};
|
||||
}
|
||||
|
||||
void UniformManager::SetWireInvalidStorageImageArm(Bool deviceNullDescriptor) {
|
||||
const char* forced = std::getenv("MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER");
|
||||
const Bool forcePrivate = forced != nullptr && std::strcmp(forced, "1") == 0;
|
||||
m_wireInvalidStorageImagesBindNull = deviceNullDescriptor && !forcePrivate;
|
||||
MGLOG_I("Magma wire: an invalid or unbound image unit binds %s (VK_EXT_robustness2 nullDescriptor %s%s)",
|
||||
m_wireInvalidStorageImagesBindNull ? "a NULL storage descriptor"
|
||||
: "a placeholder private to its unit",
|
||||
deviceNullDescriptor ? "enabled" : "unavailable",
|
||||
deviceNullDescriptor && forcePrivate ? "; MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER=1" : "");
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveWirePlaceholderImage(VkCommandBuffer commands, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
|
||||
VkDescriptorImageInfo& out, VkFormat boundStorageFormat, Uint32 unit) const {
|
||||
// GL 4.6 core 8.26, codex closeout finding 2: every access through an invalid image unit
|
||||
// is invalid - a load returns zero, a store and an atomic are discarded. A NULL storage
|
||||
// descriptor (VK_EXT_robustness2 nullDescriptor) is exactly that, for any target, format
|
||||
// and sample count, so nothing below is needed.
|
||||
if (storage && m_wireInvalidStorageImagesBindNull) {
|
||||
out = {VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL};
|
||||
return true;
|
||||
}
|
||||
if (binding >= programObj.samplerTextureTargetByBinding.size() ||
|
||||
binding >= programObj.samplerNumericDomainByBinding.size()) return false;
|
||||
const auto target = programObj.samplerTextureTargetByBinding[binding];
|
||||
const auto domain = programObj.samplerNumericDomainByBinding[binding];
|
||||
const Bool multisample = target == TextureTarget::Texture2DMultisample ||
|
||||
target == TextureTarget::Texture2DMultisampleArray;
|
||||
Bool shadow = false;
|
||||
if (!storage) {
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
switch (program.GetUniformType(static_cast<Uint>(location))) {
|
||||
case GL_SAMPLER_1D_SHADOW: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_CUBE_SHADOW:
|
||||
case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW:
|
||||
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW: case GL_SAMPLER_2D_RECT_SHADOW: shadow = true; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
if (storage) {
|
||||
if (binding >= programObj.storageImageFormatByBinding.size()) return false;
|
||||
format = programObj.storageImageFormatByBinding[binding];
|
||||
// A formatless declaration of an INVALID binding (a unit whose texture lacks the
|
||||
// named level or layer) takes the format the application bound, where the device can
|
||||
// store to it and it is in the shader's numeric class. The R32 default expands a load
|
||||
// to (0, 0, 0, 1); a four-channel carrier cleared to zero reads (0, 0, 0, 0), which
|
||||
// is what KHR-GL46.shader_image_load_store.incomplete_textures compares against.
|
||||
if (format == VK_FORMAT_UNDEFINED && boundStorageFormat != VK_FORMAT_UNDEFINED) {
|
||||
const Bool classMatches = domain == SamplerNumericDomain::Float
|
||||
? !vkuFormatIsUINT(boundStorageFormat) && !vkuFormatIsSINT(boundStorageFormat)
|
||||
: domain == SamplerNumericDomain::SignedInteger ? vkuFormatIsSINT(boundStorageFormat)
|
||||
: domain == SamplerNumericDomain::UnsignedInteger && vkuFormatIsUINT(boundStorageFormat);
|
||||
VkFormatProperties properties{};
|
||||
if (classMatches) vkGetPhysicalDeviceFormatProperties(m_physicalDevice, boundStorageFormat, &properties);
|
||||
if (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) format = boundStorageFormat;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED) format = PlaceholderFormatForNumericDomain(domain);
|
||||
} else if (shadow) format = VK_FORMAT_D16_UNORM;
|
||||
else if (domain == SamplerNumericDomain::Float) format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
else if (domain == SamplerNumericDomain::SignedInteger) format = VK_FORMAT_R8G8B8A8_SINT;
|
||||
else if (domain == SamplerNumericDomain::UnsignedInteger) format = VK_FORMAT_R8G8B8A8_UINT;
|
||||
if (format == VK_FORMAT_UNDEFINED) return false;
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkImageCreateFlags flags = 0;
|
||||
Uint32 layers = 1;
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D; break;
|
||||
case TextureTarget::Texture1DArray: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY; break;
|
||||
case TextureTarget::Texture2D: case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample: break;
|
||||
case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisampleArray:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; break;
|
||||
case TextureTarget::Texture3D: imageType = VK_IMAGE_TYPE_3D; viewType = VK_IMAGE_VIEW_TYPE_3D; break;
|
||||
case TextureTarget::TextureCubeMap: case TextureTarget::TextureCubeMapArray:
|
||||
flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; layers = 6;
|
||||
viewType = target == TextureTarget::TextureCubeMap ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
break;
|
||||
default: return false;
|
||||
}
|
||||
// A STORAGE placeholder is private to its unit (per shape). Shared by shape alone, a store
|
||||
// through invalid unit A was visible, after a barrier, to a load through invalid unit B of
|
||||
// the same shape - one Vulkan image behind both (codex closeout finding 2). The clear
|
||||
// below still runs before each use; what a private image cannot hide is the SAME unit's
|
||||
// own store to its own later load inside one pass (CONTRACT-P7 §12). Not per binding: GL
|
||||
// aliases per unit, and a (binding, unit) key only multiplied allocations. Past
|
||||
// kWirePrivateStoragePlaceholderCap a new (shape, unit) shares its shape's (WirePlaceholderKey.h).
|
||||
const Uint64 shape = static_cast<Uint32>(format) | (static_cast<Uint64>(target) << 32) |
|
||||
(static_cast<Uint64>(storage) << 48) | (static_cast<Uint64>(shadow) << 49);
|
||||
const WirePlaceholderKey key = ChooseWirePlaceholderKey(shape, storage, unit, m_wirePrivateStoragePlaceholders,
|
||||
[this](const WirePlaceholderKey& candidate) { return m_wirePlaceholderImages.count(candidate) != 0; });
|
||||
if (storage && unit != WirePlaceholderKey::kShared && key.unit == WirePlaceholderKey::kShared) {
|
||||
MGLOG_W_ONCE("Magma wire: %zu image-unit-private placeholders exist (the cap); a further invalid or "
|
||||
"unbound unit shares its shape's placeholder, so two such units of one shape can alias",
|
||||
static_cast<size_t>(m_wirePrivateStoragePlaceholders));
|
||||
}
|
||||
auto found = m_wirePlaceholderImages.find(key);
|
||||
if (found == m_wirePlaceholderImages.end()) {
|
||||
WirePlaceholderImage image{};
|
||||
image.format = format; image.layers = layers;
|
||||
image.aspect = shadow ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
const auto fail = [&] {
|
||||
MGLOG_E_ONCE("Magma wire placeholder allocation failed: target=%d format=%d storage=%d multisample=%d",
|
||||
static_cast<Int>(target), static_cast<Int>(format), static_cast<Int>(storage), static_cast<Int>(multisample));
|
||||
DestroyWirePlaceholderImage(image);
|
||||
return false;
|
||||
};
|
||||
const VkImageUsageFlags usage = (storage ? VK_IMAGE_USAGE_STORAGE_BIT : VK_IMAGE_USAGE_SAMPLED_BIT) |
|
||||
(multisample ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : VK_IMAGE_USAGE_TRANSFER_DST_BIT);
|
||||
VkImageFormatProperties properties{};
|
||||
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, imageType,
|
||||
VK_IMAGE_TILING_OPTIMAL, usage, flags, &properties) != VK_SUCCESS) return fail();
|
||||
if (multisample) {
|
||||
// MS=1 in SPIR-V cannot name a single-sampled image, even when
|
||||
// the shader's runtime branch does not fetch that descriptor.
|
||||
image.samples = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM;
|
||||
for (const auto sample : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT,
|
||||
VK_SAMPLE_COUNT_16_BIT, VK_SAMPLE_COUNT_32_BIT, VK_SAMPLE_COUNT_64_BIT}) {
|
||||
if (properties.sampleCounts & sample) { image.samples = sample; break; }
|
||||
}
|
||||
if (image.samples == VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM) return fail();
|
||||
}
|
||||
VkImageCreateInfo create{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
create.flags = flags; create.imageType = imageType; create.format = format;
|
||||
create.extent = {1, 1, 1}; create.mipLevels = 1; create.arrayLayers = layers;
|
||||
create.samples = image.samples; create.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
create.usage = usage; create.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (vkCreateImage(m_device, &create, nullptr, &image.image) != VK_SUCCESS) return fail();
|
||||
VkMemoryRequirements requirements{};
|
||||
vkGetImageMemoryRequirements(m_device, image.image, &requirements);
|
||||
VkPhysicalDeviceMemoryProperties memoryProperties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memoryProperties);
|
||||
Uint32 memoryType = ~0u;
|
||||
for (Uint32 i = 0; i < memoryProperties.memoryTypeCount; ++i) {
|
||||
if (!(requirements.memoryTypeBits & (1u << i))) continue;
|
||||
if (memoryType == ~0u) memoryType = i;
|
||||
if (memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
|
||||
memoryType = i; break;
|
||||
}
|
||||
}
|
||||
if (memoryType == ~0u) return fail();
|
||||
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = memoryType;
|
||||
if (vkAllocateMemory(m_device, &allocate, nullptr, &image.memory) != VK_SUCCESS ||
|
||||
vkBindImageMemory(m_device, image.image, image.memory, 0) != VK_SUCCESS) return fail();
|
||||
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view.image = image.image; view.viewType = viewType; view.format = format;
|
||||
view.subresourceRange = {image.aspect, 0, 1, 0, layers};
|
||||
if (vkCreateImageView(m_device, &view, nullptr, &image.view) != VK_SUCCESS) return fail();
|
||||
if (!storage) {
|
||||
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
|
||||
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sampler.compareEnable = shadow; sampler.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||
sampler.maxLod = 0.0f;
|
||||
if (vkCreateSampler(m_device, &sampler, nullptr, &image.sampler) != VK_SUCCESS) return fail();
|
||||
}
|
||||
if (multisample) {
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format; attachment.samples = image.samples;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color;
|
||||
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
pass.attachmentCount = 1; pass.pAttachments = &attachment;
|
||||
pass.subpassCount = 1; pass.pSubpasses = &subpass;
|
||||
VkSubpassDependency dependencies[2]{};
|
||||
dependencies[0] = {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0};
|
||||
dependencies[1] = {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0};
|
||||
pass.dependencyCount = 2; pass.pDependencies = dependencies;
|
||||
if (vkCreateRenderPass(m_device, &pass, nullptr, &image.clearPass) != VK_SUCCESS) return fail();
|
||||
VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebuffer.renderPass = image.clearPass; framebuffer.attachmentCount = 1;
|
||||
framebuffer.pAttachments = &image.view; framebuffer.width = framebuffer.height = 1; framebuffer.layers = layers;
|
||||
if (vkCreateFramebuffer(m_device, &framebuffer, nullptr, &image.clearFramebuffer) != VK_SUCCESS) return fail();
|
||||
}
|
||||
found = m_wirePlaceholderImages.emplace(key, image).first;
|
||||
if (key.unit != WirePlaceholderKey::kShared) ++m_wirePrivateStoragePlaceholders;
|
||||
}
|
||||
auto& image = found->second;
|
||||
// Sampled incomplete textures remain immutable opaque black. A storage
|
||||
// placeholder is reset before each use, so a prior discarded store cannot
|
||||
// leak into a later pass's loads through the same unit.
|
||||
if (storage || image.layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
VkClearValue clear{};
|
||||
if (!storage) {
|
||||
if (domain == SamplerNumericDomain::SignedInteger) clear.color.int32[3] = 1;
|
||||
else if (domain == SamplerNumericDomain::UnsignedInteger) clear.color.uint32[3] = 1;
|
||||
else clear.color.float32[3] = 1.0f;
|
||||
}
|
||||
if (multisample) {
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = image.clearPass; begin.framebuffer = image.clearFramebuffer;
|
||||
begin.renderArea.extent = {1, 1}; begin.clearValueCount = 1; begin.pClearValues = &clear;
|
||||
vkCmdBeginRenderPass(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(commands);
|
||||
image.layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
} else {
|
||||
if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, image.aspect)) return false;
|
||||
const VkImageSubresourceRange range{image.aspect, 0, 1, 0, image.layers};
|
||||
if (shadow) {
|
||||
const VkClearDepthStencilValue depth{0.0f, 0};
|
||||
vkCmdClearDepthStencilImage(commands, image.image, image.layout, &depth, 1, &range);
|
||||
} else vkCmdClearColorImage(commands, image.image, image.layout, &clear.color, 1, &range);
|
||||
if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
image.aspect)) return false;
|
||||
}
|
||||
VkMemoryBarrier visible{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
visible.srcAccessMask = multisample ? VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
visible.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commands, multisample ? VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &visible, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
out = {image.sampler, image.view, VK_IMAGE_LAYOUT_GENERAL};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1,62 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderKey.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>
|
||||
|
||||
// The pure half of UniformManager's wire placeholder cache (WirePlaceholderImages.inc): WHICH
|
||||
// placeholder a binding with no usable image takes. A unit test pins it
|
||||
// (MG_Test/Pipeline/PipelineQuirkTest.cpp); the Vulkan half that makes the image stays in the .inc.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The placeholder's SHAPE (format, target, storage, shadow) and, for a STORAGE placeholder on
|
||||
// the arm without a null descriptor, the image UNIT it is private to. GL aliases per UNIT, so the
|
||||
// unit is the whole of the no-alias guarantee (codex closeout finding 2): two bindings that name
|
||||
// one unit are one GL image, and keying them apart as well only multiplied allocations (cf-magma
|
||||
// critic). A sampled placeholder is never written, so it stays shared: its unit is kShared.
|
||||
struct WirePlaceholderKey {
|
||||
static constexpr Uint32 kShared = ~0u;
|
||||
Uint64 shape = 0;
|
||||
Uint32 unit = kShared;
|
||||
Bool operator==(const WirePlaceholderKey& other) const { return shape == other.shape && unit == other.unit; }
|
||||
};
|
||||
|
||||
struct WirePlaceholderKeyHash {
|
||||
SizeT operator()(const WirePlaceholderKey& key) const {
|
||||
Uint64 h = key.shape * 0x9E3779B97F4A7C15ull;
|
||||
h ^= static_cast<Uint64>(key.unit) + 0x9E3779B97F4A7C15ull + (h << 6) + (h >> 2);
|
||||
return static_cast<SizeT>(h);
|
||||
}
|
||||
};
|
||||
|
||||
// Every private placeholder is its own vkCreateImage + vkAllocateMemory, freed only at Shutdown.
|
||||
// (shape, unit) bounds them by shapes x kMGPipeMaxImageUnits, and a peer rotating formats and
|
||||
// targets across the units could still walk that towards maxMemoryAllocationCount (4096 is the
|
||||
// spec minimum). So past this many, a NEW (shape, unit) takes its shape's SHARED placeholder - the
|
||||
// pre-fix aliasing, logged once by the caller - instead of another allocation; a unit that already
|
||||
// has its private placeholder keeps it. Two per unit is far past any real program.
|
||||
inline constexpr SizeT kWirePrivateStoragePlaceholderCap = 2 * static_cast<SizeT>(MG_Pipe::kMGPipeMaxImageUnits);
|
||||
|
||||
// The key a placeholder of `shape` is found or made under. `privateStorage` = a storage binding on
|
||||
// the arm without a null descriptor (with one, the caller never gets here); `unit` = its image
|
||||
// unit; `privateCount` = how many private placeholders exist; `known(key)` = the cache holds `key`.
|
||||
template <typename KnownFn>
|
||||
WirePlaceholderKey ChooseWirePlaceholderKey(Uint64 shape, Bool privateStorage, Uint32 unit, SizeT privateCount,
|
||||
KnownFn&& known) {
|
||||
WirePlaceholderKey key;
|
||||
key.shape = shape;
|
||||
if (!privateStorage || unit == WirePlaceholderKey::kShared) return key;
|
||||
key.unit = unit;
|
||||
if (privateCount < kWirePrivateStoragePlaceholderCap || known(key)) return key;
|
||||
key.unit = WirePlaceholderKey::kShared;
|
||||
return key;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
@@ -1,49 +0,0 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The wire draw pass has one graphics subpass, no input attachments,
|
||||
// multiview or extensions. Its Vulkan pipeline compatibility is formats,
|
||||
// sample counts and the ordered attachment references, not native handles,
|
||||
// image identity, dimensions, load/store operations or image layouts.
|
||||
struct WireRenderPassCompatibilityKey {
|
||||
Vector<Uint64> attachmentFormatsAndSamples;
|
||||
Vector<Uint32> colorReferences;
|
||||
Uint32 depthReference = VK_ATTACHMENT_UNUSED;
|
||||
Bool operator==(const WireRenderPassCompatibilityKey&) const = default;
|
||||
};
|
||||
|
||||
struct WireRenderPassCompatibilityHash {
|
||||
SizeT operator()(const WireRenderPassCompatibilityKey& key) const {
|
||||
SizeT hash = 0;
|
||||
const auto mix = [&](Uint64 value) {
|
||||
hash ^= static_cast<SizeT>(value) + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2);
|
||||
};
|
||||
mix(key.attachmentFormatsAndSamples.size());
|
||||
for (const auto value : key.attachmentFormatsAndSamples) mix(value);
|
||||
mix(key.colorReferences.size());
|
||||
for (const auto value : key.colorReferences) mix(value);
|
||||
mix(key.depthReference);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
class WireRenderPassCompatibilityTable {
|
||||
public:
|
||||
Uint64 Intern(const WireRenderPassCompatibilityKey& key) {
|
||||
if (const auto found = m_ids.find(key); found != m_ids.end()) return found->second;
|
||||
// No reset/eviction: an ID cannot be reused while a pipeline factory
|
||||
// or a renderer memo can still hold its previous meaning.
|
||||
MOBILEGL_ASSERT(m_nextId != 0, "wire render-pass compatibility ID exhausted");
|
||||
const Uint64 id = m_nextId++;
|
||||
m_ids.emplace(key, id);
|
||||
return id;
|
||||
}
|
||||
private:
|
||||
UnorderedMap<WireRenderPassCompatibilityKey, Uint64, WireRenderPassCompatibilityHash> m_ids;
|
||||
Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -1,214 +0,0 @@
|
||||
// Server-owned texture readback. Included after WireFramebuffer.inc so image
|
||||
// layout tracking and frame retirement are the same as every other wire verb.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
|
||||
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
|
||||
Vector<Uint8>& result) {
|
||||
using namespace DirectGLES::ReadbackImpl;
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(info.Format, mapping)) return false;
|
||||
const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format);
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type);
|
||||
const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D;
|
||||
if (!info.Box.W || !rows64 || rows64 > std::numeric_limits<GLsizei>::max() ||
|
||||
rows64 > std::numeric_limits<SizeT>::max() / 16 / info.Box.W) return false;
|
||||
const SizeT pixels = static_cast<SizeT>(info.Box.W) * static_cast<SizeT>(rows64);
|
||||
if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize ||
|
||||
info.DstSize != pixels * pixelBytes) return false;
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
||||
if (pixelBytes != sourcePixelSize) return false;
|
||||
result.assign(source, source + sourceBytes);
|
||||
return true;
|
||||
}
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
const GLsizei rows = static_cast<GLsizei>(Uint64(info.Box.H) * info.Box.D);
|
||||
if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) ||
|
||||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
|
||||
if (ResolveTextureFormatInfo(logicalFormat).expandRgbToRgba) {
|
||||
const Float floatOne = 1.0f;
|
||||
const Uint32 integerOne = 1;
|
||||
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
||||
for (SizeT i = 0; i < pixels; ++i)
|
||||
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
||||
}
|
||||
result.resize(info.DstSize);
|
||||
for (GLsizei row = 0; row < rows; ++row)
|
||||
ConvertWideReadbackRow(wide.data() + row * static_cast<SizeT>(info.Box.W) * 16,
|
||||
result.data() + row * static_cast<SizeT>(info.Box.W) * pixelBytes,
|
||||
info.Box.W, wideType, mapping, info.Type);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info,
|
||||
Vector<Uint8>& ownedBytes) {
|
||||
ownedBytes.clear();
|
||||
const auto& box = info.Box;
|
||||
if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D ||
|
||||
box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
||||
Uint64(box.H) * box.D > static_cast<Uint64>(std::numeric_limits<GLsizei>::max())) return false;
|
||||
const auto& state = MG_Pipe::MGPipeApplier();
|
||||
if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false;
|
||||
const auto& record = state.TextureResources[info.Res.Slot];
|
||||
if (!record.Live || record.Gen != info.Res.Gen) return false;
|
||||
|
||||
using Target = MG_Pipe::MGPipeResourceTarget;
|
||||
const auto target = static_cast<Target>(record.Desc.Target);
|
||||
const Bool oneDArray = target == Target::Tex1DArray;
|
||||
const Bool volume = target == Target::Tex3D;
|
||||
Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1;
|
||||
VkExtent3D extent{box.W, box.H, 1};
|
||||
if (oneDArray) {
|
||||
if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false;
|
||||
layerCount = box.H; extent.height = 1;
|
||||
} else if (volume) {
|
||||
if (info.Target != GL_TEXTURE_3D) return false;
|
||||
extent.depth = box.D;
|
||||
} else if (target == Target::Tex2DArray || target == Target::TexCubeArray) {
|
||||
if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY)) return false;
|
||||
layerCount = box.D;
|
||||
} else if (target == Target::TexCube) {
|
||||
if (info.Target == GL_TEXTURE_CUBE_MAP) {
|
||||
if (box.D != 6) return false;
|
||||
layerCount = 6;
|
||||
} else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
|
||||
if (box.D != 1) return false;
|
||||
layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X;
|
||||
} else return false;
|
||||
} else {
|
||||
if (box.D != 1) return false;
|
||||
const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D :
|
||||
target == Target::Tex2D ? GL_TEXTURE_2D :
|
||||
target == Target::TexRect ? GL_TEXTURE_RECTANGLE : GL_NONE;
|
||||
if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false;
|
||||
}
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers);
|
||||
if (MG_Pipe::MGPipeHandleIsNull(storage)) return false;
|
||||
const Uint64 pixels64 = Uint64(box.W) * box.H * box.D;
|
||||
if (pixels64 > std::numeric_limits<SizeT>::max() / 16) return false;
|
||||
const auto readUnbacked = [&]() {
|
||||
// Only a level the VkImage cannot represent may use its owned,
|
||||
// non-GPU-dirty staging bytes. A failed GPU read never falls back.
|
||||
if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false;
|
||||
Vector<Uint8> raw;
|
||||
VkFormat nativeFormat = VK_FORMAT_UNDEFINED;
|
||||
if (!m_textureManager->ReadUnbackedWireLevel(storage,
|
||||
MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
|
||||
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
|
||||
nativeFormat, raw)) return false;
|
||||
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
|
||||
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
|
||||
};
|
||||
RetireWireDrawPass();
|
||||
MakeXfbWritesVisible();
|
||||
auto* resource = m_textureManager->SyncTextureResourceByHandle(storage);
|
||||
if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked();
|
||||
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
||||
const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers;
|
||||
if (extent.width != std::max(resource->extent.width >> level, 1u) ||
|
||||
extent.height != std::max(resource->extent.height >> level, 1u) ||
|
||||
layer >= nativeDepth || (nativeVolume ? extent.depth : layerCount) > nativeDepth - layer ||
|
||||
(!nativeVolume && layerCount > availableLayers)) return readUnbacked();
|
||||
// A view can reinterpret the same native words. Decode using the outer
|
||||
// view format, while Vulkan copies the storage allocation's bytes.
|
||||
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
|
||||
const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL;
|
||||
const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL;
|
||||
const Bool depthStencil = depth || stencil;
|
||||
const VkImageAspectFlags aspects = depthStencil ?
|
||||
((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)) : VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
if ((resource->aspect & aspects) != aspects) return false;
|
||||
const SizeT pixels = static_cast<SizeT>(pixels64);
|
||||
const SizeT destinationPixelSize = MG_Util::GetInputBytesPerPixel(
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
|
||||
if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false;
|
||||
SizeT sourcePixelSize = GetReadbackTexelSize(resource->format);
|
||||
if (depthStencil) {
|
||||
switch (resource->format) {
|
||||
case VK_FORMAT_D16_UNORM: sourcePixelSize = 2; break;
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_X8_D24_UNORM_PACK32:
|
||||
case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: sourcePixelSize = 4; break;
|
||||
case VK_FORMAT_S8_UINT: sourcePixelSize = 0; break;
|
||||
default: return false;
|
||||
}
|
||||
} else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format)) return false;
|
||||
const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize);
|
||||
const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3};
|
||||
const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes;
|
||||
if (!copyBytes) return false;
|
||||
VkBufferObject readback;
|
||||
if (!readback.Create({.allocator = m_allocator, .size = copyBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) return false;
|
||||
WireImage image{};
|
||||
image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect;
|
||||
image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels;
|
||||
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
|
||||
TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
VkBufferImageCopy regions[2]{};
|
||||
Uint32 regionCount = 0;
|
||||
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
|
||||
auto& copy = regions[regionCount++];
|
||||
copy.bufferOffset = offset;
|
||||
copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount};
|
||||
copy.imageOffset = {0, 0, nativeVolume ? static_cast<Int32>(layer) : 0};
|
||||
copy.imageExtent = extent;
|
||||
};
|
||||
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
||||
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
||||
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
readback.GetHandle(), regionCount, regions);
|
||||
TransitionWireImage(image, restore);
|
||||
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &host, 0, nullptr, 0, nullptr);
|
||||
if (!SubmitReadbackCommandsAndWait(frame)) return false;
|
||||
const auto* mapped = static_cast<const Uint8*>(readback.Map());
|
||||
if (!mapped || !readback.Invalidate(copyBytes)) return false;
|
||||
Vector<Uint8> result;
|
||||
if (depthStencil) {
|
||||
if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr,
|
||||
resource->format, pixels, info.Format, info.Type, result)) return false;
|
||||
} else {
|
||||
using namespace DirectGLES::ReadbackImpl;
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(info.Format, mapping) ||
|
||||
GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false;
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
||||
if (destinationPixelSize != sourcePixelSize) return false;
|
||||
result.assign(mapped, mapped + firstBytes);
|
||||
} else {
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
const GLsizei rows = static_cast<GLsizei>(Uint64(box.H) * box.D);
|
||||
if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) ||
|
||||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
|
||||
if (ResolveTextureFormatInfo(static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba) {
|
||||
const Float floatOne = 1.0f;
|
||||
const Uint32 integerOne = 1;
|
||||
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
||||
for (SizeT i = 0; i < pixels; ++i)
|
||||
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
||||
}
|
||||
result.resize(pixels * destinationPixelSize);
|
||||
const SizeT srcRow = static_cast<SizeT>(box.W) * 4 * sizeof(Uint32);
|
||||
const SizeT dstRow = static_cast<SizeT>(box.W) * destinationPixelSize;
|
||||
for (GLsizei row = 0; row < rows; ++row)
|
||||
ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow,
|
||||
box.W, wideType, mapping, info.Type);
|
||||
}
|
||||
}
|
||||
if (result.size() != info.DstSize) return false;
|
||||
ownedBytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -22,6 +22,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 CacheVersion = MG_Config::CacheVersion;
|
||||
Uint32 SurfaceWidth = 1;
|
||||
Uint32 SurfaceHeight = 1;
|
||||
// The app's eglSwapInterval; empty until it asks. Window renderers only - see
|
||||
// VulkanRenderer::SetSwapInterval.
|
||||
Optional<Int> SwapInterval;
|
||||
Bool DisablePipelineCache = false;
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
Bool EnableValidationLayers = true;
|
||||
|
||||
@@ -11,28 +11,6 @@
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/FatalFunnel.h> // InstallPipeSessionFailHook, installed at step 0 below
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// [p5/v1-joint PREVIEW EDIT - the merge-time form of v1's hook, c1-v1 8.2's exact order.] The
|
||||
// weak CreateRemoteBackendObject placeholder v1 shipped for a c1-less tree is DELETED at the
|
||||
// merge: the integration test links the STATIC MobileGL_s archive, and an archive member is
|
||||
// only pulled to satisfy an UNDEFINED reference - the weak definition in ServerLoop.cpp.o
|
||||
// already satisfied it, so BackendObject_Remote.cpp.o was never linked at all and the joint
|
||||
// build aborted in the placeholder (~/w7/p5-v1-joint-preflight.log). Direct construction
|
||||
// needs no factory and no weak symbol.
|
||||
#include <MG_Remote/Client/BackendObject_Remote.h>
|
||||
#endif
|
||||
|
||||
#include "ServerRole.h"
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
void LogBackendInfo() {
|
||||
if (!pActiveBackendObject) {
|
||||
@@ -67,372 +45,9 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace {
|
||||
// WHICH MGPipe SUBSYSTEMS THIS SERVER HAS A CONSUMER FOR - CallMask bits 32..47 (R-8 /
|
||||
// C-4). It is stated from what the server's own backend IS, and NOT derived from
|
||||
// MGPipeGetResourceOps(): that is a PROCESS-WIDE global, so under inproc a derivation
|
||||
// would answer with whatever the client half of the same process registered and under
|
||||
// spawn it would collapse to P2's 0x7f. Either way CapsMirror::ServerConsumes would
|
||||
// then answer a client-side liveness gate with a guess, the client would stop emitting
|
||||
// whole record families, clear its dirty flags on acceptance anyway, and the lane would
|
||||
// go green with the uploads lost - ID-39's 66 lost uploads, reflected (ServerSession.h).
|
||||
//
|
||||
// DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's
|
||||
// 9..12): it registers the resource op table in Initialize()
|
||||
// (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs.
|
||||
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
|
||||
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
|
||||
// the same fact ObjectSubsystemControlScenario already pins from the client side.
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
|
||||
//
|
||||
// kCapRunAheadApply says "this server applies an unbarriered record without reading
|
||||
// client memory". That is only true once EVERY per-draw family reads records instead
|
||||
// of the frontend - vi, sb, pg, tx2 and fb all land before it is - so the caps arm
|
||||
// below is gated on this constant, which the P5e INTEGRATION COMMIT flips to true
|
||||
// after the last of them. Until then every P5e package lands with the wait rule, the
|
||||
// static WaitClass column and the barriered predicate compiled and INERT: the client
|
||||
// never latches RunAheadArmed, so it runs today's lockstep path byte for byte.
|
||||
//
|
||||
// It is a constant and not a knob on purpose. An operator cannot turn a half-migrated
|
||||
// server into a run-ahead one, because the failure mode is not a slow frame - it is
|
||||
// the apply thread reading client memory that has already moved, which renders wrong
|
||||
// rather than aborting.
|
||||
constexpr Bool kMGPipeP5eRunAheadReady = true;
|
||||
constexpr Bool kMGPipeMagmaRunAheadReady = true;
|
||||
|
||||
// The Magma transport now owns buffer stores and consumes their vertex,
|
||||
// index and shader binding records. Both backends publish the resource
|
||||
// family only alongside a real op table (checked below). This does not
|
||||
// itself establish run-ahead readiness: that has its separate gate below.
|
||||
Uint64 ConsumedSubsystemsFor(BackendType type) {
|
||||
switch (type) {
|
||||
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e;
|
||||
case BackendType::DirectVulkan:
|
||||
return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a | MG_Pipe::kMGPipeSubsystemBufferBindings;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// THE CROSS-CHECK THAT MAKES A WRONG ANSWER LOUD. Claiming bit 7 while no resource op
|
||||
// table is registered is the exact failure the mask exists to prevent, one level down:
|
||||
// the client would keep emitting the resource family and the server would drop every
|
||||
// record of it. The check runs AFTER Initialize(), which is where DirectGLES registers
|
||||
// the table, so it can see the real answer rather than a promise.
|
||||
void AssertConsumerMaskIsHonest(Uint64 mask) {
|
||||
const Bool claimsResources = (mask & MG_Pipe::kMGPipeSubsystemResources) != 0;
|
||||
const Bool hasResourceOps = MG_Pipe::MGPipeGetResourceOps() != nullptr;
|
||||
if (claimsResources && !hasResourceOps) {
|
||||
MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"kMGPipeSubsystemResources\"} - the "
|
||||
"server published a consumer bit for the resource family while "
|
||||
"MGPipeGetResourceOps() is null. The client's R-8 liveness gate would "
|
||||
"keep emitting resource_create / resource_subdata records that this "
|
||||
"server drops on the floor, and the client clears its dirty flags on "
|
||||
"acceptance anyway (ID-39). A mask is a statement about this backend, "
|
||||
"not a hope");
|
||||
std::abort();
|
||||
}
|
||||
if (!claimsResources && hasResourceOps) {
|
||||
// The safe direction: the legacy pull path keeps running. Said out loud anyway,
|
||||
// because it silently costs the whole P3a family its migration.
|
||||
MGLOG_W("MG_Remote server: a resource op table is registered but the consumer "
|
||||
"mask withholds kMGPipeSubsystemResources; the buffer family will fall "
|
||||
"back to the legacy path for this session");
|
||||
}
|
||||
}
|
||||
|
||||
// The resource op table the SERVER's backend registered at step 1 (BackendObject_DirectGLES::
|
||||
// Initialize -> RegisterBufferBackendOps), as step 2 saw it. Step 5 compares against it
|
||||
// (review v2 N-8): a client object that registered a table of its own would have made
|
||||
// AssertConsumerMaskIsHonest's "is a table registered" answer TRUE, so re-asking that
|
||||
// question could never notice the swap - only the pointer can.
|
||||
const MG_Pipe::MGPipeResourceOps* g_resourceOpsAtStep2 = nullptr;
|
||||
|
||||
// The single hook (ARCHITECTURE.md:29). Returns false when the split could not be
|
||||
// brought up, and the caller then REFUSES TO CONTINUE rather than falling back to the
|
||||
// switch below - a fallback here is "the split lane ran monolith and went green".
|
||||
// Steps 1 and 2 of the split bring-up: the server role's private backend
|
||||
// object and the two CallMask halves. EXTRACTED so the spawn server
|
||||
// process runs the SAME code the inproc server role runs - a second copy
|
||||
// is how the two roles end up disagreeing about a capability bit that
|
||||
// only one of them ever computes. ServerMain calls this and then does
|
||||
// its own Accept; InitSplitRoles calls it and then starts the client.
|
||||
Bool InitServerRoleCommon() {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
|
||||
// 0. the Magma wire funnels' route to Session::Fail (P7 wave 0). FIRST, before
|
||||
// CreateBackend can reach a wire arm: a hook installed after the first death it
|
||||
// was meant to catch is a hook that does nothing on the only run that mattered.
|
||||
// Here rather than in ServerMain because BOTH server roles pass through this
|
||||
// function - the inproc role and the spawn/TCP session child - and a per-transport
|
||||
// install is how one of the two ends up without it.
|
||||
InstallPipeSessionFailHook();
|
||||
|
||||
// 1. the SERVER role's private backend object, on the app thread, with no GL and no
|
||||
// EGL. The context is created and made current later, on mgl-srv-apply, when the
|
||||
// client's first eglMakeCurrent crosses as a blocking control request.
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
const MobileGLResult created = loop.CreateBackend(MG_Config::ActiveBackendType);
|
||||
if (created != MOBILEGL_OK) return false;
|
||||
|
||||
// 2. the two CallMask halves. NEITHER HAS A DEFAULT and CallMask() is a named Fatal
|
||||
// on an unset one (s1's BLOCKER fix), so this is the "somebody" that block names.
|
||||
Server::ServerSession& session = Server::ServerSessionInstance();
|
||||
const Uint64 consumed = ConsumedSubsystemsFor(MG_Config::ActiveBackendType);
|
||||
AssertConsumerMaskIsHonest(consumed);
|
||||
g_resourceOpsAtStep2 = MG_Pipe::MGPipeGetResourceOps();
|
||||
session.SetConsumedSubsystems(consumed);
|
||||
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
|
||||
// every optional capability bit belongs to the package that owns its question, and
|
||||
// withholding one leaves the legacy path running, which is the safe direction.
|
||||
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
||||
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
||||
// what keeps every one of them out of the first IPC frame (contract table 0).
|
||||
//
|
||||
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
|
||||
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
|
||||
// SERVER's backend own the transform-feedback capture", and the server's table is
|
||||
// visible here and nowhere on the client. It is read straight off the table
|
||||
// ServerLoop::CreateBackend just built - Espryt registers XfbImpl::EndTransformFeedback
|
||||
// (BackendObject_DirectGLES.cpp:1458) and Magma registers no XFB slot at all - so the
|
||||
// bit is a statement about THIS backend rather than about a build option, which is
|
||||
// what makes it survive a backend switch. The client reads it through
|
||||
// MGL_BACKEND_SLOT_CAP at GL_Drawing.cpp's FixupGsStripCaptureOrder.
|
||||
Uint64 capBits = 0;
|
||||
if (const MG_Backend::BackendObject* serverBackend = loop.Backend();
|
||||
serverBackend != nullptr &&
|
||||
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
|
||||
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
|
||||
}
|
||||
if (const auto* backend = loop.Backend()) {
|
||||
const auto& gl = backend->GetBackendFunctions().GL;
|
||||
// The query owner publishes only complete native query/reply paths.
|
||||
// Timer hardware support is refreshed when caps are published after
|
||||
// make-current; no CPU primitive-accounting preference is advertised.
|
||||
if (gl.IsQueryResultAvailable && gl.GetQueryResult64 && gl.DeleteBackendQuery &&
|
||||
gl.FenceSync && gl.ClientWaitSync && gl.DeleteSync) {
|
||||
if (gl.BeginXfbPrimitivesQuery && gl.EndXfbPrimitivesQuery)
|
||||
capBits |= MG_Pipe::kCapXfbPrimitivesQuery;
|
||||
if (gl.BeginOcclusionQuery && gl.EndOcclusionQuery)
|
||||
capBits |= MG_Pipe::kCapOcclusionQuery;
|
||||
if (gl.IsTimerQuerySupported && gl.BeginTimeElapsedQuery &&
|
||||
gl.EndTimeElapsedQuery && gl.QueryCounterTimestamp)
|
||||
capBits |= MG_Pipe::kCapTimerQuery;
|
||||
}
|
||||
}
|
||||
// P7 wave 2 package C, OQ-10 (CONTRACT-P7 §5.4): kCapResidentSubData, PUBLISHED
|
||||
// OFF THE SERVER'S OWN WIRE RESOURCE TABLE AND NOTHING ELSE.
|
||||
//
|
||||
// The question the bit answers is the one BufferObject::LandBytesIntoResidentStore
|
||||
// asks before it chooses between emitting `buffer_subdata_resident` (opcode 49) and
|
||||
// falling back to the ordered in-place host memcpy: "does the side that will APPLY
|
||||
// this record implement the resident arm at all?" Under a transport the client has
|
||||
// no op table to probe - MGPipeResourceOpsHaveSubDataResident says so in as many
|
||||
// words (MG_Impl/Pipe/PipeFill.cpp) and reads this bit instead - and until now
|
||||
// nothing ever set it, so the answer was permanently "no" and BOTH backends fell
|
||||
// back to the memcpy while both of them in fact register a SubDataResident arm
|
||||
// (DirectGLES Managers.cpp's g_glesResourceOps, Magma VkBufferManager.cpp's
|
||||
// g_vulkanWireResourceOps). Magma's implementation was dead code on the wire.
|
||||
//
|
||||
// BY THE TABLE, NEVER BY THE BACKEND ENUM, and that is ID-39's lesson rather than a
|
||||
// style preference: a mask is a statement about THIS backend's registered table, so
|
||||
// a bit derived from `ActiveBackendType == ...` would keep claiming the capability
|
||||
// for a build, a flavour or a future backend whose table does not carry the arm -
|
||||
// which is exactly how 66 uploads were lost to a consumer bit that nobody could
|
||||
// have honoured. `g_resourceOpsAtStep2` is the pointer this function just pinned,
|
||||
// and step 5 already refuses a run where that pointer was swapped underneath us, so
|
||||
// reading the arm off it means the published bit and the table that has to answer
|
||||
// for it cannot come apart.
|
||||
if (g_resourceOpsAtStep2 != nullptr && g_resourceOpsAtStep2->SubDataResident != nullptr) {
|
||||
capBits |= MG_Pipe::kCapResidentSubData;
|
||||
}
|
||||
// Each backend has an independent implementation-readiness gate.
|
||||
// The runtime RunAhead knob can decline the feature, never create it.
|
||||
const Bool runAheadReady = MG_Config::ActiveBackendType == BackendType::DirectVulkan
|
||||
? kMGPipeMagmaRunAheadReady : kMGPipeP5eRunAheadReady;
|
||||
capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType, runAheadReady);
|
||||
session.SetCapabilityBits(capBits);
|
||||
session.SetBackend(loop.Backend());
|
||||
return true;
|
||||
}
|
||||
|
||||
// The single hook (ARCHITECTURE.md:29) for the INPROC shape: both roles in
|
||||
// this process. The spawn shape runs InitServerRoleCommon in the child and
|
||||
// the client half here.
|
||||
Bool InitSplitRoles() {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
|
||||
// SPAWN: THE SERVER ROLE IS NOT IN THIS PROCESS. Steps 1 and 2 -
|
||||
// the backend and the two CallMask halves - happen in the server,
|
||||
// which ran InitServerRoleForSpawn before it accepted us. Running
|
||||
// them here as well would build a second BackendObject in the one
|
||||
// process that must not have one, and would publish a capability
|
||||
// mask computed from the WRONG backend's function table.
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Spawn) {
|
||||
const MobileGLResult started = Client::ClientSessionInstance().StartSpawned();
|
||||
if (started != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote: the spawn session failed to start (rc=%d); MobileGL will "
|
||||
"NOT fall back to monolith - a lane named split that ran monolith is "
|
||||
"the one failure this phase is built to make impossible",
|
||||
static_cast<int>(started));
|
||||
return false;
|
||||
}
|
||||
pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!InitServerRoleCommon()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. the handshake, the four segments, and - at its end - the apply thread.
|
||||
const MobileGLResult started =
|
||||
Client::ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint);
|
||||
if (started != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote: the split session failed to start (rc=%d); MobileGL will not "
|
||||
"fall back to monolith - a lane named split that ran monolith is the one "
|
||||
"failure this phase is built to make impossible",
|
||||
static_cast<int>(started));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. and only now the CLIENT's backend object in the one global that holds it.
|
||||
// Table 3: pActiveBackendObject holds BackendObject_Remote and the server's
|
||||
// BackendObject_DirectGLES stays private to ServerLoop.
|
||||
pActiveBackendObject = MakeUnique<MG_Remote::Client::BackendObject_Remote>();
|
||||
return true;
|
||||
}
|
||||
|
||||
// F1 (P7 wave 2). THE WHOLE SPLIT ARM OF Init(), AS ONE BODY, so it can run from
|
||||
// either of the two places that may reach it - MobileGL::Initialize's early hook,
|
||||
// before MG_State::Init(), and Init() itself for a direct caller such as a unit
|
||||
// fixture - without a second copy. Two copies of a bring-up order is how the two
|
||||
// orders come to differ on the day one of them is wrong.
|
||||
Bool g_splitBackendBroughtUp = false;
|
||||
|
||||
void InitSplitBackend() {
|
||||
g_splitBackendBroughtUp = true;
|
||||
if (!InitSplitRoles()) {
|
||||
// NOT a fallback to the switch. pActiveBackendObject stays null and the next GL
|
||||
// call fails loudly, which is the only honest outcome: the operator asked for a
|
||||
// transport this process could not bring up.
|
||||
pActiveBackendObject = nullptr;
|
||||
return;
|
||||
}
|
||||
Bool remoteResult = InitSpecificBackendLibs();
|
||||
if (!remoteResult) {
|
||||
MGLOG_W("Failed to initialize MobileGL backend libraries for the remote object");
|
||||
return;
|
||||
}
|
||||
// P6: BOTH CHECKS BELOW ARE STATEMENTS ABOUT THE SERVER'S PROCESS, and under
|
||||
// spawn that is not this one. Their own wording says so - "a mask is a
|
||||
// statement about THIS BACKEND", "the table the SERVER'S BACKEND registered at
|
||||
// step 1" - and in a spawn client there is no backend and never was a table, so
|
||||
// MGPipeGetResourceOps() is null by construction and the first check fires on a
|
||||
// correct session. It did, on the first spawn retrace ever run.
|
||||
//
|
||||
// The checks are NOT weakened: the server runs the step-2 one itself, inside
|
||||
// InitServerRoleForSpawn, against its own freshly registered table - which is the
|
||||
// process where the question has an answer. What is lost under spawn is the
|
||||
// step-5 re-run's ability to catch a table REPLACED between accept and here, and
|
||||
// that is a question about one address space; it cannot be asked across two.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// m-6, re-worded per review v2 N-8. The honesty cross-check runs a SECOND time, now
|
||||
// that step 4's pActiveBackendObject (the client's BackendObject_Remote) exists and
|
||||
// its Initialize() has run inside InitSpecificBackendLibs. What the re-run CAN catch
|
||||
// is a table that was REMOVED between step 2 and here (the claim would then be a lie
|
||||
// again). What it cannot catch - and its first comment claimed it could - is a client
|
||||
// object that REGISTERED a table of its own: that leaves "is a table registered"
|
||||
// true. Only the pointer tells those apart, so the table is compared against the one
|
||||
// step 2 saw and a swap is refused by name: the applier would otherwise dispatch the
|
||||
// server's resource records into the CLIENT object's table under its feet.
|
||||
AssertConsumerMaskIsHonest(ConsumedSubsystemsFor(MG_Config::ActiveBackendType));
|
||||
if (MG_Pipe::MGPipeGetResourceOps() != g_resourceOpsAtStep2) {
|
||||
MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"resource ops table replaced\"} - the "
|
||||
"resource op table MGPipeGetResourceOps() answers with is not the one the "
|
||||
"server's backend registered at step 1 (%p now, %p then). Something between "
|
||||
"ServerSession::Accept and the client object's Initialize() registered its "
|
||||
"own table, and the applier would dispatch every resource record into it. A "
|
||||
"mask is a statement about the server's backend, and so is the table",
|
||||
static_cast<const void*>(MG_Pipe::MGPipeGetResourceOps()),
|
||||
static_cast<const void*>(g_resourceOpsAtStep2));
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
LogBackendInfo();
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// F1 (P7 wave 2). See BackendObjects.h for why this exists and MobileGL/Init.cpp for the
|
||||
// defect the order closes.
|
||||
Bool InitSplitRolesBeforeState() {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
|
||||
InitSplitBackend();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void ShutdownSplitRoles() {
|
||||
g_splitBackendBroughtUp = false;
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// ClientSession::Stop IS table 3's whole order and it is idempotent: publish and wait
|
||||
// for the server to drain (bounded - a lost record must be a red lane, not a hung
|
||||
// exit), Doorbell::Kill through the transport's Shutdown, ServerLoop::Stop's bounded
|
||||
// join - which also destroys the server's private BackendObject ON the apply thread
|
||||
// while it still owns the context - the transport, and only THEN anything an emitter
|
||||
// owns. A var-tail still named by an unapplied record is a use-after-free the join is
|
||||
// what prevents, which is why the order is not a preference.
|
||||
MG_Remote::Client::ClientSessionInstance().Stop();
|
||||
// M-6: ClientSession::Stop's !m_started arm (a Start that FAILED after
|
||||
// ServerSession::Accept - a refused Accept, an invalid cmd/reply ring) tears down only
|
||||
// the client half and never stops the apply thread or drops the server's private backend,
|
||||
// which ServerLoop::CreateBackend already built and which holds the process-wide
|
||||
// g_resourceOps. So call ServerLoop::Stop() here unconditionally. It is idempotent: on the
|
||||
// started path ClientSession::Stop already joined the thread, so this hits Stop's
|
||||
// !joinable arm, which resets a backend that never ran a thread and is otherwise a no-op.
|
||||
// Without this an early Start failure leaves BackendObject_DirectGLES permanently alive
|
||||
// and every later split bring-up in the process fails at CreateBackend's m_backend!=null
|
||||
// guard.
|
||||
MG_Remote::Server::ServerLoopInstance().Stop();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The spawn server's entry into the SAME bring-up the inproc server role
|
||||
// runs (ServerRole.h). A thin forwarder on purpose: the body stays in the
|
||||
// anonymous namespace beside InitSplitRoles so the two cannot drift.
|
||||
//
|
||||
// THE WHOLE FUNCTION IS INSIDE THE GUARD, not just its body. A version with
|
||||
// the guard inside still DEFINES the symbol in a pull build, and G1 caught
|
||||
// it: 2 symbols added, 1 removed, .text +16 bytes. The pull build's identity
|
||||
// is byte-for-byte, and "it returns false there" is not the same as "it is
|
||||
// not there".
|
||||
Bool InitServerRoleForSpawn() { return InitServerRoleCommon(); }
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Initializing MobileGL Backend...");
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE SINGLE HOOK. In a build without MOBILEGL_BUILD_DISAGGREGATED, MG_Config::Transport
|
||||
// is a `constexpr Monolith` (Config.h) and this whole statement is discarded, so the
|
||||
// pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// F1: MobileGL::Initialize now brings the split roles up BEFORE MG_State::Init(),
|
||||
// through InitSplitRolesBeforeState() below, so by the time Init() is reached on
|
||||
// that path the work is done. A DIRECT caller - a unit fixture that calls
|
||||
// MG_Backend::Init() itself - still gets the whole bring-up here. The flag rather
|
||||
// than `pActiveBackendObject == nullptr`, because a bring-up that FAILED leaves
|
||||
// that pointer null too and retrying it would refuse at CreateBackend's
|
||||
// m_backend != nullptr guard with a second, misleading line.
|
||||
if (!g_splitBackendBroughtUp) InitSplitBackend();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (MG_Config::ActiveBackendType) {
|
||||
case BackendType::DirectGLES:
|
||||
pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>();
|
||||
|
||||
@@ -1,715 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.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
|
||||
|
||||
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
|
||||
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
|
||||
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
|
||||
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
const char* MGPipeVerbName(MGPipeVerb verb) {
|
||||
const auto index = static_cast<SizeT>(verb);
|
||||
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
|
||||
}
|
||||
|
||||
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
|
||||
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
|
||||
}
|
||||
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
|
||||
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ================================================================================
|
||||
// P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict
|
||||
// ================================================================================
|
||||
namespace {
|
||||
Uint64 g_residualPulls = 0;
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// ---- P7 wave 3 (V1): negative control B's SERVER half ----------------------------
|
||||
//
|
||||
// MOBILEGL_PIPE_POISON_OMIT withholds the STAMP of one (verb, field) pair, and until a
|
||||
// verify build first ran on the split arm it was only ever read by the client's filler
|
||||
// (MG_Impl/Pipe/PipeFill.cpp's ParsePoisonOmissionKnob). On the split arm that is not
|
||||
// where the stamp the backend reads comes from: the stamp below re-stamps every field
|
||||
// the verb's class can answer out of applied records, so it restamped the very field
|
||||
// the client had withheld and the control went GREEN - measured, both backends, the
|
||||
// child's glGenerateMipmap returned and the process exited 0 with
|
||||
// GenerateMipmap:GetActiveTextureUnit omitted. The knob means "this verb's stamp of this
|
||||
// field was never written", and under a transport the stamp that answers the backend is
|
||||
// this one, so this one honours it too.
|
||||
//
|
||||
// PARSED HERE, NOT SHARED WITH THE CLIENT'S PARSER, because this file is in the server
|
||||
// library and MG_Impl is not. A malformed value is the client's Fatal{PipeVerifyBadKnob}
|
||||
// (it parses first, at the first fill of the process); this side simply does not arm.
|
||||
// VERIFY BUILDS ONLY: the knob belongs to the verify harness, and a push build's stamp
|
||||
// stays the branch-free fill the P5d profile asked for.
|
||||
struct ServerPoisonOmission {
|
||||
Bool Parsed = false;
|
||||
String Value;
|
||||
Bool Armed = false;
|
||||
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
|
||||
MGPipeInputField Field = MGPipeInputField::kFieldCount;
|
||||
};
|
||||
ServerPoisonOmission g_serverOmission;
|
||||
|
||||
const ServerPoisonOmission& ServerOmission() {
|
||||
const String& knob = MG_Config::Features.PipePoisonOmit;
|
||||
if (g_serverOmission.Parsed && knob.size() == g_serverOmission.Value.size() &&
|
||||
(knob.empty() || knob == g_serverOmission.Value)) {
|
||||
return g_serverOmission;
|
||||
}
|
||||
g_serverOmission = ServerPoisonOmission{};
|
||||
g_serverOmission.Parsed = true;
|
||||
g_serverOmission.Value = knob;
|
||||
const auto colon = knob.find(':');
|
||||
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) return g_serverOmission;
|
||||
const auto verb = MGPipeFindVerb(knob.substr(0, colon).c_str());
|
||||
const auto field = MGPipeFindInputField(knob.substr(colon + 1).c_str());
|
||||
if (!verb || !field) return g_serverOmission;
|
||||
g_serverOmission.Armed = true;
|
||||
g_serverOmission.Verb = *verb;
|
||||
g_serverOmission.Field = *field;
|
||||
return g_serverOmission;
|
||||
}
|
||||
|
||||
Bool ServerOmitsStamp(MGPipeVerb verb, MGPipeInputField field) {
|
||||
const ServerPoisonOmission& omission = ServerOmission();
|
||||
return omission.Armed && omission.Verb == verb && omission.Field == field;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
|
||||
// The strict arm of R-7.3. Same first line as the ordinary poison Fatal, so every
|
||||
// existing filter on Fatal{UnmigratedPipeInput still matches, plus the class and the
|
||||
// phase that retires it - a strict abort that did not say which phase owes the answer
|
||||
// would leave the reader exactly where the gate found them.
|
||||
// P5e (ra): `why` is the second half of the marker, because the lane's allowlist step
|
||||
// reads this line and the two reasons mean different things to it. "MOBILEGL_IPC_
|
||||
// STRICT_ERRORS=1" is the operator asking for a real, ordered value to be loud;
|
||||
// "UNBARRIERED" is the value not existing (§3.3). The `<field>@<verb>` prefix and the
|
||||
// Fatal{UnmigratedPipeInput tag are unchanged, so every filter written since P5c still
|
||||
// matches.
|
||||
[[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb,
|
||||
const char* why) {
|
||||
const SizeT index = static_cast<SizeT>(field);
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, %s, "
|
||||
"retires in %s]",
|
||||
kMGPipeInputFieldNames[index], MGPipeVerbName(verb), why,
|
||||
kMGPipeFieldRetiringPhase[index]);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// P5e (gl), ID-117: THE ADMITTED MARKER, AND WHY IT IS ONLY THE TAG THAT CHANGED.
|
||||
//
|
||||
// `<field>@<verb>` and the `[BARRIER-PULLED, …, retires in <phase>]` tail are IDENTICAL
|
||||
// to the Fatal above; the leading tag is `Admitted{` instead of `Fatal{`. Every filter
|
||||
// written since P5c matches `Fatal{UnmigratedPipeInput`, so all of them keep meaning
|
||||
// exactly "red" and none of them has to learn a new grammar to keep meaning it. A reader
|
||||
// grepping for the pair still finds it, and the lane can ratchet on both sets with one
|
||||
// regex per tag (ID-119's two-sided ratchet).
|
||||
//
|
||||
// THE DEDUPE IS NOT TIDINESS. An 852-draw Minecraft frame reaches an admitted readback
|
||||
// row once per draw; without this, one frame writes 852 identical lines, the log file is
|
||||
// the size of the run and the marker census cannot be read at all. So it is once per
|
||||
// (field, verb) per process, which is the granularity the allowlist is written in.
|
||||
//
|
||||
// A PLAIN ARRAY, NOT AN ATOMIC, and that is the file's existing rule rather than a
|
||||
// shortcut: CountBarrierPull is reachable only after the server stamped a verb boundary,
|
||||
// which happens on the apply thread inside PipeApplier::ApplyOne, and g_residualPulls
|
||||
// beside it is a plain Uint64 for the same reason. A racing writer here would at worst
|
||||
// log a duplicate line, never lose one.
|
||||
void AdmittedBarrierPullOnce(MGPipeInputField field, MGPipeVerb verb, Bool escalated) {
|
||||
const SizeT fieldIndex = static_cast<SizeT>(field);
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (fieldIndex >= kMGPipeInputFieldCount || verbIndex >= kMGPipeVerbCount) return;
|
||||
static Uint64 seen[(kMGPipeVerbCount * kMGPipeInputFieldCount + 63) / 64] = {};
|
||||
const SizeT bit = verbIndex * kMGPipeInputFieldCount + fieldIndex;
|
||||
const Uint64 mask = Uint64{1} << (bit % 64);
|
||||
if ((seen[bit / 64] & mask) != 0) return;
|
||||
seen[bit / 64] |= mask;
|
||||
// P5e (gl), ID-128: WHICH DISJUNCT ADMITTED IT, in the slot the grammar already has
|
||||
// for the reason. `ADMITTED` means the generated table said so, and the lane can
|
||||
// check that against `--print-admitted`. `ADMITTED-ESCALATED` means the table did
|
||||
// NOT and the record was barriered by an escalation the table cannot see, which is a
|
||||
// RUNTIME fact about that record's payload - so the lane must not look for it in a
|
||||
// static list. Saying which is what keeps the lane's comparison exact instead of
|
||||
// widening the list with every pair that could ever escalate.
|
||||
MGLOG_W("MGPipe: Admitted{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, %s, "
|
||||
"retires in %s]",
|
||||
kMGPipeInputFieldNames[fieldIndex], MGPipeVerbName(verb),
|
||||
escalated ? "ADMITTED-ESCALATED" : "ADMITTED",
|
||||
kMGPipeFieldRetiringPhase[fieldIndex]);
|
||||
}
|
||||
|
||||
// One place decides what a BARRIER-PULLED read does, so the field accessors and the
|
||||
// seven sticky forwards cannot drift apart on it.
|
||||
//
|
||||
// ---- P5e (ra), CONTRACT-P5E §3.3: THE DETECTOR IS UNCONDITIONAL UNDER AN UNBARRIERED
|
||||
// RECORD, AND THAT IS WHAT MAKES THE STRICT LANE A GATE -------------------------------
|
||||
//
|
||||
// The knob exists because under LOCKSTEP a pulled row is a real, ordered, fresh value:
|
||||
// the client filled it and then parked, so "count it and carry on" is an honest
|
||||
// measurement of remaining debt and MOBILEGL_IPC_STRICT_ERRORS is the operator asking
|
||||
// for the debt to be loud instead. None of that survives run-ahead. With the client
|
||||
// running ahead of this apply, the row was either never filled for this verb (§3.1
|
||||
// skips the fill) or is being overwritten by a verb two frames later - so the value is
|
||||
// torn or stale BY CONSTRUCTION and there is nothing for a counter to count. A
|
||||
// "count it" arm here would be a wrong picture with a number beside it.
|
||||
//
|
||||
// P5e (gl), ID-119: THE OLD PIN HERE - "rsp is 0 on unbarriered records" - WAS VACUOUS
|
||||
// AND HAS BEEN WITHDRAWN. The unbarriered arm below is [[noreturn]] and runs BEFORE
|
||||
// ++g_residualPulls, so an unbarriered pull can never reach the counter whatever this
|
||||
// function is written to do; the sentence was true of every possible implementation and
|
||||
// therefore checked nothing. What rsp actually counts is BARRIERED pulls, and under the
|
||||
// strict knob it counts exactly the ADMITTED ones, because every other barriered pull
|
||||
// aborts two lines down. That is the form CONTRACT-P5E §7 now states and the lane checks.
|
||||
//
|
||||
// ---- P5e (gl), ID-117: AN ADMITTED PULL IS LOUD, NOT FATAL ---------------------------
|
||||
//
|
||||
// Before this, strict aborted on EVERY barrier-pulled read, admitted or not - which made
|
||||
// the CI's allowlist comparison unreachable code (the run's rc != 0 exits first) and made
|
||||
// "hard green" impossible with the knob as written. So there is a third state, and the
|
||||
// two existing ones are untouched:
|
||||
//
|
||||
// unbarriered -> Fatal, no knob. The value is torn by construction.
|
||||
// barriered, unadmitted -> Fatal under strict. A debt no phase has taken.
|
||||
// barriered, admitted -> ONE MGLOG_W per (field, verb), and the entry completes.
|
||||
//
|
||||
// "Admitted" has three disjuncts and the third is a runtime one (ID-128): the generated
|
||||
// table answers the first two, and the record's own escalation flag answers the third.
|
||||
//
|
||||
// An admitted pull is a debt this phase deliberately leaves standing: the field's row is
|
||||
// BARRIER_PULLED, the verb's op is statically barriered so the client really is parked
|
||||
// behind the record, and the field is inside the verb's own may-read mask - so the value
|
||||
// is real, ordered and fresh, exactly ruling 4's "barriered records keep P5C semantics".
|
||||
// MGPipeBarrierPullAdmitted is generated from those three tables (ID-116); there is no
|
||||
// list here to drift.
|
||||
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
|
||||
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: UNDER THE DUAL-BLOCK REHEARSAL EVERY
|
||||
// BARRIER-PULLED READ IS A NAMED FATAL, unconditionally - ahead of the barriered
|
||||
// question, ahead of the strict knob, ahead of the counter. The server block has no
|
||||
// residual fill to answer from, so the value the read would return is not "torn"
|
||||
// or "stale" but ABSENT: the field's stamp was withdrawn by the server's own verb
|
||||
// boundary and nothing on this side of the role split ever writes it. Letting the
|
||||
// read proceed would hand the backend default storage - or, for the O-class rows
|
||||
// with no null check, an empty SharedPtr to dereference - which is an UNNAMED
|
||||
// crash exactly where the rehearsal exists to produce a named one. The red is the
|
||||
// census (Harness/dualblock-expected-fatals.txt), so the marker keeps the strict
|
||||
// grammar with the knob's own name as the reason.
|
||||
// D10: rides MGPipeBlocksAreDistinct() - under spawn the two blocks are distinct
|
||||
// whatever the rehearsal knob says, and a barrier pull across a real process
|
||||
// boundary is exactly the thing this Fatal exists to name.
|
||||
if (MGPipeBlocksAreDistinct()) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_ROLE_SPLIT_STATE=1");
|
||||
}
|
||||
if (!MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
|
||||
}
|
||||
++g_residualPulls;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
|
||||
}
|
||||
if (MG_Config::Ipc.StrictErrors) {
|
||||
// THE THIRD DISJUNCT (P5e gl, ID-128), and it has to be asked at RUNTIME because
|
||||
// it is a fact about this record's payload rather than about its opcode. The
|
||||
// static table admits a pair when the verb's op waits (disjunct 1) or when the
|
||||
// field's retiring phase is not this phase (disjunct 2); an escalated record is
|
||||
// one the client parks behind for a reason only the payload knows - an open
|
||||
// transform-feedback span, or a draw carrying client vertex arrays. Both are
|
||||
// outside this phase by ruling (§5.7, ID-82), and the client-array read site
|
||||
// aborts by its own name if it is ever applied unbarriered, so the pull is legal
|
||||
// and P5e is not the phase that owes it.
|
||||
const Bool statically = MGPipeBarrierPullAdmitted(field, verb);
|
||||
const Bool escalated = MGPipeApplierCurrentRecordIsBarrieredByEscalation();
|
||||
if (!statically && !escalated) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_STRICT_ERRORS=1");
|
||||
}
|
||||
AdmittedBarrierPullOnce(field, verb, !statically);
|
||||
}
|
||||
}
|
||||
|
||||
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
|
||||
// respects it for the same reason the client's residual fill does: a field outside the
|
||||
// verb's class is one the fill never copied, so answering it out of gPipeInputs would
|
||||
// hand the server the PREVIOUS verb's value - the exact staleness the generation poison
|
||||
// exists to catch, re-introduced by the very mechanism meant to instrument it.
|
||||
Bool FieldIsInVerbClass(MGPipeInputField field, MGPipeVerb verb) {
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (verbIndex >= kMGPipeVerbCount) return false;
|
||||
const MGPipeVerbClass verbClass = kMGPipeVerbClass[verbIndex];
|
||||
return MGPipeFieldMaskHas(kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)], field);
|
||||
}
|
||||
|
||||
// ---- the same verdict, per verb CLASS, as a constant (P5d round 3, package C) ----
|
||||
//
|
||||
// THE STAMP's ANSWER IS A CONSTANT OF THE VERB CLASS AND OF NOTHING ELSE. Both halves of
|
||||
// `answerable` below read constexpr tables only - kMGPipeFieldOwnership, kMGPipeVerbClass
|
||||
// and kMGPipeClassFieldMask - so the 63-field loop was recomputing, at every verb on the
|
||||
// apply thread, a table the compiler can build once. The 2026-09-17 inproc profile put
|
||||
// MGPipeServerStampVerbBoundary at 1.1% self of the apply thread (Minecraft 26.3-rc-3,
|
||||
// ~852 draws/frame), and every cycle taken there is a cycle the lockstep client waits for.
|
||||
//
|
||||
// IT IS A MASK, NOT A BOOL ARRAY, so the stamp loop's body stays a shift and a store with
|
||||
// no branch: FilledGen[i] = serial & -bit, which is `serial` for an answerable field and
|
||||
// the WITHDRAWAL 0 for every other. The verdict itself is unchanged, and it is still
|
||||
// computed by the one expression argued at the stamp - it just runs at compile time.
|
||||
constexpr MGPipeFieldMask AnswerableMaskForClass(MGPipeVerbClass verbClass) {
|
||||
MGPipeFieldMask mask{};
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[i];
|
||||
const Bool answerable = (ownership == MGPipeFieldOwnership::kRecordSupplied ||
|
||||
ownership == MGPipeFieldOwnership::kApplierDerived) &&
|
||||
MGPipeFieldMaskHas(
|
||||
kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)], field);
|
||||
if (answerable) mask.Words[i / 64] |= (Uint64{1} << (i % 64));
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
struct AnswerableMaskTable {
|
||||
MGPipeFieldMask ByClass[kMGPipeVerbClassCount];
|
||||
};
|
||||
|
||||
constexpr AnswerableMaskTable MakeAnswerableMaskTable() {
|
||||
AnswerableMaskTable table{};
|
||||
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) {
|
||||
table.ByClass[i] = AnswerableMaskForClass(static_cast<MGPipeVerbClass>(i));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
constexpr AnswerableMaskTable kAnswerableByClass = MakeAnswerableMaskTable();
|
||||
|
||||
// A verb id outside the table answers NOTHING, which is exactly what FieldIsInVerbClass
|
||||
// said for it (`verbIndex >= kMGPipeVerbCount` -> false for every field) and therefore
|
||||
// what the stamp said: all 63 withdrawn, every read the poison Fatal.
|
||||
constexpr MGPipeFieldMask kNoFieldIsAnswerable{};
|
||||
|
||||
const MGPipeFieldMask& AnswerableMaskForVerb(MGPipeVerb verb) {
|
||||
const SizeT verbIndex = static_cast<SizeT>(verb);
|
||||
if (verbIndex >= kMGPipeVerbCount) return kNoFieldIsAnswerable;
|
||||
return kAnswerableByClass.ByClass[static_cast<SizeT>(kMGPipeVerbClass[verbIndex])];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The third door into the storage (see PipeInputs.h). It exists because neither of the
|
||||
// other two can be the one that stamps: MGPipeApplyAccess deliberately does not, and
|
||||
// MGPipeFillAccess lives in MG_Impl, the role a server does not have.
|
||||
struct MGPipeStampAccess {
|
||||
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
|
||||
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
|
||||
static void SetServerStamped(PipeInputs& inputs, Bool stamped) {
|
||||
inputs.m_serverStampedVerb = stamped;
|
||||
}
|
||||
// P5f (f1): the server block's identity half of the stamp door (CONTRACT-P5E §3.2).
|
||||
// The client's SetIdentity lives in MG_Impl's MGPipeFillAccess and writes the CLIENT
|
||||
// block now; this one is how the server block learns which served context it describes
|
||||
// without MG_Impl being in the process at all.
|
||||
static void SetIdentity(PipeInputs& inputs, Bool live, const void* identity) {
|
||||
inputs.m_live = live;
|
||||
inputs.m_contextIdentity = identity;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- P5f (f1): the dual-block rehearsal's selection functions (PipeInputs.h) -----------
|
||||
//
|
||||
// MGPipeRoleSplitRehearsalActive is deliberately NOT latched: it is two global loads, asked once per
|
||||
// verb on the fill side and once per verb boundary on the stamp side, and a latch is a
|
||||
// second thing a test that flips the knob mid-process would have to know about.
|
||||
Bool MGPipeRoleSplitRehearsalActive() {
|
||||
return MG_Config::Ipc.RoleSplitState &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- D1c's predicates (CONTRACT-P6 3.2) -------------------------------------------------
|
||||
namespace {
|
||||
// Set once by ServerMain, before any GL work. A spawn server is the server for the whole
|
||||
// life of the process; there is no thread in it that is not.
|
||||
Bool g_serverProcessRole = false;
|
||||
// MG_Remote's "is the calling thread inside the applier". Null until a session installs
|
||||
// it, which is the honest answer for a process that has no applier.
|
||||
Bool (*g_applyThreadProbe)() = nullptr;
|
||||
Bool g_sessionLive = false;
|
||||
} // namespace
|
||||
|
||||
void MGPipeSetServerProcessRole(Bool isServerProcess) { g_serverProcessRole = isServerProcess; }
|
||||
void MGPipeSetApplyThreadProbe(Bool (*probe)()) { g_applyThreadProbe = probe; }
|
||||
void MGPipeSetSessionLive(Bool live) { g_sessionLive = live; }
|
||||
|
||||
Bool MGPipeServerArm() {
|
||||
// THE PROCESS FACT FIRST, and it short-circuits: in a spawn server every thread is a
|
||||
// server thread, and asking the probe would be asking MG_Remote a question it answers
|
||||
// only about the applier.
|
||||
if (g_serverProcessRole) return true;
|
||||
return g_applyThreadProbe != nullptr && g_applyThreadProbe();
|
||||
}
|
||||
|
||||
Bool MGPipeSessionLive() { return g_sessionLive; }
|
||||
|
||||
Bool MGPipeBlocksAreDistinct() {
|
||||
// TWO DIFFERENT REASONS, ONE ANSWER. The rehearsal makes two blocks in one process;
|
||||
// spawn makes them two blocks in two ADDRESS SPACES, where no knob is involved and the
|
||||
// distinctness is a fact about the machine. The guards that used to ask only the first
|
||||
// question were therefore disarmed in the one shape where the answer matters most - a
|
||||
// spawn server's block is never the client's, whatever MOBILEGL_IPC_ROLE_SPLIT_STATE
|
||||
// says.
|
||||
return MGPipeRoleSplitRehearsalActive() ||
|
||||
MG_Config::Transport == MG_Config::TransportMode::Spawn;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
PipeInputs& MGPipeClientInputs() {
|
||||
return MGPipeRoleSplitRehearsalActive() ? gPipeInputsClientBlock : gPipeInputs;
|
||||
}
|
||||
|
||||
void MGPipeClientClearVerbBoundary() {
|
||||
MGPipeStampAccess::SetServerStamped(MGPipeClientInputs(), false);
|
||||
}
|
||||
|
||||
namespace { Bool g_serverContextLive = false; }
|
||||
|
||||
void MGPipeServerSetContextLive(Bool live) {
|
||||
g_serverContextLive = live;
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
}
|
||||
|
||||
Bool MGPipeServerContextIsLive() { return g_serverContextLive; }
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace { const void* g_serverOwnedWindow = nullptr; }
|
||||
|
||||
void MGPipeServerSetOwnedWindow(const void* window) { g_serverOwnedWindow = window; }
|
||||
|
||||
const void* MGPipeServerOwnedWindow() { return g_serverOwnedWindow; }
|
||||
#endif
|
||||
|
||||
void MGPipeServerBlockNoteIdentity() {
|
||||
// D10: THE WIDER PREDICATE, and the narrow one was a latent crash rather than a missing
|
||||
// optimisation. Gated on the REHEARSAL, this early-returned for the whole life of a
|
||||
// spawn server - because that knob is off by default and nothing in the spawn shape
|
||||
// turns it on - so the server's gPipeInputs.ContextIdentity() stayed nullptr forever.
|
||||
// DirectGLES' fb-slot memo compares that identity FIRST and with no generation, so a
|
||||
// nullptr against its own nullptr initialiser reads as a CACHE HIT and hands out a slot
|
||||
// that was never filled. `st` lands the named Fatal at that site; this is the gate that
|
||||
// stops it being reachable in the first place.
|
||||
if (!MGPipeBlocksAreDistinct()) return;
|
||||
// The server cannot name the client's GLContext - under a real transport it is in
|
||||
// another process - so the identity the server block carries is the server's OWN
|
||||
// served-context clock: MGPipeApplierContextSerial moves exactly when the served
|
||||
// context does (MGPipeApplierReset), which is all the backends' per-context memo keys
|
||||
// ask of an identity. The odd-encoding keeps the token non-null at serial 0: a null
|
||||
// identity reads as a hit against DirectGLES' zero-initialised fb-slot memo cache and
|
||||
// hands out a null slot - an unnamed crash where the rehearsal exists to produce a
|
||||
// named one.
|
||||
const Uint64 serial = MGPipeApplierContextSerial();
|
||||
MGPipeStampAccess::SetIdentity(gPipeInputs, g_serverContextLive,
|
||||
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(
|
||||
(serial << 1) | Uint64{1})));
|
||||
}
|
||||
|
||||
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
// P5e (gl), ID-115: THE POSITIVE CONTROL'S ONLY HONEST SIGNAL, and it belongs HERE
|
||||
// because this is the line the whole strict mechanism hangs off. The poison, rsp and
|
||||
// the BARRIER-PULLED verdict are all reachable only after this stamp, and the monolith
|
||||
// arm never stamps at all - so "the strict lane is green" and "strict was never armed"
|
||||
// were observationally identical, which is how a lane whose seven passing entries
|
||||
// included no record-carrying split GL scenario read as rigour. Behind Enabled(), which
|
||||
// is the file's rule for counters (`rsp` beside it does the same) and keeps the stamp a
|
||||
// table lookup and a branch-free fill when stats are off.
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ServerVerbBoundaries, 1);
|
||||
}
|
||||
MGPipeFilledState& filled = MGPipeStampAccess::Filled(inputs);
|
||||
MGPipeStampAccess::SetVerb(inputs, verb);
|
||||
// Starts at 1 for MGPipeValidateForVerb's reason: FilledGen == 0 is "never filled" on
|
||||
// BOTH branches of MGPipeInputFieldIsFresh, so the zeroing below is a real withdrawal
|
||||
// rather than a stamp that happens to be old.
|
||||
++filled.CurrentVerbSerial;
|
||||
// STAMPED: in this verb's class AND answerable out of the records the applier has
|
||||
// already applied. WITHDRAWN (0): everything else - which is BARRIER-PULLED, FATAL,
|
||||
// and anything the verb's own may-read table says this verb does not read.
|
||||
//
|
||||
// The withdrawal is the load-bearing half of the rule: the client's residual fill
|
||||
// stamped all 63 fields at its own verb boundary, so without it every field would
|
||||
// read fresh on the server, `rsp` would be identically 0 and the exit gate would be
|
||||
// decoration. It also cancels the sticky exemption for free - generated/
|
||||
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so 0 wins over
|
||||
// kMGPipeInputFieldSticky without a line of the generated file changing.
|
||||
//
|
||||
// THE VERDICT IS THE SAME EXPRESSION; it is just precomputed per verb class
|
||||
// (AnswerableMaskForClass above) instead of re-derived 63 times per verb, so what is
|
||||
// left here is a table lookup and a branch-free fill.
|
||||
const MGPipeFieldMask& answerable = AnswerableMaskForVerb(verb);
|
||||
const Uint64 serial = filled.CurrentVerbSerial;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const Uint64 bit = (answerable.Words[i / 64] >> (i % 64)) & Uint64{1};
|
||||
filled.FilledGen[i] = serial & (Uint64{0} - bit);
|
||||
}
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// Negative control B's server half (ServerPoisonOmission above): the one withheld pair.
|
||||
{
|
||||
const ServerPoisonOmission& omission = ServerOmission();
|
||||
if (omission.Armed && omission.Verb == verb) {
|
||||
filled.FilledGen[static_cast<SizeT>(omission.Field)] = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
MGPipeStampAccess::SetServerStamped(inputs, true);
|
||||
// P5f (f1): the server block's identity rides the stamp (CONTRACT-P5E §3.2's
|
||||
// "SetIdentity moves to ApplyOne" - the stamp is the per-verb half of ApplyOne, and
|
||||
// refreshing here rather than once at Attach keeps the token in step with
|
||||
// MGPipeApplierContextSerial across a context switch). A no-op with the rehearsal off.
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
}
|
||||
|
||||
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
|
||||
|
||||
Uint64 MGPipeResidualPullCount() { return g_residualPulls; }
|
||||
void MGPipeResetResidualPullCountForTesting() { g_residualPulls = 0; }
|
||||
|
||||
void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped) {
|
||||
// OUTSIDE A SERVER-STAMPED VERB THIS IS THE MONOLITH ANSWER, UNCHANGED. A split BUILD
|
||||
// running MOBILEGL_TRANSPORT=monolith - every unit and integration-gpu lane of
|
||||
// build-split - has a client that stamped all 63 fields, so a stale read there is the
|
||||
// same defect it is in a verify build. Softening it on the build rather than on the
|
||||
// stamp would take 1842 unit cases' ability to go red away with it.
|
||||
//
|
||||
// AND A READ OUTSIDE THE VERB'S OWN CLASS IS STILL FATAL even for a BARRIER-PULLED
|
||||
// field: the value it would be answered with was never copied for this verb, so
|
||||
// counting it would trade a loud staleness for a quiet one.
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// An injected omission is reported as what it models - a stamp nobody wrote, the
|
||||
// client filler's own Fatal{UnmigratedPipeInput, "<Field>@<Verb>"} - and never as the
|
||||
// RoleViolation below, which would blame the wire for a withdrawal the control made.
|
||||
if (serverStamped && ServerOmitsStamp(verb, field)) MGPipeInputPoisonFatalForVerb(field, verb);
|
||||
#endif
|
||||
if (!serverStamped ||
|
||||
kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled ||
|
||||
!FieldIsInVerbClass(field, verb)) {
|
||||
// P5c (gt, CONTRACT-P5C §6 layer 1): under a server stamp, a stale read of a field
|
||||
// whose row is RECORD-SUPPLIED or APPLIER-DERIVED - i.e. a value a pushed record
|
||||
// DOES carry, read where this verb's stamp does not cover it - is a role violation
|
||||
// named by its surface, not a generic unmigrated read: the wire already owns the
|
||||
// answer, so reaching past it into the residual fill is rule E's shape. A
|
||||
// BARRIER-PULLED field outside the verb's class and a FATAL field keep the poison
|
||||
// Fatal - those are the stamp table's own verdicts, not a role's overreach.
|
||||
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[static_cast<SizeT>(field)];
|
||||
if (serverStamped && (ownership == MGPipeFieldOwnership::kRecordSupplied ||
|
||||
ownership == MGPipeFieldOwnership::kApplierDerived)) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"%s\"} - the server read this field stale "
|
||||
"at %s, but a pushed record carries it (the row is %s): the read reached "
|
||||
"the client's residual fill for a value the wire already owns",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb),
|
||||
MGPipeFieldOwnershipName(ownership));
|
||||
std::abort();
|
||||
}
|
||||
MGPipeInputPoisonFatalForVerb(field, verb);
|
||||
}
|
||||
CountBarrierPull(field, verb);
|
||||
}
|
||||
|
||||
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped) {
|
||||
if (!serverStamped) return false;
|
||||
const MGPipeFieldOwnership narrowed = MGPipeFieldOwnershipOf(field, arg0);
|
||||
if (narrowed == MGPipeFieldOwnershipOf(field)) return false; // the argument narrows nothing
|
||||
if (narrowed == MGPipeFieldOwnership::kFatal) {
|
||||
// The field's own stamp says fresh - the applier really did write the half that has
|
||||
// a carrier - so only the argument can say that THIS read is unserved. THE MESSAGE
|
||||
// NAMES THE ARGUMENT, because without it this line is byte-identical to what a
|
||||
// genuinely stale read of the OTHER half would print, and the whole case for
|
||||
// narrowing by argument rather than by a second field id is that the reader is told
|
||||
// which half they asked for.
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [argument 0 = %u is %s while the "
|
||||
"field is %s]",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb), arg0,
|
||||
MGPipeFieldOwnershipName(narrowed),
|
||||
MGPipeFieldOwnershipName(MGPipeFieldOwnershipOf(field)));
|
||||
std::abort();
|
||||
}
|
||||
if (narrowed == MGPipeFieldOwnership::kBarrierPulled) {
|
||||
CountBarrierPull(field, verb);
|
||||
return true; // decided here; the field-level check must not count it again
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MGPipeStickyForwardPull(MGPipeInputField field) {
|
||||
// The seven carry no MGP_INPUT_CHECK at all (the declared exception argued at
|
||||
// PipeInputs.h's F-class block), so freshness can never reach them and neither can the
|
||||
// stamp's withdrawal. This is the only thing that puts them in `rsp`.
|
||||
if (!gPipeInputs.ServerStampedVerb()) return;
|
||||
const auto ownership = MGPipeFieldOwnershipOf(field);
|
||||
if (ownership == MGPipeFieldOwnership::kFatal)
|
||||
MGPipeInputPoisonFatalForVerb(field, gPipeInputs.CurrentVerb());
|
||||
if (ownership == MGPipeFieldOwnership::kBarrierPulled)
|
||||
CountBarrierPull(field, gPipeInputs.CurrentVerb());
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
namespace {
|
||||
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
|
||||
|
||||
// Every overload is declared up front: the array overloads recurse into their element
|
||||
// type, and a call inside a template only sees what was declared before the template.
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
|
||||
template <class T>
|
||||
void CorruptStorage(T& v);
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p);
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p);
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]);
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v);
|
||||
|
||||
// ---- equality over one field's storage ----
|
||||
// O-class storage compares by identity: a raw pointer into the context, or the object a
|
||||
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
|
||||
// C arrays element-wise.
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b) {
|
||||
return a == b;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
|
||||
return a.get() == b.get();
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
|
||||
for (SizeT i = 0; i < N; ++i) {
|
||||
if (!StorageEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
|
||||
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
|
||||
}
|
||||
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
|
||||
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
|
||||
// what turns a fourth member into a build break rather than a blind spot.
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
|
||||
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
|
||||
"CurrentVertexAttributeValue grew a member; update the comparator");
|
||||
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b) {
|
||||
return MGPipeFieldEqual(a, b);
|
||||
}
|
||||
|
||||
// ---- corruption of one field's storage ----
|
||||
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
|
||||
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
|
||||
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
|
||||
// flipped address with no control block, an array corrupts its first element, and any
|
||||
// other struct has its first byte XOR'ed with 0x5A.
|
||||
template <class T>
|
||||
T* FlipPointer(T* p) {
|
||||
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p) {
|
||||
p = FlipPointer(p);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p) {
|
||||
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]) {
|
||||
CorruptStorage(a[0]);
|
||||
}
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
|
||||
CorruptStorage(c.Blend);
|
||||
}
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v) {
|
||||
v.floatValue[0] += 1.f;
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T& v) {
|
||||
if constexpr (std::is_same_v<T, Bool>) {
|
||||
v = !v;
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
|
||||
} else if constexpr (std::is_arithmetic_v<T>) {
|
||||
v = static_cast<T>(v + 1);
|
||||
} else {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
|
||||
unsigned char first = 0;
|
||||
std::memcpy(&first, &v, 1);
|
||||
first ^= 0x5A;
|
||||
std::memcpy(&v, &first, 1);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
|
||||
// A forwarded field has no storage and is equal by definition; VisitStorage answers
|
||||
// false for it, hence the explicit sticky test first.
|
||||
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
|
||||
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
|
||||
}
|
||||
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField) {
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
|
||||
if (outField != nullptr) *outField = field;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
|
||||
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
|
||||
CorruptStorage(x);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/ServerRole.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
|
||||
|
||||
// The server role's bring-up, as one entry point two shapes share.
|
||||
//
|
||||
// Steps 1 and 2 of Init.cpp's split bring-up - the server's private
|
||||
// BackendObject, and the two CallMask halves computed from THAT backend's own
|
||||
// function table. Under `inproc` InitSplitRoles calls it and then starts the
|
||||
// client in the same process; under `spawn` the child's ServerMain calls it and
|
||||
// then accepts a connection instead.
|
||||
//
|
||||
// ONE ENTRY POINT, not two, because the capability bits are a statement about
|
||||
// the server's backend and only the server can compute them. A second copy in
|
||||
// ServerMain would be a second answer to "does this backend own the XFB
|
||||
// capture", diverging silently the first time a backend registers a slot.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
// DECLARED ONLY IN A DISAGGREGATED BUILD. G1 requires the pull build's symbol
|
||||
// set and .text to be byte-identical, and a definition guarded only on the
|
||||
// inside still emits a symbol - measured, not assumed.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
|
||||
// Creates the server's BackendObject and publishes the consumed-subsystem
|
||||
// mask and the capability bits onto ServerSessionInstance(). Does NOT create
|
||||
// an EGL context: that happens later, on the apply thread, when the client's
|
||||
// first eglMakeCurrent crosses as a blocking control request.
|
||||
//
|
||||
// Returns false when the backend could not be created, and the caller must
|
||||
// then REFUSE TO CONTINUE rather than fall back - a server process that came
|
||||
// up without a backend would accept a session it can never apply.
|
||||
Bool InitServerRoleForSpawn();
|
||||
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -11,51 +11,5 @@ endif()
|
||||
add_executable(DriverBench DriverBench.c)
|
||||
target_link_libraries(DriverBench PRIVATE dl)
|
||||
|
||||
# WHY EVERY ENTRY HERE CARRIES A PASS_REGULAR_EXPRESSION.
|
||||
#
|
||||
# DriverBench prints one CSV row per case it ran and exits 0 whatever it ran. Before this, a ctest
|
||||
# entry naming a case therefore could not answer the only question it exists to ask: an argument
|
||||
# matching nothing in kBenchCases selected no case, printed only the header row, and still exited
|
||||
# 0. DriverBench.c now refuses an unknown case name (exit 2), which closes it at the source - but
|
||||
# the entry must be able to go red for the reason it exists WITHOUT depending on that check
|
||||
# staying in the binary, so each entry also requires the case's own output row to appear.
|
||||
#
|
||||
# The regex is what a healthy run of that case prints and nothing else does: the case name at the
|
||||
# start of a line, then the frames / ops-per-frame / median-ms / ns-per-op / fps columns
|
||||
# (run_case()). A rename, a drop from kBenchCases, a boot_egl() failure or
|
||||
# a crash part-way through the case all remove that row and turn the entry red.
|
||||
#
|
||||
# Note that a PASS_REGULAR_EXPRESSION makes ctest ignore the process exit code (cmCTestRunTest:
|
||||
# success is `retVal == 0 || !RequiredRegularExpressions.empty()`), which is why the row itself
|
||||
# has to be the evidence rather than a companion to the rc.
|
||||
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||
# draw_tiny's a/ops scale with $DRIVERBENCH_DRAWS (main()), so only the shape of
|
||||
# the row is pinned here, not the column values.
|
||||
set_tests_properties(DriverBench PROPERTIES
|
||||
LABELS benchmark
|
||||
PASS_REGULAR_EXPRESSION "(^|\n)draw_tiny,[0-9]+,[0-9]+,[0-9.]+,[0-9.]+,[0-9.]+")
|
||||
|
||||
# The Blaze3D blend toggle, as its own entry.
|
||||
#
|
||||
# mc_state_toggle is glEnable(GL_BLEND) / glBlendFuncSeparate / glDrawElements /
|
||||
# glDisable(GL_BLEND) / glDrawElements, 46 times - the measured vanilla-frame rate, and the exact
|
||||
# shape ROADMAP.md writes down as the microbenchmark P2 owes the GO/NO-GO. It is the workload the
|
||||
# whole "push at validate, not in the setter" decision was made for: a per-setter design pays for
|
||||
# every toggle, and a CSO that is minted twice and then reused pays for none of them.
|
||||
#
|
||||
# The case has existed in kBenchCases since P0 and nothing ran it, so nothing noticed if it broke.
|
||||
# Exposing it costs about 1.2 s inside an existing three-minute job, and it means the number the
|
||||
# P2 report quotes comes from a case CI has been executing all along rather than from a code path
|
||||
# whose first run is the day it is measured.
|
||||
#
|
||||
# Like the entry above, this runs against whatever $DRIVERBENCH_EGL_LIB names (the system driver
|
||||
# when unset) - the ctest entry is a "does this case still run" gate, not the measurement. The
|
||||
# measurement is run_driver_bench.sh against each of {native, espryt, magma}.
|
||||
add_test(NAME DriverBenchStateToggle COMMAND DriverBench mc_state_toggle)
|
||||
# The ops-per-frame column is pinned to 46 here, unlike the entry above: the mc_* cases are
|
||||
# excluded from the $DRIVERBENCH_DRAWS scaling on purpose ("the mc_* rates are measured and must
|
||||
# not move, or the numbers stop being comparable", main()), so 46 toggles per frame
|
||||
# is part of what "this case still runs" means. Change the workload and this entry says so.
|
||||
set_tests_properties(DriverBenchStateToggle PROPERTIES
|
||||
LABELS benchmark
|
||||
PASS_REGULAR_EXPRESSION "(^|\n)mc_state_toggle,[0-9]+,46,[0-9.]+,[0-9.]+,[0-9.]+")
|
||||
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
|
||||
|
||||
@@ -476,28 +476,6 @@ int main(int argc, char** argv) {
|
||||
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
||||
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||
|
||||
/* A requested case name that matches nothing used to select nothing, print the header row and
|
||||
* exit 0 - so a caller that names a case (run_driver_bench.sh, and the two ctest entries in
|
||||
* CMakeLists.txt) could not tell "the case ran" from "the case has been renamed or deleted".
|
||||
* Refuse it here, before any GL work, so the refusal reaches a caller that has no display
|
||||
* either, and name what does exist so the fix is obvious. */
|
||||
int unknownCases = 0;
|
||||
for (int j = 1; j < argc; ++j) {
|
||||
int known = 0;
|
||||
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||
if (strcmp(argv[j], kBenchCases[i].name) == 0) known = 1;
|
||||
if (!known) {
|
||||
fprintf(stderr, "DriverBench: no case named '%s'\n", argv[j]);
|
||||
unknownCases = 1;
|
||||
}
|
||||
}
|
||||
if (unknownCases) {
|
||||
fprintf(stderr, "DriverBench: the %d cases in kBenchCases are:\n", kBenchCaseCount);
|
||||
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||
fprintf(stderr, " %s\n", kBenchCases[i].name);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (boot_egl()) return 1;
|
||||
build_resources();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,900 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Client/generated_wire_trampoline.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
// GENERATED by scripts/generate_client_trampoline.py
|
||||
|
||||
MOBILEGL_GL_API void glActiveProgramEXT(GLuint program);
|
||||
MOBILEGL_GL_API void glActiveShaderProgram(GLuint pipeline, GLuint program);
|
||||
MOBILEGL_GL_API void glActiveTexture(GLenum texture);
|
||||
MOBILEGL_GL_API void glAttachShader(GLuint program, GLuint shader);
|
||||
MOBILEGL_GL_API void glBeginConditionalRender(GLuint id, GLenum mode);
|
||||
MOBILEGL_GL_API void glBeginConditionalRenderNV(GLuint id, GLenum mode);
|
||||
MOBILEGL_GL_API void glBeginPerfMonitorAMD(GLuint monitor);
|
||||
MOBILEGL_GL_API void glBeginPerfQueryINTEL(GLuint queryHandle);
|
||||
MOBILEGL_GL_API void glBeginQuery(GLenum target, GLuint id);
|
||||
MOBILEGL_GL_API void glBeginQueryIndexed(GLenum target, GLuint index, GLuint id);
|
||||
MOBILEGL_GL_API void glBeginTransformFeedback(GLenum primitiveMode);
|
||||
MOBILEGL_GL_API void glBindAttribLocation(GLuint program, GLuint index, const GLchar* name);
|
||||
MOBILEGL_GL_API void glBindBuffer(GLenum target, GLuint buffer);
|
||||
MOBILEGL_GL_API void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);
|
||||
MOBILEGL_GL_API void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glBindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers);
|
||||
MOBILEGL_GL_API void glBindFragDataLocation(GLuint program, GLuint color, const GLchar* name);
|
||||
MOBILEGL_GL_API void glBindFragDataLocationIndexed(GLuint program, GLuint colorNumber, GLuint index, const GLchar* name);
|
||||
MOBILEGL_GL_API void glBindFramebuffer(GLenum target, GLuint framebuffer);
|
||||
MOBILEGL_GL_API void glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
|
||||
MOBILEGL_GL_API void glBindImageTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
MOBILEGL_GL_API void glBindMultiTextureEXT(GLenum texunit, GLenum target, GLuint texture);
|
||||
MOBILEGL_GL_API void glBindProgramPipeline(GLuint pipeline);
|
||||
MOBILEGL_GL_API void glBindRenderbuffer(GLenum target, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glBindSampler(GLuint unit, GLuint sampler);
|
||||
MOBILEGL_GL_API void glBindSamplers(GLuint first, GLsizei count, const GLuint* samplers);
|
||||
MOBILEGL_GL_API void glBindShadingRateImageNV(GLuint texture);
|
||||
MOBILEGL_GL_API void glBindTexture(GLenum target, GLuint texture);
|
||||
MOBILEGL_GL_API void glBindTextureUnit(GLuint unit, GLuint texture);
|
||||
MOBILEGL_GL_API void glBindTextures(GLuint first, GLsizei count, const GLuint* textures);
|
||||
MOBILEGL_GL_API void glBindTransformFeedback(GLenum target, GLuint id);
|
||||
MOBILEGL_GL_API void glBindVertexArray(GLuint array);
|
||||
MOBILEGL_GL_API void glBindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
||||
MOBILEGL_GL_API void glBlendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
|
||||
MOBILEGL_GL_API void glBlendEquation(GLenum mode);
|
||||
MOBILEGL_GL_API void glBlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);
|
||||
MOBILEGL_GL_API void glBlendEquationSeparatei(GLuint buf, GLenum modeRGB, GLenum modeAlpha);
|
||||
MOBILEGL_GL_API void glBlendEquationSeparateiARB(GLuint buf, GLenum modeRGB, GLenum modeAlpha);
|
||||
MOBILEGL_GL_API void glBlendEquationi(GLuint buf, GLenum mode);
|
||||
MOBILEGL_GL_API void glBlendEquationiARB(GLuint buf, GLenum mode);
|
||||
MOBILEGL_GL_API void glBlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
MOBILEGL_GL_API void glBlendFuncSeparate(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
|
||||
MOBILEGL_GL_API void glBlendFuncSeparatei(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
|
||||
MOBILEGL_GL_API void glBlendFuncSeparateiARB(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
|
||||
MOBILEGL_GL_API void glBlendFunci(GLuint buf, GLenum src, GLenum dst);
|
||||
MOBILEGL_GL_API void glBlendFunciARB(GLuint buf, GLenum src, GLenum dst);
|
||||
MOBILEGL_GL_API void glBlendParameteriNV(GLenum pname, GLint value);
|
||||
MOBILEGL_GL_API void glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
MOBILEGL_GL_API void glBlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
MOBILEGL_GL_API void glBufferAddressRangeNV(GLenum pname, GLuint index, GLuint64EXT address, GLsizeiptr length);
|
||||
MOBILEGL_GL_API void glBufferAttachMemoryNV(GLenum target, GLuint memory, GLuint64 offset);
|
||||
MOBILEGL_GL_API void glBufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
|
||||
MOBILEGL_GL_API void glBufferPageCommitmentARB(GLenum target, GLintptr offset, GLsizeiptr size, GLboolean commit);
|
||||
MOBILEGL_GL_API void glBufferPageCommitmentMemNV(GLenum target, GLintptr offset, GLsizeiptr size, GLuint memory, GLuint64 memOffset, GLboolean commit);
|
||||
MOBILEGL_GL_API void glBufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags);
|
||||
MOBILEGL_GL_API void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
|
||||
MOBILEGL_GL_API void glCallCommandListNV(GLuint list);
|
||||
MOBILEGL_GL_API GLenum glCheckFramebufferStatus(GLenum target);
|
||||
MOBILEGL_GL_API GLenum glCheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
|
||||
MOBILEGL_GL_API GLenum glCheckNamedFramebufferStatusEXT(GLuint framebuffer, GLenum target);
|
||||
MOBILEGL_GL_API void glClampColor(GLenum target, GLenum clamp);
|
||||
MOBILEGL_GL_API void glClear(GLbitfield mask);
|
||||
MOBILEGL_GL_API void glClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data);
|
||||
MOBILEGL_GL_API void glClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
MOBILEGL_GL_API void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
|
||||
MOBILEGL_GL_API void glClearDepth(GLclampd depth);
|
||||
MOBILEGL_GL_API void glClearDepthdNV(GLdouble depth);
|
||||
MOBILEGL_GL_API void glClearDepthf(GLfloat d);
|
||||
MOBILEGL_GL_API void glClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data);
|
||||
MOBILEGL_GL_API void glClearNamedBufferSubDataEXT(GLuint buffer, GLenum internalformat, GLsizeiptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data);
|
||||
MOBILEGL_GL_API void glClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
MOBILEGL_GL_API void glClearStencil(GLint s);
|
||||
MOBILEGL_GL_API void glClientAttribDefaultEXT(GLbitfield mask);
|
||||
MOBILEGL_GL_API GLenum glClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||
MOBILEGL_GL_API void glClipControl(GLenum origin, GLenum depth);
|
||||
MOBILEGL_GL_API void glColorFormatNV(GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
|
||||
MOBILEGL_GL_API void glColorMaski(GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a);
|
||||
MOBILEGL_GL_API void glCommandListSegmentsNV(GLuint list, GLuint segments);
|
||||
MOBILEGL_GL_API void glCompileCommandListNV(GLuint list);
|
||||
MOBILEGL_GL_API void glCompileShader(GLuint shader);
|
||||
MOBILEGL_GL_API void glConservativeRasterParameterfNV(GLenum pname, GLfloat value);
|
||||
MOBILEGL_GL_API void glConservativeRasterParameteriNV(GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glCopyBufferSubData(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glCopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
MOBILEGL_GL_API void glCopyMultiTexImage1DEXT(GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
|
||||
MOBILEGL_GL_API void glCopyMultiTexImage2DEXT(GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
|
||||
MOBILEGL_GL_API void glCopyMultiTexSubImage1DEXT(GLenum texunit, GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||
MOBILEGL_GL_API void glCopyMultiTexSubImage2DEXT(GLenum texunit, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyMultiTexSubImage3DEXT(GLenum texunit, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glCopyPathNV(GLuint resultPath, GLuint srcPath);
|
||||
MOBILEGL_GL_API void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
|
||||
MOBILEGL_GL_API void glCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
|
||||
MOBILEGL_GL_API void glCopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||
MOBILEGL_GL_API void glCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyTextureImage1DEXT(GLuint texture, GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
|
||||
MOBILEGL_GL_API void glCopyTextureImage2DEXT(GLuint texture, GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage1DEXT(GLuint texture, GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage2DEXT(GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCopyTextureSubImage3DEXT(GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glCoverFillPathNV(GLuint path, GLenum coverMode);
|
||||
MOBILEGL_GL_API void glCoverStrokePathNV(GLuint path, GLenum coverMode);
|
||||
MOBILEGL_GL_API void glCoverageModulationNV(GLenum components);
|
||||
MOBILEGL_GL_API void glCoverageModulationTableNV(GLsizei n, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glCreateBuffers(GLsizei n, GLuint* buffers);
|
||||
MOBILEGL_GL_API void glCreateCommandListsNV(GLsizei n, GLuint* lists);
|
||||
MOBILEGL_GL_API void glCreateFramebuffers(GLsizei n, GLuint* framebuffers);
|
||||
MOBILEGL_GL_API void glCreateProgramPipelines(GLsizei n, GLuint* pipelines);
|
||||
MOBILEGL_GL_API void glCreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||
MOBILEGL_GL_API void glCreateRenderbuffers(GLsizei n, GLuint* renderbuffers);
|
||||
MOBILEGL_GL_API void glCreateSamplers(GLsizei n, GLuint* samplers);
|
||||
MOBILEGL_GL_API GLuint glCreateShader(GLenum type);
|
||||
MOBILEGL_GL_API GLuint glCreateShaderProgramEXT(GLenum type, const GLchar* string);
|
||||
MOBILEGL_GL_API void glCreateStatesNV(GLsizei n, GLuint* states);
|
||||
MOBILEGL_GL_API void glCreateTextures(GLenum target, GLsizei n, GLuint* textures);
|
||||
MOBILEGL_GL_API void glCreateTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||
MOBILEGL_GL_API void glCreateVertexArrays(GLsizei n, GLuint* arrays);
|
||||
MOBILEGL_GL_API void glCullFace(GLenum mode);
|
||||
MOBILEGL_GL_API void glDebugMessageControl(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled);
|
||||
MOBILEGL_GL_API void glDebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf);
|
||||
MOBILEGL_GL_API void glDeleteBuffers(GLsizei n, const GLuint* buffers);
|
||||
MOBILEGL_GL_API void glDeleteCommandListsNV(GLsizei n, const GLuint* lists);
|
||||
MOBILEGL_GL_API void glDeleteFramebuffers(GLsizei n, const GLuint* framebuffers);
|
||||
MOBILEGL_GL_API void glDeleteNamedStringARB(GLint namelen, const GLchar* name);
|
||||
MOBILEGL_GL_API void glDeletePathsNV(GLuint path, GLsizei range);
|
||||
MOBILEGL_GL_API void glDeletePerfMonitorsAMD(GLsizei n, GLuint* monitors);
|
||||
MOBILEGL_GL_API void glDeletePerfQueryINTEL(GLuint queryHandle);
|
||||
MOBILEGL_GL_API void glDeleteProgram(GLuint program);
|
||||
MOBILEGL_GL_API void glDeleteProgramPipelines(GLsizei n, const GLuint* pipelines);
|
||||
MOBILEGL_GL_API void glDeleteQueries(GLsizei n, const GLuint* ids);
|
||||
MOBILEGL_GL_API void glDeleteRenderbuffers(GLsizei n, const GLuint* renderbuffers);
|
||||
MOBILEGL_GL_API void glDeleteSamplers(GLsizei count, const GLuint* samplers);
|
||||
MOBILEGL_GL_API void glDeleteShader(GLuint shader);
|
||||
MOBILEGL_GL_API void glDeleteStatesNV(GLsizei n, const GLuint* states);
|
||||
MOBILEGL_GL_API void glDeleteSync(GLsync sync);
|
||||
MOBILEGL_GL_API void glDeleteTextures(GLsizei n, const GLuint* textures);
|
||||
MOBILEGL_GL_API void glDeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
|
||||
MOBILEGL_GL_API void glDeleteVertexArrays(GLsizei n, const GLuint* arrays);
|
||||
MOBILEGL_GL_API void glDepthBoundsdNV(GLdouble zmin, GLdouble zmax);
|
||||
MOBILEGL_GL_API void glDepthFunc(GLenum func);
|
||||
MOBILEGL_GL_API void glDepthMask(GLboolean flag);
|
||||
MOBILEGL_GL_API void glDepthRange(GLclampd near_val, GLclampd far_val);
|
||||
MOBILEGL_GL_API void glDepthRangeArraydvNV(GLuint first, GLsizei count, const GLdouble* v);
|
||||
MOBILEGL_GL_API void glDepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
MOBILEGL_GL_API void glDepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
MOBILEGL_GL_API void glDepthRangeIndexeddNV(GLuint index, GLdouble n, GLdouble f);
|
||||
MOBILEGL_GL_API void glDepthRangedNV(GLdouble zNear, GLdouble zFar);
|
||||
MOBILEGL_GL_API void glDepthRangef(GLfloat n, GLfloat f);
|
||||
MOBILEGL_GL_API void glDetachShader(GLuint program, GLuint shader);
|
||||
MOBILEGL_GL_API void glDisable(GLenum cap);
|
||||
MOBILEGL_GL_API void glDisableClientStateIndexedEXT(GLenum array, GLuint index);
|
||||
MOBILEGL_GL_API void glDisableClientStateiEXT(GLenum array, GLuint index);
|
||||
MOBILEGL_GL_API void glDisableIndexedEXT(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API void glDisableVertexArrayAttrib(GLuint vaobj, GLuint index);
|
||||
MOBILEGL_GL_API void glDisableVertexArrayAttribEXT(GLuint vaobj, GLuint index);
|
||||
MOBILEGL_GL_API void glDisableVertexArrayEXT(GLuint vaobj, GLenum array);
|
||||
MOBILEGL_GL_API void glDisableVertexAttribArray(GLuint index);
|
||||
MOBILEGL_GL_API void glDisablei(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
|
||||
MOBILEGL_GL_API void glDispatchComputeGroupSizeARB(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z, GLuint group_size_x, GLuint group_size_y, GLuint group_size_z);
|
||||
MOBILEGL_GL_API void glDispatchComputeIndirect(GLintptr indirect);
|
||||
MOBILEGL_GL_API void glDrawArrays(GLenum mode, GLint first, GLsizei count);
|
||||
MOBILEGL_GL_API void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
|
||||
MOBILEGL_GL_API void glDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance);
|
||||
MOBILEGL_GL_API void glDrawBuffer(GLenum mode);
|
||||
MOBILEGL_GL_API void glDrawBuffers(GLsizei n, const GLenum* bufs);
|
||||
MOBILEGL_GL_API void glDrawCommandsAddressNV(GLenum primitiveMode, const GLuint64* indirects, const GLsizei* sizes, GLuint count);
|
||||
MOBILEGL_GL_API void glDrawCommandsStatesAddressNV(const GLuint64* indirects, const GLsizei* sizes, const GLuint* states, const GLuint* fbos, GLuint count);
|
||||
MOBILEGL_GL_API void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||
MOBILEGL_GL_API void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex);
|
||||
MOBILEGL_GL_API void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount);
|
||||
MOBILEGL_GL_API void glDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance);
|
||||
MOBILEGL_GL_API void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex);
|
||||
MOBILEGL_GL_API void glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance);
|
||||
MOBILEGL_GL_API void glDrawMeshTasksIndirectNV(GLintptr indirect);
|
||||
MOBILEGL_GL_API void glDrawMeshTasksNV(GLuint first, GLuint count);
|
||||
MOBILEGL_GL_API void glDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
|
||||
MOBILEGL_GL_API void glDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex);
|
||||
MOBILEGL_GL_API void glDrawTransformFeedback(GLenum mode, GLuint id);
|
||||
MOBILEGL_GL_API void glDrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount);
|
||||
MOBILEGL_GL_API void glDrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream);
|
||||
MOBILEGL_GL_API void glDrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
|
||||
MOBILEGL_GL_API void glDrawVkImageNV(GLuint64 vkImage, GLuint sampler, GLfloat x0, GLfloat y0, GLfloat x1, GLfloat y1, GLfloat z, GLfloat s0, GLfloat t0, GLfloat s1, GLfloat t1);
|
||||
MOBILEGL_GL_API void glEdgeFlagFormatNV(GLsizei stride);
|
||||
MOBILEGL_GL_API void glEnable(GLenum cap);
|
||||
MOBILEGL_GL_API void glEnableClientStateIndexedEXT(GLenum array, GLuint index);
|
||||
MOBILEGL_GL_API void glEnableClientStateiEXT(GLenum array, GLuint index);
|
||||
MOBILEGL_GL_API void glEnableIndexedEXT(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API void glEnableVertexArrayAttrib(GLuint vaobj, GLuint index);
|
||||
MOBILEGL_GL_API void glEnableVertexArrayAttribEXT(GLuint vaobj, GLuint index);
|
||||
MOBILEGL_GL_API void glEnableVertexArrayEXT(GLuint vaobj, GLenum array);
|
||||
MOBILEGL_GL_API void glEnableVertexAttribArray(GLuint index);
|
||||
MOBILEGL_GL_API void glEnablei(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API void glEndPerfMonitorAMD(GLuint monitor);
|
||||
MOBILEGL_GL_API void glEndPerfQueryINTEL(GLuint queryHandle);
|
||||
MOBILEGL_GL_API void glEndQuery(GLenum target);
|
||||
MOBILEGL_GL_API void glEndQueryIndexed(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API void glFlushMappedBufferRange(GLenum target, GLintptr offset, GLsizeiptr length);
|
||||
MOBILEGL_GL_API void glFlushMappedNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length);
|
||||
MOBILEGL_GL_API void glFlushMappedNamedBufferRangeEXT(GLuint buffer, GLintptr offset, GLsizeiptr length);
|
||||
MOBILEGL_GL_API void glFogCoordFormatNV(GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glFragmentCoverageColorNV(GLuint color);
|
||||
MOBILEGL_GL_API void glFramebufferDrawBufferEXT(GLuint framebuffer, GLenum mode);
|
||||
MOBILEGL_GL_API void glFramebufferDrawBuffersEXT(GLuint framebuffer, GLsizei n, const GLenum* bufs);
|
||||
MOBILEGL_GL_API void glFramebufferParameteri(GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glFramebufferParameteriMESA(GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glFramebufferReadBufferEXT(GLuint framebuffer, GLenum mode);
|
||||
MOBILEGL_GL_API void glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glFramebufferSampleLocationsfvARB(GLenum target, GLuint start, GLsizei count, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glFramebufferSampleLocationsfvNV(GLenum target, GLuint start, GLsizei count, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glFramebufferTexture(GLenum target, GLenum attachment, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glFramebufferTexture1D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glFramebufferTexture1DEXT(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glFramebufferTexture3D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint zoffset);
|
||||
MOBILEGL_GL_API void glFramebufferTexture3DEXT(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint zoffset);
|
||||
MOBILEGL_GL_API void glFramebufferTextureFaceARB(GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face);
|
||||
MOBILEGL_GL_API void glFramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer);
|
||||
MOBILEGL_GL_API void glFramebufferTextureMultiviewOVR(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint baseViewIndex, GLsizei numViews);
|
||||
MOBILEGL_GL_API void glFrontFace(GLenum mode);
|
||||
MOBILEGL_GL_API void glGenBuffers(GLsizei n, GLuint* buffers);
|
||||
MOBILEGL_GL_API void glGenFramebuffers(GLsizei n, GLuint* framebuffers);
|
||||
MOBILEGL_GL_API GLuint glGenPathsNV(GLsizei range);
|
||||
MOBILEGL_GL_API void glGenPerfMonitorsAMD(GLsizei n, GLuint* monitors);
|
||||
MOBILEGL_GL_API void glGenProgramPipelines(GLsizei n, GLuint* pipelines);
|
||||
MOBILEGL_GL_API void glGenQueries(GLsizei n, GLuint* ids);
|
||||
MOBILEGL_GL_API void glGenRenderbuffers(GLsizei n, GLuint* renderbuffers);
|
||||
MOBILEGL_GL_API void glGenSamplers(GLsizei count, GLuint* samplers);
|
||||
MOBILEGL_GL_API void glGenTextures(GLsizei n, GLuint* textures);
|
||||
MOBILEGL_GL_API void glGenTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||
MOBILEGL_GL_API void glGenVertexArrays(GLsizei n, GLuint* arrays);
|
||||
MOBILEGL_GL_API void glGenerateMipmap(GLenum target);
|
||||
MOBILEGL_GL_API void glGenerateMultiTexMipmapEXT(GLenum texunit, GLenum target);
|
||||
MOBILEGL_GL_API void glGenerateTextureMipmap(GLuint texture);
|
||||
MOBILEGL_GL_API void glGenerateTextureMipmapEXT(GLuint texture, GLenum target);
|
||||
MOBILEGL_GL_API void glGetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetActiveSubroutineUniformiv(GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values);
|
||||
MOBILEGL_GL_API void glGetActiveUniformBlockiv(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLint glGetAttribLocation(GLuint program, const GLchar* name);
|
||||
MOBILEGL_GL_API void glGetBooleanv(GLenum pname, GLboolean* data);
|
||||
MOBILEGL_GL_API void glGetBufferParameteri64v(GLenum target, GLenum pname, GLint64* params);
|
||||
MOBILEGL_GL_API void glGetBufferParameteriv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetBufferParameterui64vNV(GLenum target, GLenum pname, GLuint64EXT* params);
|
||||
MOBILEGL_GL_API void glGetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
|
||||
MOBILEGL_GL_API GLuint glGetCommandHeaderNV(GLenum tokenID, GLuint size);
|
||||
MOBILEGL_GL_API GLuint glGetDebugMessageLog(GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog);
|
||||
MOBILEGL_GL_API void glGetDoublei_vEXT(GLenum pname, GLuint index, GLdouble* params);
|
||||
MOBILEGL_GL_API void glGetDoublev(GLenum pname, GLdouble* params);
|
||||
MOBILEGL_GL_API void glGetFloati_vEXT(GLenum pname, GLuint index, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetFloatv(GLenum pname, GLfloat* data);
|
||||
MOBILEGL_GL_API GLint glGetFragDataIndex(GLuint program, const GLchar* name);
|
||||
MOBILEGL_GL_API GLint glGetFragDataLocation(GLuint program, const GLchar* name);
|
||||
MOBILEGL_GL_API void glGetFramebufferAttachmentParameteriv(GLenum target, GLenum attachment, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetFramebufferParameteriv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetFramebufferParameterivMESA(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLuint64 glGetImageHandleARB(GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum format);
|
||||
MOBILEGL_GL_API GLuint64 glGetImageHandleNV(GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum format);
|
||||
MOBILEGL_GL_API void glGetInteger64v(GLenum pname, GLint64* data);
|
||||
MOBILEGL_GL_API void glGetIntegerv(GLenum pname, GLint* data);
|
||||
MOBILEGL_GL_API void glGetInternalformatSampleivNV(GLenum target, GLenum internalformat, GLsizei samples, GLenum pname, GLsizei count, GLint* params);
|
||||
MOBILEGL_GL_API void glGetInternalformati64v(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params);
|
||||
MOBILEGL_GL_API void glGetInternalformativ(GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMemoryObjectDetachedResourcesuivNV(GLuint memory, GLenum pname, GLint first, GLsizei count, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexEnvfvEXT(GLenum texunit, GLenum target, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexEnvivEXT(GLenum texunit, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexGendvEXT(GLenum texunit, GLenum coord, GLenum pname, GLdouble* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexGenfvEXT(GLenum texunit, GLenum coord, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexGenivEXT(GLenum texunit, GLenum coord, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexLevelParameterfvEXT(GLenum texunit, GLenum target, GLint level, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexLevelParameterivEXT(GLenum texunit, GLenum target, GLint level, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexParameterIivEXT(GLenum texunit, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexParameterIuivEXT(GLenum texunit, GLenum target, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexParameterfvEXT(GLenum texunit, GLenum target, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetMultiTexParameterivEXT(GLenum texunit, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetMultisamplefv(GLenum pname, GLuint index, GLfloat* val);
|
||||
MOBILEGL_GL_API void glGetNamedBufferParameteri64v(GLuint buffer, GLenum pname, GLint64* params);
|
||||
MOBILEGL_GL_API void glGetNamedBufferParameteriv(GLuint buffer, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedBufferParameterivEXT(GLuint buffer, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedBufferParameterui64vNV(GLuint buffer, GLenum pname, GLuint64EXT* params);
|
||||
MOBILEGL_GL_API void glGetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data);
|
||||
MOBILEGL_GL_API void glGetNamedBufferSubDataEXT(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data);
|
||||
MOBILEGL_GL_API void glGetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedFramebufferAttachmentParameterivEXT(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedFramebufferParameteriv(GLuint framebuffer, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API void glGetNamedFramebufferParameterivEXT(GLuint framebuffer, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedProgramivEXT(GLuint program, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedRenderbufferParameteriv(GLuint renderbuffer, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedRenderbufferParameterivEXT(GLuint renderbuffer, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetNamedStringivARB(GLint namelen, const GLchar* name, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLfloat glGetPathLengthNV(GLuint path, GLsizei startSegment, GLsizei numSegments);
|
||||
MOBILEGL_GL_API void glGetPathParameterfvNV(GLuint path, GLenum pname, GLfloat* value);
|
||||
MOBILEGL_GL_API void glGetPathParameterivNV(GLuint path, GLenum pname, GLint* value);
|
||||
MOBILEGL_GL_API void glGetPerfMonitorCounterDataAMD(GLuint monitor, GLenum pname, GLsizei dataSize, GLuint* data, GLint* bytesWritten);
|
||||
MOBILEGL_GL_API void glGetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLuint glGetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
MOBILEGL_GL_API GLint glGetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
MOBILEGL_GL_API GLint glGetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||
MOBILEGL_GL_API void glGetProgramResourcefvNV(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum* props, GLsizei count, GLsizei* length, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetProgramStageiv(GLuint program, GLenum shadertype, GLenum pname, GLint* values);
|
||||
MOBILEGL_GL_API void glGetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
MOBILEGL_GL_API void glGetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
MOBILEGL_GL_API void glGetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
MOBILEGL_GL_API void glGetQueryBufferObjectuiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||
MOBILEGL_GL_API void glGetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
||||
MOBILEGL_GL_API void glGetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
|
||||
MOBILEGL_GL_API void glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetQueryiv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetRenderbufferParameteriv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetShaderiv(GLuint shader, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLushort glGetStageIndexNV(GLenum shadertype);
|
||||
MOBILEGL_GL_API const GLubyte* glGetString(GLenum name);
|
||||
MOBILEGL_GL_API const GLubyte* glGetStringi(GLenum name, GLuint index);
|
||||
MOBILEGL_GL_API GLuint glGetSubroutineIndex(GLuint program, GLenum shadertype, const GLchar* name);
|
||||
MOBILEGL_GL_API GLint glGetSubroutineUniformLocation(GLuint program, GLenum shadertype, const GLchar* name);
|
||||
MOBILEGL_GL_API void glGetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
||||
MOBILEGL_GL_API void glGetTexLevelParameterfv(GLenum target, GLint level, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTexLevelParameteriv(GLenum target, GLint level, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTexParameterIiv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTexParameterIuiv(GLenum target, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetTexParameterfv(GLenum target, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTexParameteriv(GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLuint64 glGetTextureHandleARB(GLuint texture);
|
||||
MOBILEGL_GL_API GLuint64 glGetTextureHandleNV(GLuint texture);
|
||||
MOBILEGL_GL_API void glGetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTextureLevelParameterfvEXT(GLuint texture, GLenum target, GLint level, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTextureLevelParameterivEXT(GLuint texture, GLenum target, GLint level, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterIivEXT(GLuint texture, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterIuivEXT(GLuint texture, GLenum target, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterfvEXT(GLuint texture, GLenum target, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetTextureParameterivEXT(GLuint texture, GLenum target, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API GLuint64 glGetTextureSamplerHandleARB(GLuint texture, GLuint sampler);
|
||||
MOBILEGL_GL_API GLuint64 glGetTextureSamplerHandleNV(GLuint texture, GLuint sampler);
|
||||
MOBILEGL_GL_API void glGetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param);
|
||||
MOBILEGL_GL_API void glGetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param);
|
||||
MOBILEGL_GL_API void glGetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API GLuint glGetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName);
|
||||
MOBILEGL_GL_API GLint glGetUniformLocation(GLuint program, const GLchar* name);
|
||||
MOBILEGL_GL_API void glGetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param);
|
||||
MOBILEGL_GL_API void glGetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API void glGetVertexArrayIntegeri_vEXT(GLuint vaobj, GLuint index, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API void glGetVertexArrayIntegervEXT(GLuint vaobj, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API void glGetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param);
|
||||
MOBILEGL_GL_API void glGetVertexAttribIiv(GLuint index, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribIuiv(GLuint index, GLenum pname, GLuint* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribLdv(GLuint index, GLenum pname, GLdouble* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribLi64vNV(GLuint index, GLenum pname, GLint64EXT* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribLui64vARB(GLuint index, GLenum pname, GLuint64EXT* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribLui64vNV(GLuint index, GLenum pname, GLuint64EXT* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribdv(GLuint index, GLenum pname, GLdouble* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribfv(GLuint index, GLenum pname, GLfloat* params);
|
||||
MOBILEGL_GL_API void glGetVertexAttribiv(GLuint index, GLenum pname, GLint* params);
|
||||
MOBILEGL_GL_API void glHint(GLenum target, GLenum mode);
|
||||
MOBILEGL_GL_API void glIndexFormatNV(GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glInsertEventMarkerEXT(GLsizei length, const GLchar* marker);
|
||||
MOBILEGL_GL_API void glInterpolatePathsNV(GLuint resultPath, GLuint pathA, GLuint pathB, GLfloat weight);
|
||||
MOBILEGL_GL_API void glInvalidateBufferData(GLuint buffer);
|
||||
MOBILEGL_GL_API void glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr length);
|
||||
MOBILEGL_GL_API void glInvalidateTexImage(GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glInvalidateTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth);
|
||||
MOBILEGL_GL_API GLboolean glIsBuffer(GLuint buffer);
|
||||
MOBILEGL_GL_API GLboolean glIsBufferResidentNV(GLenum target);
|
||||
MOBILEGL_GL_API GLboolean glIsCommandListNV(GLuint list);
|
||||
MOBILEGL_GL_API GLboolean glIsEnabled(GLenum cap);
|
||||
MOBILEGL_GL_API GLboolean glIsEnabledIndexedEXT(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API GLboolean glIsEnabledi(GLenum target, GLuint index);
|
||||
MOBILEGL_GL_API GLboolean glIsFramebuffer(GLuint framebuffer);
|
||||
MOBILEGL_GL_API GLboolean glIsImageHandleResidentARB(GLuint64 handle);
|
||||
MOBILEGL_GL_API GLboolean glIsImageHandleResidentNV(GLuint64 handle);
|
||||
MOBILEGL_GL_API GLboolean glIsNamedBufferResidentNV(GLuint buffer);
|
||||
MOBILEGL_GL_API GLboolean glIsNamedStringARB(GLint namelen, const GLchar* name);
|
||||
MOBILEGL_GL_API GLboolean glIsPathNV(GLuint path);
|
||||
MOBILEGL_GL_API GLboolean glIsPointInFillPathNV(GLuint path, GLuint mask, GLfloat x, GLfloat y);
|
||||
MOBILEGL_GL_API GLboolean glIsPointInStrokePathNV(GLuint path, GLfloat x, GLfloat y);
|
||||
MOBILEGL_GL_API GLboolean glIsProgram(GLuint program);
|
||||
MOBILEGL_GL_API GLboolean glIsProgramPipeline(GLuint pipeline);
|
||||
MOBILEGL_GL_API GLboolean glIsQuery(GLuint id);
|
||||
MOBILEGL_GL_API GLboolean glIsRenderbuffer(GLuint renderbuffer);
|
||||
MOBILEGL_GL_API GLboolean glIsSampler(GLuint sampler);
|
||||
MOBILEGL_GL_API GLboolean glIsShader(GLuint shader);
|
||||
MOBILEGL_GL_API GLboolean glIsStateNV(GLuint state);
|
||||
MOBILEGL_GL_API GLboolean glIsSync(GLsync sync);
|
||||
MOBILEGL_GL_API GLboolean glIsTexture(GLuint texture);
|
||||
MOBILEGL_GL_API GLboolean glIsTextureHandleResidentARB(GLuint64 handle);
|
||||
MOBILEGL_GL_API GLboolean glIsTextureHandleResidentNV(GLuint64 handle);
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id);
|
||||
MOBILEGL_GL_API GLboolean glIsVertexArray(GLuint array);
|
||||
MOBILEGL_GL_API void glLabelObjectEXT(GLenum type, GLuint object, GLsizei length, const GLchar* label);
|
||||
MOBILEGL_GL_API void glLineWidth(GLfloat width);
|
||||
MOBILEGL_GL_API void glLinkProgram(GLuint program);
|
||||
MOBILEGL_GL_API void glLogicOp(GLenum opcode);
|
||||
MOBILEGL_GL_API void glMakeBufferNonResidentNV(GLenum target);
|
||||
MOBILEGL_GL_API void glMakeBufferResidentNV(GLenum target, GLenum access);
|
||||
MOBILEGL_GL_API void glMakeImageHandleNonResidentARB(GLuint64 handle);
|
||||
MOBILEGL_GL_API void glMakeImageHandleNonResidentNV(GLuint64 handle);
|
||||
MOBILEGL_GL_API void glMakeImageHandleResidentARB(GLuint64 handle, GLenum access);
|
||||
MOBILEGL_GL_API void glMakeImageHandleResidentNV(GLuint64 handle, GLenum access);
|
||||
MOBILEGL_GL_API void glMakeNamedBufferNonResidentNV(GLuint buffer);
|
||||
MOBILEGL_GL_API void glMakeNamedBufferResidentNV(GLuint buffer, GLenum access);
|
||||
MOBILEGL_GL_API void glMakeTextureHandleNonResidentARB(GLuint64 handle);
|
||||
MOBILEGL_GL_API void glMakeTextureHandleNonResidentNV(GLuint64 handle);
|
||||
MOBILEGL_GL_API void glMakeTextureHandleResidentARB(GLuint64 handle);
|
||||
MOBILEGL_GL_API void glMakeTextureHandleResidentNV(GLuint64 handle);
|
||||
MOBILEGL_GL_API void* glMapBuffer(GLenum target, GLenum access);
|
||||
MOBILEGL_GL_API void* glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
|
||||
MOBILEGL_GL_API void glMatrixFrustumEXT(GLenum mode, GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);
|
||||
MOBILEGL_GL_API void glMatrixLoadIdentityEXT(GLenum mode);
|
||||
MOBILEGL_GL_API void glMatrixOrthoEXT(GLenum mode, GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);
|
||||
MOBILEGL_GL_API void glMatrixPopEXT(GLenum mode);
|
||||
MOBILEGL_GL_API void glMatrixPushEXT(GLenum mode);
|
||||
MOBILEGL_GL_API void glMatrixRotatedEXT(GLenum mode, GLdouble ane, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glMatrixRotatefEXT(GLenum mode, GLfloat ane, GLfloat x, GLfloat y, GLfloat z);
|
||||
MOBILEGL_GL_API void glMatrixScaledEXT(GLenum mode, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glMatrixScalefEXT(GLenum mode, GLfloat x, GLfloat y, GLfloat z);
|
||||
MOBILEGL_GL_API void glMatrixTranslatedEXT(GLenum mode, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glMatrixTranslatefEXT(GLenum mode, GLfloat x, GLfloat y, GLfloat z);
|
||||
MOBILEGL_GL_API void glMaxShaderCompilerThreadsARB(GLuint count);
|
||||
MOBILEGL_GL_API void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||
MOBILEGL_GL_API void glMemoryBarrier(GLbitfield barriers);
|
||||
MOBILEGL_GL_API void glMemoryBarrierByRegion(GLbitfield barriers);
|
||||
MOBILEGL_GL_API void glMinSampleShading(GLfloat value);
|
||||
MOBILEGL_GL_API void glMultiDrawMeshTasksIndirectCountNV(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);
|
||||
MOBILEGL_GL_API void glMultiDrawMeshTasksIndirectNV(GLintptr indirect, GLsizei drawcount, GLsizei stride);
|
||||
MOBILEGL_GL_API void glMultiTexBufferEXT(GLenum texunit, GLenum target, GLenum internalformat, GLuint buffer);
|
||||
MOBILEGL_GL_API void glMultiTexCoordPointerEXT(GLenum texunit, GLint size, GLenum type, GLsizei stride, const void* pointer);
|
||||
MOBILEGL_GL_API void glMultiTexEnvfEXT(GLenum texunit, GLenum target, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glMultiTexEnviEXT(GLenum texunit, GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glMultiTexGendEXT(GLenum texunit, GLenum coord, GLenum pname, GLdouble param);
|
||||
MOBILEGL_GL_API void glMultiTexGenfEXT(GLenum texunit, GLenum coord, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glMultiTexGeniEXT(GLenum texunit, GLenum coord, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glMultiTexParameterfEXT(GLenum texunit, GLenum target, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glMultiTexParameteriEXT(GLenum texunit, GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glMultiTexRenderbufferEXT(GLenum texunit, GLenum target, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glNamedBufferAttachMemoryNV(GLuint buffer, GLuint memory, GLuint64 offset);
|
||||
MOBILEGL_GL_API void glNamedBufferData(GLuint buffer, GLsizeiptr size, const void* data, GLenum usage);
|
||||
MOBILEGL_GL_API void glNamedBufferDataEXT(GLuint buffer, GLsizeiptr size, const void* data, GLenum usage);
|
||||
MOBILEGL_GL_API void glNamedBufferPageCommitmentARB(GLuint buffer, GLintptr offset, GLsizeiptr size, GLboolean commit);
|
||||
MOBILEGL_GL_API void glNamedBufferPageCommitmentEXT(GLuint buffer, GLintptr offset, GLsizeiptr size, GLboolean commit);
|
||||
MOBILEGL_GL_API void glNamedBufferPageCommitmentMemNV(GLuint buffer, GLintptr offset, GLsizeiptr size, GLuint memory, GLuint64 memOffset, GLboolean commit);
|
||||
MOBILEGL_GL_API void glNamedBufferStorage(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags);
|
||||
MOBILEGL_GL_API void glNamedBufferStorageEXT(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags);
|
||||
MOBILEGL_GL_API void glNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||
MOBILEGL_GL_API void glNamedBufferSubDataEXT(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||
MOBILEGL_GL_API void glNamedCopyBufferSubDataEXT(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glNamedFramebufferDrawBuffer(GLuint framebuffer, GLenum buf);
|
||||
MOBILEGL_GL_API void glNamedFramebufferDrawBuffers(GLuint framebuffer, GLsizei n, const GLenum* bufs);
|
||||
MOBILEGL_GL_API void glNamedFramebufferParameteri(GLuint framebuffer, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glNamedFramebufferParameteriEXT(GLuint framebuffer, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glNamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
|
||||
MOBILEGL_GL_API void glNamedFramebufferRenderbuffer(GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glNamedFramebufferRenderbufferEXT(GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glNamedFramebufferSampleLocationsfvARB(GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glNamedFramebufferSampleLocationsfvNV(GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTexture(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTexture1DEXT(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTexture2DEXT(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTexture3DEXT(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint zoffset);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTextureEXT(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTextureFaceEXT(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLenum face);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTextureLayer(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);
|
||||
MOBILEGL_GL_API void glNamedFramebufferTextureLayerEXT(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParameter4dEXT(GLuint program, GLenum target, GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParameter4fEXT(GLuint program, GLenum target, GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParameterI4iEXT(GLuint program, GLenum target, GLuint index, GLint x, GLint y, GLint z, GLint w);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParameterI4uiEXT(GLuint program, GLenum target, GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParameters4fvEXT(GLuint program, GLenum target, GLuint index, GLsizei count, const GLfloat* params);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParametersI4ivEXT(GLuint program, GLenum target, GLuint index, GLsizei count, const GLint* params);
|
||||
MOBILEGL_GL_API void glNamedProgramLocalParametersI4uivEXT(GLuint program, GLenum target, GLuint index, GLsizei count, const GLuint* params);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorage(GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorageEXT(GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorageMultisample(GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorageMultisampleAdvancedAMD(GLuint renderbuffer, GLsizei samples, GLsizei storageSamples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorageMultisampleCoverageEXT(GLuint renderbuffer, GLsizei coverageSamples, GLsizei colorSamples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedRenderbufferStorageMultisampleEXT(GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glNamedStringARB(GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string);
|
||||
MOBILEGL_GL_API void glNormalFormatNV(GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
|
||||
MOBILEGL_GL_API void glObjectPtrLabel(const void* ptr, GLsizei length, const GLchar* label);
|
||||
MOBILEGL_GL_API void glPatchParameteri(GLenum pname, GLint value);
|
||||
MOBILEGL_GL_API void glPathCoverDepthFuncNV(GLenum func);
|
||||
MOBILEGL_GL_API void glPathParameterfNV(GLuint path, GLenum pname, GLfloat value);
|
||||
MOBILEGL_GL_API void glPathParameteriNV(GLuint path, GLenum pname, GLint value);
|
||||
MOBILEGL_GL_API void glPathStencilDepthOffsetNV(GLfloat factor, GLfloat units);
|
||||
MOBILEGL_GL_API void glPathStencilFuncNV(GLenum func, GLint ref, GLuint mask);
|
||||
MOBILEGL_GL_API void glPathStringNV(GLuint path, GLenum format, GLsizei length, const void* pathString);
|
||||
MOBILEGL_GL_API void glPixelStoref(GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glPixelStorei(GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glPointParameterf(GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glPointParameteri(GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glPointSize(GLfloat size);
|
||||
MOBILEGL_GL_API void glPolygonMode(GLenum face, GLenum mode);
|
||||
MOBILEGL_GL_API void glPolygonOffset(GLfloat factor, GLfloat units);
|
||||
MOBILEGL_GL_API void glPolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
|
||||
MOBILEGL_GL_API void glPolygonOffsetClampEXT(GLfloat factor, GLfloat units, GLfloat clamp);
|
||||
MOBILEGL_GL_API void glPrimitiveBoundingBoxARB(GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW);
|
||||
MOBILEGL_GL_API void glPrimitiveRestartIndex(GLuint index);
|
||||
MOBILEGL_GL_API void glProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
|
||||
MOBILEGL_GL_API void glProgramParameteri(GLuint program, GLenum pname, GLint value);
|
||||
MOBILEGL_GL_API void glProgramUniform1d(GLuint program, GLint location, GLdouble v0);
|
||||
MOBILEGL_GL_API void glProgramUniform1dEXT(GLuint program, GLint location, GLdouble x);
|
||||
MOBILEGL_GL_API void glProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1dvEXT(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1f(GLuint program, GLint location, GLfloat v0);
|
||||
MOBILEGL_GL_API void glProgramUniform1fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1i(GLuint program, GLint location, GLint v0);
|
||||
MOBILEGL_GL_API void glProgramUniform1i64ARB(GLuint program, GLint location, GLint64 x);
|
||||
MOBILEGL_GL_API void glProgramUniform1i64NV(GLuint program, GLint location, GLint64EXT x);
|
||||
MOBILEGL_GL_API void glProgramUniform1i64vARB(GLuint program, GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1i64vNV(GLuint program, GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1iv(GLuint program, GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1ui(GLuint program, GLint location, GLuint v0);
|
||||
MOBILEGL_GL_API void glProgramUniform1ui64ARB(GLuint program, GLint location, GLuint64 x);
|
||||
MOBILEGL_GL_API void glProgramUniform1ui64NV(GLuint program, GLint location, GLuint64EXT x);
|
||||
MOBILEGL_GL_API void glProgramUniform1ui64vARB(GLuint program, GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1ui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform1uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1);
|
||||
MOBILEGL_GL_API void glProgramUniform2dEXT(GLuint program, GLint location, GLdouble x, GLdouble y);
|
||||
MOBILEGL_GL_API void glProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2dvEXT(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2f(GLuint program, GLint location, GLfloat v0, GLfloat v1);
|
||||
MOBILEGL_GL_API void glProgramUniform2fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2i(GLuint program, GLint location, GLint v0, GLint v1);
|
||||
MOBILEGL_GL_API void glProgramUniform2i64ARB(GLuint program, GLint location, GLint64 x, GLint64 y);
|
||||
MOBILEGL_GL_API void glProgramUniform2i64NV(GLuint program, GLint location, GLint64EXT x, GLint64EXT y);
|
||||
MOBILEGL_GL_API void glProgramUniform2i64vARB(GLuint program, GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2i64vNV(GLuint program, GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2iv(GLuint program, GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2ui(GLuint program, GLint location, GLuint v0, GLuint v1);
|
||||
MOBILEGL_GL_API void glProgramUniform2ui64ARB(GLuint program, GLint location, GLuint64 x, GLuint64 y);
|
||||
MOBILEGL_GL_API void glProgramUniform2ui64NV(GLuint program, GLint location, GLuint64EXT x, GLuint64EXT y);
|
||||
MOBILEGL_GL_API void glProgramUniform2ui64vARB(GLuint program, GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2ui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform2uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
|
||||
MOBILEGL_GL_API void glProgramUniform3dEXT(GLuint program, GLint location, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3dvEXT(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3f(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
|
||||
MOBILEGL_GL_API void glProgramUniform3fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3i(GLuint program, GLint location, GLint v0, GLint v1, GLint v2);
|
||||
MOBILEGL_GL_API void glProgramUniform3i64ARB(GLuint program, GLint location, GLint64 x, GLint64 y, GLint64 z);
|
||||
MOBILEGL_GL_API void glProgramUniform3i64NV(GLuint program, GLint location, GLint64EXT x, GLint64EXT y, GLint64EXT z);
|
||||
MOBILEGL_GL_API void glProgramUniform3i64vARB(GLuint program, GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3i64vNV(GLuint program, GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3iv(GLuint program, GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3ui(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2);
|
||||
MOBILEGL_GL_API void glProgramUniform3ui64ARB(GLuint program, GLint location, GLuint64 x, GLuint64 y, GLuint64 z);
|
||||
MOBILEGL_GL_API void glProgramUniform3ui64NV(GLuint program, GLint location, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z);
|
||||
MOBILEGL_GL_API void glProgramUniform3ui64vARB(GLuint program, GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3ui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform3uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
|
||||
MOBILEGL_GL_API void glProgramUniform4dEXT(GLuint program, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
|
||||
MOBILEGL_GL_API void glProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4dvEXT(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4f(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
|
||||
MOBILEGL_GL_API void glProgramUniform4fv(GLuint program, GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4i(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
|
||||
MOBILEGL_GL_API void glProgramUniform4i64ARB(GLuint program, GLint location, GLint64 x, GLint64 y, GLint64 z, GLint64 w);
|
||||
MOBILEGL_GL_API void glProgramUniform4i64NV(GLuint program, GLint location, GLint64EXT x, GLint64EXT y, GLint64EXT z, GLint64EXT w);
|
||||
MOBILEGL_GL_API void glProgramUniform4i64vARB(GLuint program, GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4i64vNV(GLuint program, GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4iv(GLuint program, GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4ui(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
|
||||
MOBILEGL_GL_API void glProgramUniform4ui64ARB(GLuint program, GLint location, GLuint64 x, GLuint64 y, GLuint64 z, GLuint64 w);
|
||||
MOBILEGL_GL_API void glProgramUniform4ui64NV(GLuint program, GLint location, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z, GLuint64EXT w);
|
||||
MOBILEGL_GL_API void glProgramUniform4ui64vARB(GLuint program, GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4ui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glProgramUniform4uiv(GLuint program, GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glProgramUniformHandleui64ARB(GLuint program, GLint location, GLuint64 value);
|
||||
MOBILEGL_GL_API void glProgramUniformHandleui64NV(GLuint program, GLint location, GLuint64 value);
|
||||
MOBILEGL_GL_API void glProgramUniformHandleui64vARB(GLuint program, GLint location, GLsizei count, const GLuint64* values);
|
||||
MOBILEGL_GL_API void glProgramUniformHandleui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64* values);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x3dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x4dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x2dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x4dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x2dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x3dvEXT(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glProgramUniformui64NV(GLuint program, GLint location, GLuint64EXT value);
|
||||
MOBILEGL_GL_API void glProgramUniformui64vNV(GLuint program, GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glProvokingVertex(GLenum mode);
|
||||
MOBILEGL_GL_API void glPushClientAttribDefaultEXT(GLbitfield mask);
|
||||
MOBILEGL_GL_API void glPushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
|
||||
MOBILEGL_GL_API void glPushGroupMarkerEXT(GLsizei length, const GLchar* marker);
|
||||
MOBILEGL_GL_API void glQueryCounter(GLuint id, GLenum target);
|
||||
MOBILEGL_GL_API void glRasterSamplesEXT(GLuint samples, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glReadBuffer(GLenum src);
|
||||
MOBILEGL_GL_API void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glRenderbufferStorageMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glRenderbufferStorageMultisampleAdvancedAMD(GLenum target, GLsizei samples, GLsizei storageSamples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glRenderbufferStorageMultisampleCoverageNV(GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glResetMemoryObjectParameterNV(GLuint memory, GLenum pname);
|
||||
MOBILEGL_GL_API void glSampleCoverage(GLfloat value, GLboolean invert);
|
||||
MOBILEGL_GL_API void glSampleMaski(GLuint maskNumber, GLbitfield mask);
|
||||
MOBILEGL_GL_API void glSamplerParameterf(GLuint sampler, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glSamplerParameteri(GLuint sampler, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glScissor(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
MOBILEGL_GL_API void glScissorExclusiveArrayvNV(GLuint first, GLsizei count, const GLint* v);
|
||||
MOBILEGL_GL_API void glScissorExclusiveNV(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glSecondaryColorFormatNV(GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
|
||||
MOBILEGL_GL_API void glShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||
MOBILEGL_GL_API void glShadingRateImageBarrierNV(GLboolean synchronize);
|
||||
MOBILEGL_GL_API void glShadingRateImagePaletteNV(GLuint viewport, GLuint first, GLsizei count, const GLenum* rates);
|
||||
MOBILEGL_GL_API void glShadingRateSampleOrderNV(GLenum order);
|
||||
MOBILEGL_GL_API void glSignalVkFenceNV(GLuint64 vkFence);
|
||||
MOBILEGL_GL_API void glSignalVkSemaphoreNV(GLuint64 vkSemaphore);
|
||||
MOBILEGL_GL_API void glStateCaptureNV(GLuint state, GLenum mode);
|
||||
MOBILEGL_GL_API void glStencilFillPathNV(GLuint path, GLenum fillMode, GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilFunc(GLenum func, GLint ref, GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilMask(GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilMaskSeparate(GLenum face, GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
MOBILEGL_GL_API void glStencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
MOBILEGL_GL_API void glStencilStrokePathNV(GLuint path, GLint reference, GLuint mask);
|
||||
MOBILEGL_GL_API void glStencilThenCoverFillPathNV(GLuint path, GLenum fillMode, GLuint mask, GLenum coverMode);
|
||||
MOBILEGL_GL_API void glStencilThenCoverStrokePathNV(GLuint path, GLint reference, GLuint mask, GLenum coverMode);
|
||||
MOBILEGL_GL_API void glSubpixelPrecisionBiasNV(GLuint xbits, GLuint ybits);
|
||||
MOBILEGL_GL_API void glTexAttachMemoryNV(GLenum target, GLuint memory, GLuint64 offset);
|
||||
MOBILEGL_GL_API void glTexBuffer(GLenum target, GLenum internalformat, GLuint buffer);
|
||||
MOBILEGL_GL_API void glTexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glTexCoordFormatNV(GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glTexImage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTexImage3DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTexPageCommitmentARB(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLboolean commit);
|
||||
MOBILEGL_GL_API void glTexPageCommitmentMemNV(GLenum target, GLint layer, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLuint memory, GLuint64 offset, GLboolean commit);
|
||||
MOBILEGL_GL_API void glTexParameterf(GLenum target, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glTexParameteri(GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glTexStorage1DEXT(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
MOBILEGL_GL_API void glTexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glTexStorage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);
|
||||
MOBILEGL_GL_API void glTexStorage3DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTextureAttachMemoryNV(GLuint texture, GLuint memory, GLuint64 offset);
|
||||
MOBILEGL_GL_API void glTextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer);
|
||||
MOBILEGL_GL_API void glTextureBufferEXT(GLuint texture, GLenum target, GLenum internalformat, GLuint buffer);
|
||||
MOBILEGL_GL_API void glTextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glTextureBufferRangeEXT(GLuint texture, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glTexturePageCommitmentEXT(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLboolean commit);
|
||||
MOBILEGL_GL_API void glTexturePageCommitmentMemNV(GLuint texture, GLint layer, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLuint memory, GLuint64 offset, GLboolean commit);
|
||||
MOBILEGL_GL_API void glTextureParameterf(GLuint texture, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glTextureParameterfEXT(GLuint texture, GLenum target, GLenum pname, GLfloat param);
|
||||
MOBILEGL_GL_API void glTextureParameteri(GLuint texture, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glTextureParameteriEXT(GLuint texture, GLenum target, GLenum pname, GLint param);
|
||||
MOBILEGL_GL_API void glTextureRenderbufferEXT(GLuint texture, GLenum target, GLuint renderbuffer);
|
||||
MOBILEGL_GL_API void glTextureStorage1D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
MOBILEGL_GL_API void glTextureStorage1DEXT(GLuint texture, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
|
||||
MOBILEGL_GL_API void glTextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glTextureStorage2DEXT(GLuint texture, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glTextureStorage2DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTextureStorage2DMultisampleEXT(GLuint texture, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);
|
||||
MOBILEGL_GL_API void glTextureStorage3DEXT(GLuint texture, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);
|
||||
MOBILEGL_GL_API void glTextureStorage3DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTextureStorage3DMultisampleEXT(GLuint texture, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
|
||||
MOBILEGL_GL_API void glTextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers);
|
||||
MOBILEGL_GL_API void glTransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer);
|
||||
MOBILEGL_GL_API void glTransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
MOBILEGL_GL_API void glUniform1d(GLint location, GLdouble x);
|
||||
MOBILEGL_GL_API void glUniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniform1f(GLint location, GLfloat v0);
|
||||
MOBILEGL_GL_API void glUniform1fv(GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniform1i(GLint location, GLint v0);
|
||||
MOBILEGL_GL_API void glUniform1i64ARB(GLint location, GLint64 x);
|
||||
MOBILEGL_GL_API void glUniform1i64NV(GLint location, GLint64EXT x);
|
||||
MOBILEGL_GL_API void glUniform1i64vARB(GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glUniform1i64vNV(GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform1iv(GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glUniform1ui(GLint location, GLuint v0);
|
||||
MOBILEGL_GL_API void glUniform1ui64ARB(GLint location, GLuint64 x);
|
||||
MOBILEGL_GL_API void glUniform1ui64NV(GLint location, GLuint64EXT x);
|
||||
MOBILEGL_GL_API void glUniform1ui64vARB(GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glUniform1ui64vNV(GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform1uiv(GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glUniform2d(GLint location, GLdouble x, GLdouble y);
|
||||
MOBILEGL_GL_API void glUniform2dv(GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniform2f(GLint location, GLfloat v0, GLfloat v1);
|
||||
MOBILEGL_GL_API void glUniform2fv(GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniform2i(GLint location, GLint v0, GLint v1);
|
||||
MOBILEGL_GL_API void glUniform2i64ARB(GLint location, GLint64 x, GLint64 y);
|
||||
MOBILEGL_GL_API void glUniform2i64NV(GLint location, GLint64EXT x, GLint64EXT y);
|
||||
MOBILEGL_GL_API void glUniform2i64vARB(GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glUniform2i64vNV(GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform2iv(GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glUniform2ui(GLint location, GLuint v0, GLuint v1);
|
||||
MOBILEGL_GL_API void glUniform2ui64ARB(GLint location, GLuint64 x, GLuint64 y);
|
||||
MOBILEGL_GL_API void glUniform2ui64NV(GLint location, GLuint64EXT x, GLuint64EXT y);
|
||||
MOBILEGL_GL_API void glUniform2ui64vARB(GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glUniform2ui64vNV(GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform2uiv(GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glUniform3d(GLint location, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glUniform3dv(GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
|
||||
MOBILEGL_GL_API void glUniform3fv(GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniform3i(GLint location, GLint v0, GLint v1, GLint v2);
|
||||
MOBILEGL_GL_API void glUniform3i64ARB(GLint location, GLint64 x, GLint64 y, GLint64 z);
|
||||
MOBILEGL_GL_API void glUniform3i64NV(GLint location, GLint64EXT x, GLint64EXT y, GLint64EXT z);
|
||||
MOBILEGL_GL_API void glUniform3i64vARB(GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glUniform3i64vNV(GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform3iv(GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glUniform3ui(GLint location, GLuint v0, GLuint v1, GLuint v2);
|
||||
MOBILEGL_GL_API void glUniform3ui64ARB(GLint location, GLuint64 x, GLuint64 y, GLuint64 z);
|
||||
MOBILEGL_GL_API void glUniform3ui64NV(GLint location, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z);
|
||||
MOBILEGL_GL_API void glUniform3ui64vARB(GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glUniform3ui64vNV(GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform3uiv(GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glUniform4d(GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
|
||||
MOBILEGL_GL_API void glUniform4dv(GLint location, GLsizei count, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
|
||||
MOBILEGL_GL_API void glUniform4fv(GLint location, GLsizei count, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniform4i(GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
|
||||
MOBILEGL_GL_API void glUniform4i64ARB(GLint location, GLint64 x, GLint64 y, GLint64 z, GLint64 w);
|
||||
MOBILEGL_GL_API void glUniform4i64NV(GLint location, GLint64EXT x, GLint64EXT y, GLint64EXT z, GLint64EXT w);
|
||||
MOBILEGL_GL_API void glUniform4i64vARB(GLint location, GLsizei count, const GLint64* value);
|
||||
MOBILEGL_GL_API void glUniform4i64vNV(GLint location, GLsizei count, const GLint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform4iv(GLint location, GLsizei count, const GLint* value);
|
||||
MOBILEGL_GL_API void glUniform4ui(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
|
||||
MOBILEGL_GL_API void glUniform4ui64ARB(GLint location, GLuint64 x, GLuint64 y, GLuint64 z, GLuint64 w);
|
||||
MOBILEGL_GL_API void glUniform4ui64NV(GLint location, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z, GLuint64EXT w);
|
||||
MOBILEGL_GL_API void glUniform4ui64vARB(GLint location, GLsizei count, const GLuint64* value);
|
||||
MOBILEGL_GL_API void glUniform4ui64vNV(GLint location, GLsizei count, const GLuint64EXT* value);
|
||||
MOBILEGL_GL_API void glUniform4uiv(GLint location, GLsizei count, const GLuint* value);
|
||||
MOBILEGL_GL_API void glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
|
||||
MOBILEGL_GL_API void glUniformHandleui64ARB(GLint location, GLuint64 value);
|
||||
MOBILEGL_GL_API void glUniformHandleui64NV(GLint location, GLuint64 value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||
MOBILEGL_GL_API void glUniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
|
||||
MOBILEGL_GL_API void glUniformui64NV(GLint location, GLuint64EXT value);
|
||||
MOBILEGL_GL_API GLboolean glUnmapBuffer(GLenum target);
|
||||
MOBILEGL_GL_API GLboolean glUnmapNamedBuffer(GLuint buffer);
|
||||
MOBILEGL_GL_API GLboolean glUnmapNamedBufferEXT(GLuint buffer);
|
||||
MOBILEGL_GL_API void glUseProgram(GLuint program);
|
||||
MOBILEGL_GL_API void glUseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program);
|
||||
MOBILEGL_GL_API void glUseShaderProgramEXT(GLenum type, GLuint program);
|
||||
MOBILEGL_GL_API void glValidateProgram(GLuint program);
|
||||
MOBILEGL_GL_API void glValidateProgramPipeline(GLuint pipeline);
|
||||
MOBILEGL_GL_API void glVertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
|
||||
MOBILEGL_GL_API void glVertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayBindVertexBufferEXT(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
||||
MOBILEGL_GL_API void glVertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
|
||||
MOBILEGL_GL_API void glVertexArrayColorOffsetEXT(GLuint vaobj, GLuint buffer, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayEdgeFlagOffsetEXT(GLuint vaobj, GLuint buffer, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
|
||||
MOBILEGL_GL_API void glVertexArrayFogCoordOffsetEXT(GLuint vaobj, GLuint buffer, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayIndexOffsetEXT(GLuint vaobj, GLuint buffer, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayMultiTexCoordOffsetEXT(GLuint vaobj, GLuint buffer, GLenum texunit, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayNormalOffsetEXT(GLuint vaobj, GLuint buffer, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArraySecondaryColorOffsetEXT(GLuint vaobj, GLuint buffer, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayTexCoordOffsetEXT(GLuint vaobj, GLuint buffer, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribBindingEXT(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribDivisorEXT(GLuint vaobj, GLuint index, GLuint divisor);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribFormatEXT(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribIFormatEXT(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribIOffsetEXT(GLuint vaobj, GLuint buffer, GLuint index, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribLFormatEXT(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribLOffsetEXT(GLuint vaobj, GLuint buffer, GLuint index, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexAttribOffsetEXT(GLuint vaobj, GLuint buffer, GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexBindingDivisorEXT(GLuint vaobj, GLuint bindingindex, GLuint divisor);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
||||
MOBILEGL_GL_API void glVertexArrayVertexOffsetEXT(GLuint vaobj, GLuint buffer, GLint size, GLenum type, GLsizei stride, GLintptr offset);
|
||||
MOBILEGL_GL_API void glVertexAttrib1d(GLuint index, GLdouble x);
|
||||
MOBILEGL_GL_API void glVertexAttrib1f(GLuint index, GLfloat x);
|
||||
MOBILEGL_GL_API void glVertexAttrib1s(GLuint index, GLshort x);
|
||||
MOBILEGL_GL_API void glVertexAttrib2d(GLuint index, GLdouble x, GLdouble y);
|
||||
MOBILEGL_GL_API void glVertexAttrib2f(GLuint index, GLfloat x, GLfloat y);
|
||||
MOBILEGL_GL_API void glVertexAttrib2s(GLuint index, GLshort x, GLshort y);
|
||||
MOBILEGL_GL_API void glVertexAttrib3d(GLuint index, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glVertexAttrib3f(GLuint index, GLfloat x, GLfloat y, GLfloat z);
|
||||
MOBILEGL_GL_API void glVertexAttrib3s(GLuint index, GLshort x, GLshort y, GLshort z);
|
||||
MOBILEGL_GL_API void glVertexAttrib4Nub(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w);
|
||||
MOBILEGL_GL_API void glVertexAttrib4d(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
|
||||
MOBILEGL_GL_API void glVertexAttrib4f(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
|
||||
MOBILEGL_GL_API void glVertexAttrib4s(GLuint index, GLshort x, GLshort y, GLshort z, GLshort w);
|
||||
MOBILEGL_GL_API void glVertexAttribBinding(GLuint attribindex, GLuint bindingindex);
|
||||
MOBILEGL_GL_API void glVertexAttribDivisor(GLuint index, GLuint divisor);
|
||||
MOBILEGL_GL_API void glVertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexAttribFormatNV(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride);
|
||||
MOBILEGL_GL_API void glVertexAttribI1i(GLuint index, GLint x);
|
||||
MOBILEGL_GL_API void glVertexAttribI1ui(GLuint index, GLuint x);
|
||||
MOBILEGL_GL_API void glVertexAttribI2i(GLuint index, GLint x, GLint y);
|
||||
MOBILEGL_GL_API void glVertexAttribI2ui(GLuint index, GLuint x, GLuint y);
|
||||
MOBILEGL_GL_API void glVertexAttribI3i(GLuint index, GLint x, GLint y, GLint z);
|
||||
MOBILEGL_GL_API void glVertexAttribI3ui(GLuint index, GLuint x, GLuint y, GLuint z);
|
||||
MOBILEGL_GL_API void glVertexAttribI4i(GLuint index, GLint x, GLint y, GLint z, GLint w);
|
||||
MOBILEGL_GL_API void glVertexAttribI4ui(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);
|
||||
MOBILEGL_GL_API void glVertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexAttribIFormatNV(GLuint index, GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glVertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer);
|
||||
MOBILEGL_GL_API void glVertexAttribL1d(GLuint index, GLdouble x);
|
||||
MOBILEGL_GL_API void glVertexAttribL1i64NV(GLuint index, GLint64EXT x);
|
||||
MOBILEGL_GL_API void glVertexAttribL1ui64ARB(GLuint index, GLuint64EXT x);
|
||||
MOBILEGL_GL_API void glVertexAttribL1ui64NV(GLuint index, GLuint64EXT x);
|
||||
MOBILEGL_GL_API void glVertexAttribL2d(GLuint index, GLdouble x, GLdouble y);
|
||||
MOBILEGL_GL_API void glVertexAttribL2i64NV(GLuint index, GLint64EXT x, GLint64EXT y);
|
||||
MOBILEGL_GL_API void glVertexAttribL2ui64NV(GLuint index, GLuint64EXT x, GLuint64EXT y);
|
||||
MOBILEGL_GL_API void glVertexAttribL3d(GLuint index, GLdouble x, GLdouble y, GLdouble z);
|
||||
MOBILEGL_GL_API void glVertexAttribL3i64NV(GLuint index, GLint64EXT x, GLint64EXT y, GLint64EXT z);
|
||||
MOBILEGL_GL_API void glVertexAttribL3ui64NV(GLuint index, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z);
|
||||
MOBILEGL_GL_API void glVertexAttribL4d(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
|
||||
MOBILEGL_GL_API void glVertexAttribL4i64NV(GLuint index, GLint64EXT x, GLint64EXT y, GLint64EXT z, GLint64EXT w);
|
||||
MOBILEGL_GL_API void glVertexAttribL4ui64NV(GLuint index, GLuint64EXT x, GLuint64EXT y, GLuint64EXT z, GLuint64EXT w);
|
||||
MOBILEGL_GL_API void glVertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
MOBILEGL_GL_API void glVertexAttribLFormatNV(GLuint index, GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glVertexAttribLPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer);
|
||||
MOBILEGL_GL_API void glVertexAttribP1ui(GLuint index, GLenum type, GLboolean normalized, GLuint value);
|
||||
MOBILEGL_GL_API void glVertexAttribP2ui(GLuint index, GLenum type, GLboolean normalized, GLuint value);
|
||||
MOBILEGL_GL_API void glVertexAttribP3ui(GLuint index, GLenum type, GLboolean normalized, GLuint value);
|
||||
MOBILEGL_GL_API void glVertexAttribP4ui(GLuint index, GLenum type, GLboolean normalized, GLuint value);
|
||||
MOBILEGL_GL_API void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer);
|
||||
MOBILEGL_GL_API void glVertexBindingDivisor(GLuint bindingindex, GLuint divisor);
|
||||
MOBILEGL_GL_API void glVertexFormatNV(GLint size, GLenum type, GLsizei stride);
|
||||
MOBILEGL_GL_API void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
MOBILEGL_GL_API void glViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
MOBILEGL_GL_API void glViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
MOBILEGL_GL_API void glViewportPositionWScaleNV(GLuint index, GLfloat xcoeff, GLfloat ycoeff);
|
||||
MOBILEGL_GL_API void glViewportSwizzleNV(GLuint index, GLenum swizzlex, GLenum swizzley, GLenum swizzlez, GLenum swizzlew);
|
||||
MOBILEGL_GL_API void glWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||
MOBILEGL_GL_API void glWaitVkSemaphoreNV(GLuint64 vkSemaphore);
|
||||
|
||||
namespace MobileGL::ClientTrampoline::Gen {
|
||||
// Resolves a GL function name against the exported trampoline table
|
||||
// (direct &glXxx references - no dlsym, mirroring monolith
|
||||
// MobileGL::MG_Impl::GetProcAddress semantics).
|
||||
void* LookupGeneratedProc(const char* name);
|
||||
}
|
||||
|
||||
// End of File
|
||||
@@ -11,11 +11,6 @@
|
||||
#include <Init.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MG_Config::ServerOwnedWindowSurfaces (P12, MOBILEGL_IPC_SURFACE). Split-only: the pull build's
|
||||
// translation unit is unchanged (G1).
|
||||
#include <Config.h>
|
||||
#endif
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
@@ -132,16 +127,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
state->SetError(EGL_BAD_CONFIG);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 (on-screen server window), D1. With MOBILEGL_IPC_SURFACE=server and a remote server,
|
||||
// the window is the SERVER's: a headless client passes none, so NULL is not
|
||||
// EGL_BAD_NATIVE_WINDOW here. Whatever the client passed never reaches the wire - the
|
||||
// remote backend sends WindowKind::ServerOwned with token 0 (BackendObject_Remote).
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (IsNullNativeHandle(window) && !serverOwnedWindow) {
|
||||
#else
|
||||
if (IsNullNativeHandle(window)) {
|
||||
#endif
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -153,15 +139,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)),
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false)
|
||||
: state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
#else
|
||||
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
|
||||
#endif
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -698,14 +676,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (!state) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server
|
||||
// the window is the server's, and a headless client's NULL is accepted.
|
||||
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
|
||||
if (native_window == nullptr && !serverOwnedWindow) {
|
||||
#else
|
||||
if (native_window == nullptr) {
|
||||
#endif
|
||||
state->SetError(EGL_BAD_NATIVE_WINDOW);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -725,15 +696,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValueAttrib(attrib_list, EGL_HEIGHT, 0), 0)),
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EGLSurface surface = serverOwnedWindow
|
||||
? state->CreateServerOwnedWindowSurface(
|
||||
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
|
||||
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true)
|
||||
: state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
#else
|
||||
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
|
||||
#endif
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
@@ -1497,20 +1497,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (sb, CONTRACT-P5E.md §5.6). THE BIND-POINT GENERATION, and it is bumped HERE -
|
||||
// before the writes below and after every refusal above - rather than inside
|
||||
// BindingSlotRange1D, for two reasons. The slot has no back-pointer to the state
|
||||
// container that owns it, which is the same reason every other aggregate is bumped from
|
||||
// the entry point; and a bump on a bind that is about to be REJECTED by
|
||||
// ValidateBufferName is an over-fire the set-hash suppressor absorbs, while a bump
|
||||
// placed after the writes would be skipped by the `buffer == 0` early return below -
|
||||
// which is an unbind, the one case a shutter may least afford to miss.
|
||||
//
|
||||
// The dirty bits this feeds are 15/16/17 (Tracker.h); before P5e they shuttered on the
|
||||
// buffer CONTENT aggregate, which no bind has ever moved.
|
||||
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
|
||||
#endif
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
|
||||
@@ -1634,13 +1620,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (sb): the other half of BindBufferBase_State's bump - same generation, same
|
||||
// placement, same reason. A glBindBufferRange moves the point's EXTENT as well as what
|
||||
// is bound to it, and the record carries the resolved Offset/Size, so a range change
|
||||
// with no object change still has to reach the emitter.
|
||||
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
|
||||
#endif
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
|
||||
|
||||
@@ -11,16 +11,6 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced. P5b t2 converts ONE site in this file - the capture-ownership probe
|
||||
// in FixupGsStripCaptureOrder - for the reason CONTRACT-P5B.md §6.5 gives.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
@@ -537,7 +527,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(Clear);
|
||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||
}
|
||||
|
||||
@@ -546,7 +535,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -556,7 +544,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||
}
|
||||
|
||||
@@ -566,7 +553,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -576,7 +562,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -585,7 +570,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||
}
|
||||
|
||||
@@ -595,7 +579,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
@@ -605,7 +588,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -614,7 +596,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -624,7 +605,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||
maxdrawcount, stride);
|
||||
}
|
||||
@@ -635,7 +615,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||
stride);
|
||||
}
|
||||
@@ -646,7 +625,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||
basevertex);
|
||||
}
|
||||
@@ -657,7 +635,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -668,8 +645,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||
}
|
||||
@@ -680,7 +655,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstancedBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -691,8 +665,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawElementsInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||
instancecount, baseinstance);
|
||||
}
|
||||
@@ -703,7 +675,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
@@ -712,7 +683,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||
}
|
||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
@@ -721,8 +691,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawArraysInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||
baseinstance);
|
||||
}
|
||||
@@ -732,7 +700,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
@@ -741,7 +708,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||
}
|
||||
|
||||
@@ -773,7 +739,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
|
||||
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchCompute);
|
||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
@@ -826,7 +791,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -848,7 +812,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
|
||||
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
|
||||
MGP_FILL(PatchParameteri);
|
||||
patchParameteri(pname, value);
|
||||
}
|
||||
}
|
||||
@@ -919,7 +882,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
@@ -941,7 +903,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
}
|
||||
|
||||
@@ -955,7 +916,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support regional memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrierByRegion);
|
||||
memoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
@@ -1236,6 +1196,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void DrawMeshTasksNV(GLuint first, GLuint count) {}
|
||||
|
||||
void DrawMeshTasksIndirectNV(GLintptr indirect) {}
|
||||
|
||||
void MultiDrawMeshTasksIndirectNV(GLintptr indirect, GLsizei drawcount, GLsizei stride) {}
|
||||
|
||||
void MultiDrawMeshTasksIndirectCountNV(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride) {}
|
||||
|
||||
void BeginTransformFeedback(GLenum primitiveMode) {
|
||||
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1278,7 +1247,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||
MGP_FILL(BeginTransformFeedback);
|
||||
beginXfb(primitiveMode);
|
||||
}
|
||||
}
|
||||
@@ -1293,15 +1261,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
||||
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
||||
// already produced GL's vertex order, and reordering it again would corrupt it.
|
||||
//
|
||||
// P5b t2 (CONTRACT-P5B.md §6.5): UNDER SPLIT THE TABLE THIS USED TO ASK IS THE CLIENT'S
|
||||
// EMIT TABLE, whose EndTransformFeedback slot t2 just made non-null for every server -
|
||||
// so the raw null check would answer "the backend owns the capture" even against Magma,
|
||||
// which registers no XFB slot at all, and would skip a reorder Magma needs. The question
|
||||
// is about the SERVER's table, so it is answered from the bit the server publishes.
|
||||
// Under monolith (and in a pull build) this expands to the null check it replaced,
|
||||
// character for character.
|
||||
if (MGL_BACKEND_SLOT_CAP(EndTransformFeedback, MG_Pipe::kCapBackendOwnsXfbCapture)) {
|
||||
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||
@@ -1332,20 +1292,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<Uint>(bufferIndex));
|
||||
const auto& buffer = bindingPoint.GetBoundObject();
|
||||
if (buffer == nullptr) continue;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// This fixup READS the captured bytes back through the shadow, so it is the one
|
||||
// consumer that cannot simply inherit the deferral EndTransformFeedback's dropped
|
||||
// fence introduces. Under split it pays the reconciliation itself, which is the
|
||||
// same cost the fence used to charge every caller - here charged only to the
|
||||
// capture shapes that actually need reordering.
|
||||
//
|
||||
// TRANSPORT-GATED LIKE EVERY OTHER NEW SITE (D-J). Without the test this fires in
|
||||
// a build-split lane running MOBILEGL_TRANSPORT=monolith on Magma - whose
|
||||
// BeginXfbCaptureForDraw does mark the capture targets - where the fence at the
|
||||
// caller still runs, so the readback it emits is pure new work on the monolith
|
||||
// path and integration-gpu cannot see it.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) buffer->SyncGpuWrites();
|
||||
#endif
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
@@ -1383,37 +1329,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Closed while the capture state is still active: a backend that captures
|
||||
// through its own driver reads the capture program and buffer bindings here.
|
||||
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||
MGP_FILL(EndTransformFeedback);
|
||||
endXfb();
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 (b1), the second producer the client-side GPU-write set ADDS, and it has to be
|
||||
// taken HERE - before GLContext::EndTransformFeedback clears the live bindings, since
|
||||
// a mark taken after it marks nothing.
|
||||
MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets();
|
||||
#endif
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||
// End; the capture targets are host-coherent GPU memory, so completing
|
||||
// the GPU work is all that is required.
|
||||
//
|
||||
// P5 (b1): UNDER SPLIT THE UNBOUNDED WAIT GOES AND THE MARK ABOVE REPLACES IT. The
|
||||
// wait exists for one reason - so that a later MapBuffer sees real captured results -
|
||||
// and that is precisely what m_gpuWritePending says; SyncGpuWrites then pays for it
|
||||
// once, on the first read that actually wants the bytes, instead of on every
|
||||
// glEndTransformFeedback. A ~0ull ClientWaitSync on the GL thread is also the one
|
||||
// shape a verb barrier cannot make cheap, because it is the driver's wait and not the
|
||||
// barrier's.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
const Bool waitForTheCapture =
|
||||
MG_Config::Transport == MG_Config::TransportMode::Monolith;
|
||||
if (waitForTheCapture && backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
MGP_FILL(ClientWaitSync);
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
@@ -1432,7 +1358,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(true);
|
||||
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
|
||||
MGP_FILL(PauseTransformFeedback);
|
||||
pauseXfb();
|
||||
}
|
||||
}
|
||||
@@ -1447,7 +1372,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(false);
|
||||
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
|
||||
MGP_FILL(ResumeTransformFeedback);
|
||||
resumeXfb();
|
||||
}
|
||||
}
|
||||
@@ -1653,7 +1577,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
continue;
|
||||
}
|
||||
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
|
||||
MGP_FILL(DeleteTransformFeedback);
|
||||
deleteXfb(id);
|
||||
}
|
||||
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
|
||||
@@ -1685,7 +1608,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BindTransformFeedbackObject(id);
|
||||
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
|
||||
MGP_FILL(BindTransformFeedback);
|
||||
bindXfb(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
void Clear(GLbitfield mask);
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||
|
||||
void DrawMeshTasksNV(GLuint first, GLuint count);
|
||||
void DrawMeshTasksIndirectNV(GLintptr indirect);
|
||||
void MultiDrawMeshTasksIndirectNV(GLintptr indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawMeshTasksIndirectCountNV(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -108,24 +108,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawArrays, GLenum mode, GLint first, GLsizei cou
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElements, GLenum mode, GLsizei count, GLenum type, const void* indices) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElements, mode, count, type, indices)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, Enable, GLenum cap) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Enable, cap)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, EnableVertexAttribArray, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EnableVertexAttribArray, index)
|
||||
// P5e (ra), CONTRACT-P5E §2.5. STILL NOTHING ON THE WIRE - Flush and Finish have no record
|
||||
// (ARCHITECTURE §11) and this stays a no-op on every monolith build. What it is not any more
|
||||
// is a no-op on a RUN-AHEAD client: "the commands issued so far have completed" is a promise a
|
||||
// queue the client never waits on can break, and this is the only place an application can ask
|
||||
// for it. Under lockstep the client had already waited out every command it issued, so the
|
||||
// call really did have nothing to do; MGPipeClientFinish answers false-cheap there too.
|
||||
MOBILEGL_GL_API void glFinish() {
|
||||
MGLOG_D("Implementing function: %s(...)", __FUNCTION__);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MobileGL::MG_Pipe::MGPipeClientFinish();
|
||||
#endif
|
||||
}
|
||||
MOBILEGL_GL_API void glFlush() {
|
||||
MGLOG_D("Implementing function: %s(...)", __FUNCTION__);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MobileGL::MG_Pipe::MGPipeClientFlush();
|
||||
#endif
|
||||
}
|
||||
MOBILEGL_GL_API void glFinish() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); }
|
||||
MOBILEGL_GL_API void glFlush() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); }
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferRenderbuffer, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferRenderbuffer, target, attachment, renderbuffertarget, renderbuffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture2D, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture2D, target, attachment, textarget, texture, level)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FrontFace, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FrontFace, mode)
|
||||
@@ -2428,10 +2412,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BufferPageCommitmentMemNV, GLenum target, GL
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexPageCommitmentMemNV, GLenum target, GLint layer, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLuint memory, GLuint64 offset, GLboolean commit) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexPageCommitmentMemNV, target, layer, level, xoffset, yoffset, zoffset, width, height, depth, memory, offset, commit)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedBufferPageCommitmentMemNV, GLuint buffer, GLintptr offset, GLsizeiptr size, GLuint memory, GLuint64 memOffset, GLboolean commit) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedBufferPageCommitmentMemNV, buffer, offset, size, memory, memOffset, commit)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexturePageCommitmentMemNV, GLuint texture, GLint layer, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLuint memory, GLuint64 offset, GLboolean commit) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexturePageCommitmentMemNV, texture, layer, level, xoffset, yoffset, zoffset, width, height, depth, memory, offset, commit)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawMeshTasksNV, GLuint first, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawMeshTasksNV, first, count)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawMeshTasksIndirectNV, GLintptr indirect) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawMeshTasksIndirectNV, indirect)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiDrawMeshTasksIndirectNV, GLintptr indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiDrawMeshTasksIndirectNV, indirect, drawcount, stride)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiDrawMeshTasksIndirectCountNV, GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiDrawMeshTasksIndirectCountNV, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawMeshTasksNV, GLuint first, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawMeshTasksNV, first, count)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawMeshTasksIndirectNV, GLintptr indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawMeshTasksIndirectNV, indirect)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawMeshTasksIndirectNV, GLintptr indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawMeshTasksIndirectNV, indirect, drawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawMeshTasksIndirectCountNV, GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawMeshTasksIndirectCountNV, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenOcclusionQueriesNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenOcclusionQueriesNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteOcclusionQueriesNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteOcclusionQueriesNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsOcclusionQueryNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsOcclusionQueryNV, id)
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c). This file is the ONLY place every DSA framebuffer entry point lives, and the
|
||||
// emitter it reaches is this package's own header rather than a declaration in one of the
|
||||
// contract's: MG_Pipe/PipeMutation.h is the door MG_State has into the client and carries no
|
||||
// framebuffer row, and MG_Impl/GLImpl and MG_Impl/Pipe are the same layer (this file already
|
||||
// includes MG_Impl/Pipe/PipeFill.h for MGP_FILL).
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#endif
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -624,40 +612,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
|
||||
//
|
||||
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
|
||||
// ID-19 it held only the two BOUND-target records, and the emitter only ever built them
|
||||
// at the validate point out of the two bindings. So glClearNamedFramebufferfv(fbo) or
|
||||
// glBlitNamedFramebuffer(..., fbo, ...) on an fbo bound to NEITHER binding reached a
|
||||
// backend that minted a fresh driver framebuffer with no attachments, found no record
|
||||
// for it, declined, and issued the clear against it anyway: GL_INVALID_FRAMEBUFFER_-
|
||||
// OPERATION and nothing cleared, where the legacy arm cleared correctly.
|
||||
//
|
||||
// TWO CLASSES OF SITE call this, and both are "the point at which the object is final
|
||||
// for this call": the five CONSUMERS (blit and the four clears) publish immediately
|
||||
// before MGP_FILL, so the record precedes the verb that hands the object over and a
|
||||
// later bound-target record for the same object still wins; the ten MUTATORS (the DSA
|
||||
// attachment, draw-buffer and read-buffer setters) publish immediately after the
|
||||
// frontend mutation, because they have no validate point at all - FillPoints.def has no
|
||||
// verb for any of them, so there is no MGP_FILL to sit in front of.
|
||||
//
|
||||
// A CALL THAT MOVED NOTHING IS FREE: the record's ContentHash is the emitter's own
|
||||
// suppressor and it is keyed per framebuffer object, so a redundant publish emits zero
|
||||
// bytes. EmitFramebufferByName picks Draw/Read/Both over Named when the object IS
|
||||
// bound, so a Named record can never overwrite a bound record's Target underneath the
|
||||
// binding that resolves through it.
|
||||
void PipePublishFramebufferByName(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo) {
|
||||
if (!fbo) return;
|
||||
MG_Pipe::MGPipeFramebufferEmitterInstance().EmitFramebufferByName(*fbo);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
MGP_FILL(BlitFramebuffer);
|
||||
MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||
mask, filter);
|
||||
}
|
||||
@@ -671,11 +629,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(readFramebuffer);
|
||||
PipePublishFramebufferByName(drawFramebuffer);
|
||||
#endif
|
||||
MGP_FILL(BlitNamedFramebuffer);
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
dstY1, mask, filter);
|
||||
}
|
||||
@@ -687,10 +640,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfv);
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -701,10 +650,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfi);
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
@@ -715,10 +660,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferiv);
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -729,10 +670,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferuiv);
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -1460,9 +1397,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1489,9 +1423,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
||||
@@ -1515,9 +1446,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1545,9 +1473,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
||||
@@ -1602,9 +1527,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1707,9 +1629,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
||||
@@ -1779,9 +1698,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (renderbuffer == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1791,9 +1707,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!renderbufferObject) return;
|
||||
|
||||
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
||||
@@ -1993,9 +1906,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
||||
if (!framebufferObject) return;
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
||||
@@ -2010,9 +1920,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
||||
@@ -2022,9 +1929,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!framebufferObject) return;
|
||||
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
||||
"NamedFramebufferReadBuffer_State");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
||||
@@ -2825,28 +2729,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferfi);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferfv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferuiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -3094,11 +2994,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels);
|
||||
// The split emitter waits for an owned pixel reply, then uploads its
|
||||
// packed rows into the client PBO. Its shadow is already authoritative;
|
||||
// marking another GPU write here would invent a readback that never ran.
|
||||
}
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
|
||||
@@ -18,13 +18,6 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
|
||||
// GUARDED AT THE INCLUDE, not just around the call. This file compiles in the PULL build too,
|
||||
// and G1 requires that build's symbol set and .text to be byte-identical - a header that drags
|
||||
// in MG_Remote would change it whether or not any code path reaches the session.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#endif
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
@@ -37,12 +30,6 @@
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
@@ -1186,16 +1173,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetMinComputeWorkGroupSize(index);
|
||||
GLint backendValue = 0;
|
||||
if (getIntegeri) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Transport's Class A caps mirror answers these two pnames
|
||||
// locally and emits no record. A fill would wrongly quiesce
|
||||
// the apply thread before every compute dispatch validation.
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) {
|
||||
#endif
|
||||
MGP_FILL(GetIntegeri_v);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
}
|
||||
#endif
|
||||
getIntegeri(target, index, &backendValue);
|
||||
}
|
||||
*data = std::max(backendValue, minimum);
|
||||
@@ -1375,17 +1352,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
|
||||
// timestamp is not a static property, so R-15 does not reach it - and the
|
||||
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
|
||||
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
|
||||
//
|
||||
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
|
||||
// build and G1 cannot see this edit: the Bool-valued spelling moved this
|
||||
// function by -150 bytes for no behavioural reason at all.
|
||||
if (const auto getGpuTimestampNs =
|
||||
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
@@ -2295,17 +2262,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TIMESTAMP: {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
|
||||
// timestamp is not a static property, so R-15 does not reach it - and the
|
||||
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
|
||||
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
|
||||
//
|
||||
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
|
||||
// build and G1 cannot see this edit: the Bool-valued spelling moved this
|
||||
// function by -150 bytes for no behavioural reason at all.
|
||||
if (const auto getGpuTimestampNs =
|
||||
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
@@ -2860,27 +2817,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLenum GetGraphicsResetStatus() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P6 `dl` (CONTRACT-P6 5.3): THE ONE GL-VISIBLE CONSEQUENCE OF A DEAD SERVER.
|
||||
//
|
||||
// Before this, "a dead server leaves the client silently DECLINE-ing every verb forever
|
||||
// with glGetError clean" - the contract's own words, and the reason the latch is a
|
||||
// package rather than carriage. An application that asks the robustness question has no
|
||||
// other way to find out, because a DECLINE is indistinguishable from a verb that
|
||||
// happened.
|
||||
//
|
||||
// GL_UNKNOWN_CONTEXT_RESET, not GUILTY or INNOCENT. Those two assign BLAME - guilty
|
||||
// means this context's commands caused the reset, innocent means another context's did -
|
||||
// and neither is knowable here: the server died for a reason that never crossed the
|
||||
// wire, and it could as easily have been a driver fault as this client's draw. UNKNOWN is
|
||||
// the spec's answer for exactly that, and it is the one that does not lie.
|
||||
//
|
||||
// WHOLLY INSIDE THE GUARD, including the include above. In a pull build this function is
|
||||
// byte-for-byte what it was, which is what G1 measures.
|
||||
if (MG_Remote::Client::ClientSession::DeviceLost()) {
|
||||
return GL_UNKNOWN_CONTEXT_RESET;
|
||||
}
|
||||
#endif
|
||||
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
|
||||
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
|
||||
// device after every case (gl3cTestPackages.cpp:121) and, under the default
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
||||
@@ -3399,7 +3398,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support shader storage block binding."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ShaderStorageBlockBinding);
|
||||
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,6 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read, whatever class the slot itself is in.
|
||||
// The two macros carry that rule; in a pull build each expands to exactly the check it replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -169,7 +164,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ResetQueryObjectLocked(QueryObject* queryObject) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -184,7 +178,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
|
||||
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
|
||||
if (endTimeElapsedQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndTimeElapsedQuery);
|
||||
endTimeElapsedQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -264,7 +257,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
Uint64 result = 0;
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (queryObject->backendHandle && getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
|
||||
// Not ready. The whole point of the no-wait form is that the caller's
|
||||
@@ -279,7 +271,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -295,7 +286,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
|
||||
MGP_FILL(IsQueryResultAvailable);
|
||||
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
|
||||
return true;
|
||||
}
|
||||
@@ -307,7 +297,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Uint64 result = 0;
|
||||
if (queryObject->backendHandle) {
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
|
||||
// The backend could not produce the result YET (e.g. a
|
||||
@@ -328,7 +317,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -434,7 +422,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -454,7 +441,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -482,7 +468,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
@@ -532,27 +518,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} else if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const Bool xfbQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(BeginXfbPrimitivesQuery, MG_Pipe::kCapXfbPrimitivesQuery);
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
MGP_FILL(BeginXfbPrimitivesQuery);
|
||||
queryObject->backendHandle =
|
||||
xfbQuerySupported ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||
queryObject->accountedCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
||||
queryObject->geometryCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
||||
} else if (isOcclusionQuery) {
|
||||
MGP_FILL(BeginOcclusionQuery);
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const Bool timerQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(BeginTimeElapsedQuery, MG_Pipe::kCapTimerQuery);
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
MGP_FILL(BeginTimeElapsedQuery);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && timerQuerySupported) ? beginTimeElapsedQuery() : nullptr;
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
activeQueryId = id;
|
||||
}
|
||||
@@ -563,7 +542,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
@@ -600,7 +579,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
MGP_FILL(EndXfbPrimitivesQuery);
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
}
|
||||
@@ -610,7 +588,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -627,7 +604,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -665,11 +641,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
const Bool timerQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(QueryCounterTimestamp, MG_Pipe::kCapTimerQuery);
|
||||
const auto queryCounterTimestamp =
|
||||
timerQuerySupported ? MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp : nullptr;
|
||||
MGP_FILL(QueryCounterTimestamp);
|
||||
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||
queryObject->ended = true;
|
||||
@@ -778,15 +750,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
case GL_QUERY_COUNTER_BITS: {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
(target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED)) {
|
||||
// Both wire query paths now return native GPU counters. GLES'
|
||||
// core counter has 32 bits, so advertise that shared lower bound.
|
||||
*params = MGL_BACKEND_SLOT_CAP(BeginXfbPrimitivesQuery, MG_Pipe::kCapXfbPrimitivesQuery) ? 32 : 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// 64 bits are advertised only while the live backend can actually
|
||||
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
@@ -802,13 +765,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||
return;
|
||||
}
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
MGP_FILL(IsTimerQuerySupported);
|
||||
const Bool supported =
|
||||
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
|
||||
*params = supported ? 64 : 0;
|
||||
@@ -950,7 +912,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
#include "GL_Sync.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -61,11 +55,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto* syncObject = new SyncObject;
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
// P5b: FenceSync is now a class-B emitter under split. Its server sink keeps the
|
||||
// backend's optional/null-native fallback; the client must reach the wire first.
|
||||
// This is the same pointer expression MGL_BACKEND_SLOT_PTR_LOCAL had in a pull build.
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
@@ -104,7 +94,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
MGP_FILL(ClientWaitSync);
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
@@ -130,7 +119,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
MGP_FILL(WaitSync);
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +139,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
@@ -187,7 +174,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
MGP_FILL(GetSyncStatus);
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
@@ -241,7 +227,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "MG_Util/Types.h"
|
||||
#include "Validators.h"
|
||||
#include "ProxyTexture.h"
|
||||
#include "MipmapGenerationPlan.h"
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
@@ -31,15 +30,6 @@
|
||||
#include <MG_Impl/GLImpl/Sampler/Validators.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
// P4a, ID-18 M2. The ONE door MG_State and MG_Impl have into the client's emitters; the three
|
||||
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -508,36 +498,39 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return size;
|
||||
}
|
||||
|
||||
// glGenerateMipmap defines levels BASE_LEVEL+1 up to the level the base image's extent and
|
||||
// MAX_LEVEL admit, and leaves every other level exactly as it was (GL 4.6 core 8.17). Both
|
||||
// transports take this one path: the window, and the refusal to redefine the levels around
|
||||
// it, are properties of the GL call rather than of how the backend is reached.
|
||||
Bool EnsureGeneratedMipmapStorageAllocated(
|
||||
MG_State::GLState::TextureObjectMipmap& texture,
|
||||
TextureUploadTarget uploadTarget) {
|
||||
const auto plan = ComputeMipmapGenerationRange(texture, uploadTarget);
|
||||
if (plan.End <= plan.Base) return false;
|
||||
// Immutable textures and views already own every level generation can write.
|
||||
// Allocating through a view would redefine its owner's levels at the VIEW's base
|
||||
// extent - one slice of the owner's layers - destroying the storage of every layer and
|
||||
// level outside the view window.
|
||||
if (texture.IsImmutable()) return true;
|
||||
const IntVec3 baseSize = texture.GetMipmapTexelSize(uploadTarget, plan.Base);
|
||||
const SizeT baseBytes = texture.GetMipmapByteSize(uploadTarget, plan.Base);
|
||||
const SizeT texels = static_cast<SizeT>(baseSize.x()) * baseSize.y() * baseSize.z();
|
||||
if (!baseBytes || !texels || baseBytes % texels) return false;
|
||||
const SizeT pixelBytes = baseBytes / texels;
|
||||
const Int axes = MipShrinkingAxisCount(texture.GetTarget());
|
||||
for (Uint level = plan.Base + 1; level < plan.End; ++level) {
|
||||
const auto size = ComputeMipmapTexelSize(baseSize, level - plan.Base, axes);
|
||||
const SizeT bytes = pixelBytes * static_cast<SizeT>(size.x()) * size.y() * size.z();
|
||||
texture.AllocateStorage(uploadTarget, level, {size, bytes});
|
||||
const Uint existingLevelCount = texture.GetMipmapLevelCount();
|
||||
if (existingLevelCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, 0);
|
||||
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, 0);
|
||||
const SizeT baseTexelCount = static_cast<SizeT>(baseTexelSize.x()) *
|
||||
static_cast<SizeT>(baseTexelSize.y()) *
|
||||
static_cast<SizeT>(baseTexelSize.z());
|
||||
if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 ||
|
||||
baseByteSize == 0 || baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
|
||||
for (Uint level = 1; level < requiredLevelCount; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, shrinkingAxes);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
||||
texture.MarkStorageDirty(uploadTarget, level, false);
|
||||
}
|
||||
// Levels before BASE_LEVEL and after the generated end remain valid images and are left
|
||||
// alone: neither MAX_LEVEL nor a generate truncates the chain. AllocateStorage only
|
||||
// grows, so a longer chain a level-0 respecification left behind is dropped there
|
||||
// (DiscardMipmapChainOnBaseRespecification) rather than here.
|
||||
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
|
||||
// grows, so a previously longer chain (a bigger base image before respecification) would
|
||||
// otherwise keep a tail of stale levels here and read as incomplete.
|
||||
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
|
||||
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
|
||||
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
|
||||
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
|
||||
@@ -1083,7 +1076,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Vector<Uint8> scratch(static_cast<SizeT>(width) * static_cast<SizeT>(height) * bytesPerTexel);
|
||||
{
|
||||
ScopedNeutralPackState neutralPack;
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data());
|
||||
}
|
||||
|
||||
@@ -1343,30 +1335,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||
@@ -1445,30 +1413,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
@@ -1675,7 +1619,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GenerateMipmap_Backend(GLenum target) {
|
||||
MGP_FILL(GenerateMipmap);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
|
||||
}
|
||||
|
||||
@@ -2159,30 +2102,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||
// Wrong pixels, not a lost optimisation.
|
||||
//
|
||||
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||
//
|
||||
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
||||
@@ -4105,7 +4024,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexSubImage2D_Backend(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height) {
|
||||
MGP_FILL(CopyTexSubImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
@@ -4122,7 +4040,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support image-to-image copies."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(CopyImageSubData);
|
||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
@@ -4544,7 +4461,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexImage2D_Backend(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
MGP_FILL(CopyTexImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D(target, level, internalformat, x, y, width, height,
|
||||
border);
|
||||
}
|
||||
@@ -5155,7 +5071,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetTexImage_Backend(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
MGP_FILL(GetTexImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
|
||||
@@ -6075,10 +5990,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
viewObject->SetInternalFormat(viewInternalFormat);
|
||||
viewObject->SetSamples(storageOwner->GetSamples());
|
||||
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
|
||||
@@ -6539,18 +6450,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
@@ -6566,14 +6468,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
__func__)) {
|
||||
return;
|
||||
}
|
||||
auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Unlike glGetTexImage's face target, this DSA call reads all six faces.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
textureObject->GetTarget() == TextureTarget::TextureCubeMap)
|
||||
uploadTarget = TextureUploadTarget::Unknown;
|
||||
#endif
|
||||
GetTextureImageForUploadTarget(textureObject, uploadTarget, level, format, type,
|
||||
GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
|
||||
bufSize, pixels, __func__);
|
||||
}
|
||||
|
||||
@@ -6762,17 +6657,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
|
||||
.Bind(textureObject, level, layered, layer, access, format);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// AND THE IMAGE-UNIT MARK BESIDE IT. The line above moves the TEXTURE-unit high-water
|
||||
// mark, which is a different array: a reader that needs "the highest image unit ever
|
||||
// bound" cannot take it from there without either over-walking (a texture bind at unit
|
||||
// 31 with no image bound anywhere) or, worse, under-walking if that line ever moves. The
|
||||
// split client's per-draw writable-image sweep is that reader
|
||||
// (MG_Remote/Client/GpuWritePending.cpp). Push builds only, so the pull build's bytes do
|
||||
// not move (G1).
|
||||
MG_State::pGLContext->NoteImageUnitTouched(static_cast<Int>(unit));
|
||||
#endif
|
||||
MGP_FILL(BindImageTexture);
|
||||
bindImageTexture(unit, texture, level, layered, layer, access, format);
|
||||
}
|
||||
|
||||
@@ -6828,13 +6712,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
if (!GetTexImage_State(target, level, format, type, pixels)) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
|
||||
if (MG_Backend::gBackendFunctionsTable.GL.GetTexImage != nullptr) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Texture/MipmapGenerationPlan.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>
|
||||
|
||||
// The plan is the GL call's own window, not a transport's, so both arms of
|
||||
// EnsureGeneratedMipmapStorageAllocated compute it here and the wire carrier
|
||||
// (MGPMipPlan::LevelCount) publishes the same end-exclusive number.
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
struct MipmapGenerationRange {
|
||||
Uint Base = 0;
|
||||
Uint End = 0; // logical end-exclusive, including the source base level
|
||||
};
|
||||
|
||||
inline MipmapGenerationRange ComputeMipmapGenerationRange(
|
||||
const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget uploadTarget) {
|
||||
const auto range = texture.GetLevelRange();
|
||||
MipmapGenerationRange result{range.x(), range.x()};
|
||||
const Uint available = texture.GetMipmapLevelCount();
|
||||
if (result.Base >= available || range.y() < result.Base) return result;
|
||||
const auto extent = texture.GetMipmapTexelSize(uploadTarget, result.Base);
|
||||
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return result;
|
||||
Int largest = extent.x();
|
||||
if (texture.GetTarget() != TextureTarget::Texture1D &&
|
||||
texture.GetTarget() != TextureTarget::Texture1DArray)
|
||||
largest = std::max(largest, extent.y());
|
||||
if (texture.GetTarget() == TextureTarget::Texture3D) largest = std::max(largest, extent.z());
|
||||
Uint levels = 1;
|
||||
while (largest > 1) { largest /= 2; ++levels; }
|
||||
Uint64 end = std::min(static_cast<Uint64>(result.Base) + levels,
|
||||
static_cast<Uint64>(range.y()) + 1);
|
||||
// A view's level count is already relative to its own window. Neither
|
||||
// immutable storage nor a view may acquire levels beyond that allocation.
|
||||
if (texture.IsImmutable()) end = std::min(end, static_cast<Uint64>(available));
|
||||
result.End = static_cast<Uint>(end);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
}
|
||||
|
||||
struct ContextObject {
|
||||
::Display* XDisplay = nullptr;
|
||||
Display* XDisplay = nullptr;
|
||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||
EGLConfig Config = nullptr;
|
||||
EGLContext Context = EGL_NO_CONTEXT;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user