Compare commits

..
Author SHA1 Message Date
HH45137 e3fac5e9fa [Fix]: Add Task/Mesh shader enum mapping
Map the Task and Mesh shader stages to their NV OpenGL enum values in ProgramEnumConverter so shader stage conversion handles task and mesh pipelines correctly.
2026-10-12 00:41:40 +08:00
HH45137 3823fe528c [Feat, Test]: Add NV mesh shader support
Advertise GL_NV_mesh_shader in the DirectVulkan backend and expose the new task/mesh shader stages in the GL-to-MG and glslang conversion layers. This also wires the mesh task drawing entry points and adds a focused MeshShader test covering extension discovery and task shader compilation.
2026-10-12 00:20:53 +08:00
swung0x48 08124c99f1 [Test] (trace): replace the create-indirect fixture with minecraft-1.21.1-neoforge-create-indirect-in-world-align1024, a derived trace and not the original capture - the desktop capture bound Flywheel's scatter lists at 16-byte SSBO offsets that Adreno 830 and Mali's Vulkan driver (64) reject, and fixture_tools/realign_buffer_ranges.cpp moves the 1,270 binds off a 1024-byte boundary onto 1024-aligned slots (3,812 calls inserted, target call 504631 -> 508443, host SSIM unchanged to 9 digits on both backends) 2026-09-30 10:00:26 -04:00
1611 changed files with 3723 additions and 734981 deletions
+2 -27
View File
@@ -61,40 +61,15 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
# entry icon and the bottom navigation bar switches to the render server screen. What is
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
# reds here instead of shipping two icons.
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
if [[ "$main_count" -ne 1 ]]; then
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+27 -157
View File
@@ -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 -2998
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -28,4 +28,3 @@ tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
-3
View File
@@ -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
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
-538
View File
@@ -1,538 +0,0 @@
/*
* Copyright © 2011 Intel Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Authors:
* Benjamin Franzke <benjaminfranzke@googlemail.com>
*/
#ifndef _GBM_H_
#define _GBM_H_
#define __GBM__ 1
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file gbm.h
* \brief Generic Buffer Manager
*/
struct gbm_device;
struct gbm_bo;
struct gbm_surface;
/**
* \mainpage The Generic Buffer Manager
*
* This module provides an abstraction that the caller can use to request a
* buffer from the underlying memory management system for the platform.
*
* This allows the creation of portable code whilst still allowing access to
* the underlying memory manager.
*/
/**
* Abstraction representing the handle to a buffer allocated by the
* manager
*/
union gbm_bo_handle {
void *ptr;
int32_t s32;
uint32_t u32;
int64_t s64;
uint64_t u64;
};
/** Format of the allocated buffer */
enum gbm_bo_format {
/** RGB with 8 bits per channel in a 32 bit value */
GBM_BO_FORMAT_XRGB8888,
/** ARGB with 8 bits per channel in a 32 bit value */
GBM_BO_FORMAT_ARGB8888
};
/**
* The FourCC format codes are taken from the drm_fourcc.h definition, and
* re-namespaced. New GBM formats must not be added, unless they are
* identical ports from drm_fourcc.
*/
#define __gbm_fourcc_code(a,b,c,d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
#define GBM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */
/* color index */
#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
/* 8 bpp Red */
#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
/* 16 bpp Red */
#define GBM_FORMAT_R16 __gbm_fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
/* 16 bpp RG */
#define GBM_FORMAT_GR88 __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
/* 32 bpp RG */
#define GBM_FORMAT_RG1616 __gbm_fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */
#define GBM_FORMAT_GR1616 __gbm_fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */
/* 8 bpp RGB */
#define GBM_FORMAT_RGB332 __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
#define GBM_FORMAT_BGR233 __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
/* 16 bpp RGB */
#define GBM_FORMAT_XRGB4444 __gbm_fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
#define GBM_FORMAT_XBGR4444 __gbm_fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
#define GBM_FORMAT_RGBX4444 __gbm_fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
#define GBM_FORMAT_BGRX4444 __gbm_fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
#define GBM_FORMAT_ARGB4444 __gbm_fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
#define GBM_FORMAT_ABGR4444 __gbm_fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
#define GBM_FORMAT_RGBA4444 __gbm_fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
#define GBM_FORMAT_BGRA4444 __gbm_fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
#define GBM_FORMAT_XRGB1555 __gbm_fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
#define GBM_FORMAT_XBGR1555 __gbm_fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
#define GBM_FORMAT_RGBX5551 __gbm_fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
#define GBM_FORMAT_BGRX5551 __gbm_fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
#define GBM_FORMAT_ARGB1555 __gbm_fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
#define GBM_FORMAT_ABGR1555 __gbm_fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
#define GBM_FORMAT_RGBA5551 __gbm_fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
#define GBM_FORMAT_BGRA5551 __gbm_fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
#define GBM_FORMAT_RGB565 __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
#define GBM_FORMAT_BGR565 __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
/* 24 bpp RGB */
#define GBM_FORMAT_RGB888 __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
#define GBM_FORMAT_BGR888 __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
/* 32 bpp RGB */
#define GBM_FORMAT_XRGB8888 __gbm_fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
#define GBM_FORMAT_XBGR8888 __gbm_fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
#define GBM_FORMAT_RGBX8888 __gbm_fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
#define GBM_FORMAT_BGRX8888 __gbm_fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
#define GBM_FORMAT_ARGB8888 __gbm_fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
#define GBM_FORMAT_ABGR8888 __gbm_fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
#define GBM_FORMAT_RGBA8888 __gbm_fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
#define GBM_FORMAT_BGRA8888 __gbm_fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
#define GBM_FORMAT_XRGB2101010 __gbm_fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
#define GBM_FORMAT_XBGR2101010 __gbm_fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
#define GBM_FORMAT_RGBX1010102 __gbm_fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
#define GBM_FORMAT_BGRX1010102 __gbm_fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
#define GBM_FORMAT_ARGB2101010 __gbm_fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
#define GBM_FORMAT_ABGR2101010 __gbm_fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
#define GBM_FORMAT_RGBA1010102 __gbm_fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
#define GBM_FORMAT_BGRA1010102 __gbm_fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
/* 64 bpp RGB */
#define GBM_FORMAT_XBGR16161616 __gbm_fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
#define GBM_FORMAT_ABGR16161616 __gbm_fourcc_code('A', 'B', '4', '8') /* [63:0] A:B:G:R 16:16:16:16 little endian */
/*
* Floating point 64bpp RGB
* IEEE 754-2008 binary16 half-precision float
* [15:0] sign:exponent:mantissa 1:5:10
*/
#define GBM_FORMAT_XBGR16161616F __gbm_fourcc_code('X', 'B', '4', 'H') /* [63:0] x:B:G:R 16:16:16:16 little endian */
#define GBM_FORMAT_ABGR16161616F __gbm_fourcc_code('A', 'B', '4', 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */
/* packed YCbCr */
#define GBM_FORMAT_YUYV __gbm_fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
#define GBM_FORMAT_YVYU __gbm_fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
#define GBM_FORMAT_UYVY __gbm_fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
#define GBM_FORMAT_VYUY __gbm_fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
#define GBM_FORMAT_AYUV __gbm_fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
/*
* 2 plane YCbCr
* index 0 = Y plane, [7:0] Y
* index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
* or
* index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
*/
#define GBM_FORMAT_NV12 __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
#define GBM_FORMAT_NV21 __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
#define GBM_FORMAT_NV16 __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
#define GBM_FORMAT_NV61 __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
/*
* 3 plane YCbCr
* index 0: Y plane, [7:0] Y
* index 1: Cb plane, [7:0] Cb
* index 2: Cr plane, [7:0] Cr
* or
* index 1: Cr plane, [7:0] Cr
* index 2: Cb plane, [7:0] Cb
*/
#define GBM_FORMAT_YUV410 __gbm_fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU410 __gbm_fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV411 __gbm_fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU411 __gbm_fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV420 __gbm_fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU420 __gbm_fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV422 __gbm_fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU422 __gbm_fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV444 __gbm_fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU444 __gbm_fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */
struct gbm_format_name_desc {
char name[5];
};
/**
* Flags to indicate the intended use for the buffer - these are passed into
* gbm_bo_create(). The caller must set the union of all the flags that are
* appropriate
*
* \sa Use gbm_device_is_format_supported() to check if the combination of format
* and use flags are supported
*/
enum gbm_bo_flags {
/**
* Buffer is going to be presented to the screen using an API such as KMS
*/
GBM_BO_USE_SCANOUT = (1 << 0),
/**
* Buffer is going to be used as cursor
*/
GBM_BO_USE_CURSOR = (1 << 1),
/**
* Deprecated
*/
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
/**
* Buffer is to be used for rendering - for example it is going to be used
* as the storage for a color buffer
*/
GBM_BO_USE_RENDERING = (1 << 2),
/**
* Buffer can be used for gbm_bo_write. This is guaranteed to work
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
*/
GBM_BO_USE_WRITE = (1 << 3),
/**
* Buffer is linear, i.e. not tiled.
*/
GBM_BO_USE_LINEAR = (1 << 4),
/**
* Buffer is protected, i.e. encrypted and not readable by CPU or any
* other non-secure / non-trusted components nor by non-trusted OpenGL,
* OpenCL, and Vulkan applications.
*/
GBM_BO_USE_PROTECTED = (1 << 5),
/**
* The buffer will be used for front buffer rendering. On some
* platforms this may (for example) disable framebuffer compression
* to avoid problems with compression flags data being out of sync
* with pixel data.
*/
GBM_BO_USE_FRONT_RENDERING = (1 << 6),
/**
* Allow the driver to select fixed-rate compression parameters.
*/
GBM_BO_FIXED_COMPRESSION_DEFAULT = (1 << 7),
/**
* Fixed-rate compression: at least 1bpc, less than 2bpc
*/
GBM_BO_FIXED_COMPRESSION_1BPC = (2 << 7),
/**
* Fixed-rate compression: at least 2bpc, less than 3bpc
*/
GBM_BO_FIXED_COMPRESSION_2BPC = (3 << 7),
/**
* Fixed-rate compression: at least 3bpc, less than 4bpc
*/
GBM_BO_FIXED_COMPRESSION_3BPC = (4 << 7),
/**
* Fixed-rate compression: at least 4bpc, less than 5bpc
*/
GBM_BO_FIXED_COMPRESSION_4BPC = (5 << 7),
/**
* Fixed-rate compression: at least 5bpc, less than 6bpc
*/
GBM_BO_FIXED_COMPRESSION_5BPC = (6 << 7),
/**
* Fixed-rate compression: at least 6bpc, less than 7bpc
*/
GBM_BO_FIXED_COMPRESSION_6BPC = (7 << 7),
/**
* Fixed-rate compression: at least 7bpc, less than 8bpc
*/
GBM_BO_FIXED_COMPRESSION_7BPC = (8 << 7),
/**
* Fixed-rate compression: at least 8bpc, less than 9bpc
*/
GBM_BO_FIXED_COMPRESSION_8BPC = (9 << 7),
/**
* Fixed-rate compression: at least 9bpc, less than 10bpc
*/
GBM_BO_FIXED_COMPRESSION_9BPC = (10 << 7),
/**
* Fixed-rate compression: at least 10bpc, less than 11bpc
*/
GBM_BO_FIXED_COMPRESSION_10BPC = (11 << 7),
/**
* Fixed-rate compression: at least 11bpc, less than 12bpc
*/
GBM_BO_FIXED_COMPRESSION_11BPC = (12 << 7),
/**
* Fixed-rate compression: at least 12bpc, no maximum rate
*/
GBM_BO_FIXED_COMPRESSION_12BPC = (13 << 7),
/* next available value is (1 << 11) */
};
#define GBM_BO_FIXED_COMPRESSION_MASK (((1 << 11) - 1) & ~((1 << 7) - 1))
int
gbm_device_get_fd(struct gbm_device *gbm);
const char *
gbm_device_get_backend_name(struct gbm_device *gbm);
int
gbm_device_is_format_supported(struct gbm_device *gbm,
uint32_t format, uint32_t flags);
int
gbm_device_get_format_modifier_plane_count(struct gbm_device *gbm,
uint32_t format,
uint64_t modifier);
void
gbm_device_destroy(struct gbm_device *gbm);
struct gbm_device *
gbm_create_device(int fd);
struct gbm_bo *
gbm_bo_create(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format, uint32_t flags);
struct gbm_bo *
gbm_bo_create_with_modifiers(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count);
struct gbm_bo *
gbm_bo_create_with_modifiers2(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count,
uint32_t flags);
#define GBM_BO_IMPORT_WL_BUFFER 0x5501
#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
#define GBM_BO_IMPORT_FD 0x5503
#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
struct gbm_import_fd_data {
int fd;
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t format;
};
#define GBM_MAX_PLANES 4
struct gbm_import_fd_modifier_data {
uint32_t width;
uint32_t height;
uint32_t format;
uint32_t num_fds;
int fds[GBM_MAX_PLANES];
int strides[GBM_MAX_PLANES];
int offsets[GBM_MAX_PLANES];
uint64_t modifier;
};
struct gbm_bo *
gbm_bo_import(struct gbm_device *gbm, uint32_t type,
void *buffer, uint32_t flags);
/**
* Flags to indicate the type of mapping for the buffer - these are
* passed into gbm_bo_map(). The caller must set the union of all the
* flags that are appropriate.
*
* These flags are independent of the GBM_BO_USE_* creation flags. However,
* mapping the buffer may require copying to/from a staging buffer.
*
* See also: pipe_map_flags
*/
enum gbm_bo_transfer_flags {
/**
* Buffer contents read back (or accessed directly) at transfer
* create time.
*/
GBM_BO_TRANSFER_READ = (1 << 0),
/**
* Buffer contents will be written back at unmap time
* (or modified as a result of being accessed directly).
*/
GBM_BO_TRANSFER_WRITE = (1 << 1),
/**
* Read/modify/write
*/
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
};
void *
gbm_bo_map(struct gbm_bo *bo,
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
uint32_t flags, uint32_t *stride, void **map_data);
void
gbm_bo_unmap(struct gbm_bo *bo, void *map_data);
uint32_t
gbm_bo_get_width(struct gbm_bo *bo);
uint32_t
gbm_bo_get_height(struct gbm_bo *bo);
uint32_t
gbm_bo_get_stride(struct gbm_bo *bo);
uint32_t
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);
uint32_t
gbm_bo_get_format(struct gbm_bo *bo);
uint32_t
gbm_bo_get_bpp(struct gbm_bo *bo);
uint32_t
gbm_bo_get_offset(struct gbm_bo *bo, int plane);
struct gbm_device *
gbm_bo_get_device(struct gbm_bo *bo);
union gbm_bo_handle
gbm_bo_get_handle(struct gbm_bo *bo);
int
gbm_bo_get_fd(struct gbm_bo *bo);
uint64_t
gbm_bo_get_modifier(struct gbm_bo *bo);
int
gbm_bo_get_plane_count(struct gbm_bo *bo);
union gbm_bo_handle
gbm_bo_get_handle_for_plane(struct gbm_bo *bo, int plane);
int
gbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);
int
gbm_bo_write(struct gbm_bo *bo, const void *buf, size_t count);
void
gbm_bo_set_user_data(struct gbm_bo *bo, void *data,
void (*destroy_user_data)(struct gbm_bo *, void *));
void *
gbm_bo_get_user_data(struct gbm_bo *bo);
void
gbm_bo_destroy(struct gbm_bo *bo);
struct gbm_surface *
gbm_surface_create(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format, uint32_t flags);
struct gbm_surface *
gbm_surface_create_with_modifiers(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count);
struct gbm_surface *
gbm_surface_create_with_modifiers2(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count,
uint32_t flags);
struct gbm_bo *
gbm_surface_lock_front_buffer(struct gbm_surface *surface);
void
gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo);
int
gbm_surface_has_free_buffers(struct gbm_surface *surface);
void
gbm_surface_destroy(struct gbm_surface *surface);
char *
gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc *desc);
#ifdef __cplusplus
}
#endif
#endif
-11
View File
@@ -1,11 +0,0 @@
# glvnd vendor ABI headers
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
headers intact.
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
smaller dependency than a build-time download of a source tree.
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
-141
View File
@@ -1,141 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#include <GL/gl.h>
#if !defined(__GL_DISPATCH_ABI_H)
#define __GL_DISPATCH_ABI_H
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup gldispatchabi GL dispatching ABI
*
* This is not a complete ABI, but rather a fragment common to the libEGL and
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
* these client ABIs.
*/
/*!
* Thread-local implementation used by libglvnd. This is passed into the patch
* function callback via the type parameter.
*
* For most architectures, the vendor library can ignore this parameter, since
* it will always be the same value. It's used for systems like ARM, where the
* stubs might be use the ARM or Thumb instruction sets.
*
* The stub type does not make any distinction between TLS and TSD stubs. The
* entire purpose of entrypoint rewriting is to skip the dispatch table in
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
*/
enum {
/*!
* Indicates that the stubs aren't defined in assembly. For example, if the
* dispatch stubs are written in C. Vendor libraries generally won't see
* this value.
*/
__GLDISPATCH_STUB_UNKNOWN,
/*!
* Used for stubs on x86 systems.
*/
__GLDISPATCH_STUB_X86,
/*!
* Used for stubs on x86-64 systems.
*/
__GLDISPATCH_STUB_X86_64,
/*!
* Used for stubs on ARMv7, using the Thumb instruction set.
*/
__GLDISPATCH_STUB_ARMV7_THUMB,
/*!
* Used for stubs on ARMv7, using the normal ARM instruction set.
*/
__GLDISPATCH_STUB_ARMV7_ARM,
/*!
* Used for stubs on ARMv8/aarch64.
*/
__GLDISPATCH_STUB_AARCH64,
/*!
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
*/
__GLDISPATCH_STUB_X32,
/*!
* Used for stubs on PPC64 systems.
*/
__GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
*/
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on LOONGARCH/loongarch64.
*/
__GLDISPATCH_STUB_LOONGARCH64,
};
/*!
* A callback function called by the vendor library to fetch the address of an
* entrypoint.
*
* The function returns two pointers, one writable and one executable. The two
* pointers may or may not be the same virtual address, but they will both be
* mappings of the same physical memory.
*
* The vendor library should write its entrypoint to the address returned by
* \p writePtr, but should use the address from \p execPtr for things like
* calculating PC-relative offsets.
*
* Note that if this function fails, then the vendor library can still try to
* patch other entrypoints.
*
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
* be zero, even if the stub uses the thumb instruction set. The vendor library
* should use the \c type parameter of \c initiatePatch to determine which
* instruction set to use.
*
* \param funcName The function name.
* \param[out] writePtr The pointer that the vendor library can write to.
* \param[out] execPtr The pointer to the executable code.
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
*/
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
void **writePtr, const void **execPtr);
#if defined(__cplusplus)
}
#endif
#endif // __GL_DISPATCH_ABI_H
-464
View File
@@ -1,464 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#if !defined(__LIB_EGL_ABI_H)
#define __LIB_EGL_ABI_H
#include <stdint.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include "glvnd/GLdispatchABI.h"
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup eglvendorabi EGL Vendor ABI
*
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
*
* Each vendor is associated with three distinct dispatch table types:
*
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
* provided by the vendor at load time during the initial handshake.
* - dynamic EGL dispatch table: this is a structure allocated by the API
* library at runtime used to manage EGL extension functions which are not
* present in the static table.
* - core GL dispatch table: this is a structure maintained by the API library
* which contains both GL core (static) and GL extension (dynamic) functions.
*
* @{
*/
/*
* Rendering API handling:
*
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
* other API is added, then the major version number will be incremented.
*
* When the application calls eglBindAPI, libEGL will forward the call to every
* vendor library. In addition, a vendor library can query the current API from
* libEGL using the getCurrentApi callback.
*
* Vendor libraries are not required to support both GL and GLES, but they must
* be able to deal with either one as the current rendering API. If a vendor
* doesn't support the current API, then it should return an error from
* eglCreateContext.
*/
/*!
* Current version of the ABI.
*
* This version number contains a major number in the high-order 16 bits, and
* a minor version number in the low-order 16 bits.
*
* The major version number is incremented when an interface change will break
* backwards compatibility with existing vendor libraries. The minor version
* number is incremented when there's a change but existing vendor libraries
* will still work.
*/
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
{
return version >> 16;
}
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
{
return version & 0xFFFF;
}
/*!
* This opaque structure stores function pointers for EGL extension functions.
* It is allocated at runtime by the API library. Vendor-provided dispatch
* functions retrieve and operate on this structure using the API below.
*/
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
/****************************************************************************
* API library exports *
****************************************************************************/
typedef struct __EGLapiExportsRec {
/************************************************************************
* The following routines are used by vendor-implemented EGL dispatch
* functions to lookup and call into the right vendor.
************************************************************************/
/*!
* This function must be called at the start of every EGL dispatch stub. It
* performs any necessary per-call bookkeeping.
*/
void (* threadInit) (void);
EGLenum (* getCurrentApi)(void);
/*!
* This function retrieves the appropriate current dynamic dispatch table,
* if a GL context is current. Otherwise, this returns NULL.
*/
__EGLvendorInfo *(*getCurrentVendor)(void);
/*!
* This retrieves the current context for this thread.
*/
EGLContext (*getCurrentContext)(void);
/*!
* Returns the current display for this thread.
*/
EGLDisplay (*getCurrentDisplay)(void);
/*!
* Returns the current drawable for this thread.
*/
EGLSurface (*getCurrentSurface)(EGLint readDraw);
/*!
* This function retrieves an entry point from the dynamic dispatch table
* given an index into the table.
*/
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
/*!
* Sets the last error for the current thread. The error will be returned
* the next time the app calls eglGetError().
*
* This function will override a call to \c setLastVendor, and vice-versa.
*/
void (* setEGLError) (EGLint errorCode);
/*!
* Notifies libEGL about the vendor library that an EGL function is
* dispatched to. This is used to look up the last EGL error code from a
* vendor.
*
* A vendor library may generate an EGL error asynchronously, if it
* offloads some EGL functions onto a worker thread. In that case, calling
* \c setEGLError to set the last error would not work.
*
* When an EGL dispatch stubs finds the vendor library, it must call this
* function before it dispatches the function call.
*
* This function will override a call to \c setEGLError, and vice-versa.
*
* \param vendor The vendor library of the current EGL function call.
* \return EGL_TRUE on success, or EGL_FALSE on failure.
*/
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
/************************************************************************
* These routines are used by vendor dispatch functions to look up
* and add mappings between various objects and vendors.
************************************************************************/
// As far as I can tell, all EGL functions that take a context or drawable
// also take an EGLDisplay. So, we don't need any extra functions for
// mapping a context/surface/whatever to a EGLDisplay or vendor.
/*!
* Returns the EGL vendor for an EGLDisplay handle.
*/
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
/*!
* Returns the EGL vendor for an EGLDeviceEXT handle.
*/
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
/**
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
* any function that returns an EGLDeviceEXT handle should call this.
*
* Supported since ABI version 1.2.
*/
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
} __EGLapiExports;
/*****************************************************************************
* API library imports *
*****************************************************************************/
/*!
* The enum values accepted by \c __EGLapiImports::getVendorString.
*/
enum
{
/*!
* The set of platform extensions that the vendor supports.
*
* Platform extensions typically just add enums to the existing
* eglGetPlatformDisplay function, so we don't need any additional logic
* or special handling from libEGL itself.
*
* As a result, vendor libraries can add these extensions without having
* to modify libEGL to match.
*/
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
};
/*!
* This structure stores required and optional vendor library callbacks.
*/
typedef struct __EGLapiImportsRec {
/**
* Creates an EGLDisplay. This function is used to handle both
* \c eglGetDisplay and \c eglGetPlatformDisplay.
*
* For some platform types, libEGL may be able to identify a specific
* vendor to use.
*
* If libEGL can't figure out which vendor to use on its own, then it will
* go through the list of available vendor libraries and call this function
* until one succeeds.
*
* If the application calls eglGetPlatformDisplay, then the parameters are
* passed through from the application.
*
* If the application calls eglGetDisplay, and the native display handle is
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
* type, and will pass that platform to the vendor library.
*
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
* libEGL will call into each vendor library with \p platform set to
* \c EGL_NONE. The vendor library can then select a default display to
* return.
*
* In all cases, if the vendor library can't find a matching EGLDisplay,
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
* through the vendor's \c eglGetError function.
*/
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
const EGLAttrib *attrib_list);
/*!
* Checks if the vendor library supports a given client API (that is, the
* API value passed to \c eglBindAPI).
*/
EGLBoolean (* getSupportsAPI) (EGLenum api);
/*!
* (OPTIONAL) Returns a string from the vendor library. This is used for
* anything that isn't available from eglQueryString.
*/
const char * (* getVendorString) (int name);
/*
* NOTE: libEGL will load all of the vendor libraries up front, so it
* handles extension functions slightly differently than EGL.
*/
/*!
* This retrieves the pointer to the real EGL or core GL function.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function.
*/
void * (* getProcAddress) (const char *procName);
/*!
* This retrieves vendor-neutral functions which use the
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
*
* A vendor library must provide a dispatch function for all EGL display
* extension functions that it supports.
*
* Client extension functions cannot be dispatched based on an EGDisplay,
* so they must be handled in libEGL itself.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function or \p procName is not a EGL display function.
*/
void * (*getDispatchAddress) (const char *procName);
/*!
* This notifies the vendor library which dispatch table index is
* assigned to a particular EGL extension function.
*/
void (*setDispatchIndex) (const char *procName, int index);
/*
* The vendor library may use the isPatchSupported, initiatePatch,
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
* entrypoints at make current time, provided no other contexts are current
* and the TLS model supports this functionality. This is a performance
* optimization that may not be available at runtime; the vendor library
* must not depend on this functionality for correctness.
*
* To use this optimization, the vendor library must provide at least the
* isPatchSupported and initiatePatch entrypoints.
*
* Note that these functions are identical to the ones used in GLX.
*/
/*!
* (OPTIONAL) Checks to see if the vendor library supports patching the
* given stub type and size.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*/
GLboolean (* isPatchSupported)(int type, int stubSize);
/*!
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
* top-level entrypoints.
*
* The vendor library should use the \p lookupStubOffset callback to find
* the addresses of each entrypoint.
*
* This function may be called more than once to patch multiple sets of
* entrypoints. For example, depending on how they're built, libOpenGL.so
* or libGL.so may have their own entrypoints that are separate functions
* from the ones in libGLdispatch.
*
* Note that during this call is the only time that the entrypoints can be
* modified. After the call to \c initiatePatch returns, the vendor library
* should treat the entrypoints as read-only.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*
* \return GL_TRUE if the vendor library supports patching with this type
* and size.
*/
GLboolean (*initiatePatch)(int type,
int stubSize,
DispatchPatchLookupStubOffset lookupStubOffset);
/*!
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
* longer owns the top-level entrypoints.
*
* Libglvnd will take care of the restoring the entrypoints back to their
* original state. The vendor library must not try to modify them.
*/
void (*releasePatch)(void);
/*!
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
* This callback allows vendor libraries to perform any per-thread
* initialization.
*
* This is basically a workaround for broken applications. A lot of apps
* will make one or more invalid GLX/EGL calls on a thread (often including
* a MakeCurrent with invalid parameters), and then will try to call an
* OpenGL function.
*
* A non-libglvnd-based driver would be able to initialize any thread state
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
* past libGLX.
*
* This function is optional. If it's \c NULL, then libGLdispatch will
* simply ignore it.
*
* \note This function may be called concurrently from multiple threads.
*/
void (*patchThreadAttach)(void);
/*!
* (OPTIONAL) Tries to determine the platform type for a native display.
*
* If the vendor library provides this function, then libglvnd will call it
* to determine which platform to use for a native display handle in
* eglGetDisplay.
*
* If no vendor library identifies the platform, then libglvnd will fall
* back to its own platform detection logic.
*
* Libglvnd can call this function for any native display handle except
* \c EGL_DEFAULT_DISPLAY.
*
* No matter what the value of \p native_display, the vendor library must
* not crash, and must not return a false match. If the vendor library
* can't identify the display, then it must return \c EGL_NONE.
*
* In particular, that means that a vendor library must not return any sort
* of default or fallback platform.
*
* \param native_display The native display handle passed to eglGetDisplay.
* \return Either a platform type enum or EGL_NONE.
*/
EGLenum (* findNativeDisplayPlatform) (void *native_display);
} __EGLapiImports;
/*****************************************************************************/
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* Vendor libraries must export a function called __egl_Main() with the
* following prototype.
*
* This function also performs a handshake based on the ABI version number.
* Vendor libraries can optionally use the version number to support older
* versions of the ABI.
*
* \param[in] version The ABI version. The upper 16 bits contains the major version
* number, and the lower 16 bits contains the minor version number.
*
* \param[in] exports The table of functions provided by libEGL. This pointer will
* remain valid for as long as the vendor is loaded.
*
* \param[in] vendor The opaque pointer used to identify this vendor library. This
* may be used in future versions to provide additional per-vendor information.
*
* \param[out] imports The function table that the vendor library should fill
* in. The vendor library must assign every non-optional function in the
* struct.
*
* \return True on success. If the vendor library does not support the
* requested ABI version or if some other error occurs, then it should return
* False.
*/
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* @}
*/
#if defined(__cplusplus)
}
#endif
#endif /* __LIB_EGL_ABI_H */
-561
View File
@@ -14,35 +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)
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" 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")
@@ -187,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
@@ -313,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
@@ -403,10 +326,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
@@ -437,14 +357,10 @@ set(SOURCE_FILES
MobileGL/MG_Backend/Init.cpp
MobileGL/MG_Backend/BackendObject.cpp
MobileGL/MG_Backend/HostFrameBridge.cpp
MobileGL/MG_Backend/GbmFrameChannel.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
@@ -502,194 +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
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
MobileGL/MG_Remote/Transport/LogForward.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
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
MobileGL/MG_Remote/Transport/AdoptTier.cpp
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
MobileGL/MG_Remote/Transport/AdoptT0.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/DmaBufImport.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
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
MobileGL/MG_Remote/Server/PairAcceptor.cpp
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.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
@@ -704,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 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)
@@ -748,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}
@@ -781,16 +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)
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
@@ -826,17 +518,6 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
${MOBILEGL_LINK_LIBRARIES}
)
# A LINUX BUILD COMPILES THE DirectVulkan BACKEND BUT HAS NO VULKAN LOADER TO LINK AGAINST, so the
# resulting shared library carries undefined vk* symbols - and that is not theoretical: glvnd dlopens
# this file as the EGL vendor with RTLD_NOW, so the load fails outright with "undefined symbol:
# vkGetQueryPoolResults" (measured on the phone's container). Android links the loader already; on
# Linux the caller passes the runtime's own libvulkan.so.1, and leaving the variable empty keeps the
# previous behaviour for every build that has no Vulkan runtime to point at.
set(MOBILEGL_VULKAN_LOADER "" CACHE FILEPATH "Vulkan loader (libvulkan.so.1) to link into the GL library")
if (UNIX AND NOT ANDROID AND MOBILEGL_VULKAN_LOADER)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${MOBILEGL_VULKAN_LOADER})
endif()
target_compile_definitions(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_COMPILE_DEF}
@@ -903,68 +584,6 @@ if(NOT ANDROID)
)
endif()
# ---------------------------------------------------------------------------
# MobileGLGbm: the GBM drop-in the container's compositor loads.
#
# kwin - the Wayland compositor in the container half of the split - allocates the
# buffers it renders into and scans out through libgbm, and the dma-buf fds it gets
# back are what the Anland display producer ships to the phone. Mesa's libgbm cannot
# serve that: its buffers belong to the host GPU, and the phone would receive a copy
# through a path that was never designed to carry one. This target is the library
# that is loaded in its place, under the name a loader looks for.
#
# It is deliberately NOT part of libMobileGL.so. The compositor needs one file named
# libgbm.so that it loads instead of Mesa's, and folding this code into the GL
# library would put a second GL frontend, a second Vulkan instance and a second
# glslang in the compositor's address space to serve an API that shares none of them.
#
# The include paths below are borrowed rather than linked. Includes.h - which this
# library includes for the MobileGL types and the MGLOG macros - pulls in the GL,
# EGL, Vulkan, glslang, SPIRV-Cross and xxHash HEADERS transitively, and headers are
# all this library wants from them: linking any of those static libraries into a
# libgbm would embed a copy of each in a process that already has one.
#
# -Bsymbolic-functions, for the same reason the main target carries it: the point of
# this library is to REPLACE a libgbm that may already be loaded in the process, so
# every internal reference has to bind to this file's own definitions rather than to
# whatever else the process has under the same names.
# ---------------------------------------------------------------------------
add_library(MobileGLGbm SHARED
MobileGL/MG_Gbm/GbmDevice.cpp
# The display host's socket protocol, kept in its own translation unit so that the
# allocation paths never have to know how a frame arrives - only that one did.
MobileGL/MG_Gbm/GbmHostChannel.cpp
MobileGL/MG_Gbm/GbmApi.cpp
# MGLOG_I/W/E expand to a call to MobileGL::MG_Util::Debug::Log, and this is the
# one translation unit that defines it. It is compiled in rather than linked from
# libMobileGL.so because this library has to load into a compositor that has no
# libMobileGL in it at all - and because a libgbm with an undefined symbol is a
# library that fails on the first diagnostic rather than at a link step somebody
# could have read.
MobileGL/MG_Util/Debug/Log.cpp
)
# The vendored gbm.h first: this library implements THAT declaration set, and a
# system <gbm.h> from another Mesa version on the include path ahead of it would
# change the ABI under the implementations in GbmApi.cpp.
target_include_directories(MobileGLGbm PRIVATE
${CMAKE_SOURCE_DIR}/3rdparty/gbm/include
${MOBILEGL_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/3rdparty/glslang
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Cross
${CMAKE_SOURCE_DIR}/3rdparty/xxHash
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
)
set_target_properties(MobileGLGbm PROPERTIES
OUTPUT_NAME gbm
POSITION_INDEPENDENT_CODE ON
)
if (UNIX AND NOT APPLE)
target_link_options(MobileGLGbm PRIVATE "LINKER:-Bsymbolic-functions")
endif()
if (TRACY_ENABLE)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
@@ -972,19 +591,6 @@ if (TRACY_ENABLE)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
endif ()
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
#
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
# With default visibility every reference inside this library resolves against the whole process,
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
# the two visibilities do not mix; this flag is that promise made by the linker instead.
if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -1093,170 +699,3 @@ endif()
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# The replay runner also builds for Android: an adb-shell executable that can
# dial a render server on the device (unix or TCP) without any Activity.
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
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()
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
# app's own process by the spike hook (`--es mobilegl_spike_spawn
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
# links nothing from MobileGL.
add_executable(MobileGLExtmemProbe
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbe PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLExtmemProbe"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbe PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
if (TARGET MobileGLServer)
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
endif()
add_executable(MobileGLExtmemProbeAsServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
endif()
# The GBM library carries the tree's logger, which reaches Android's own log. The main target gets
# these libraries in its own Android block; a library built from the same sources needs them for the
# same reason, and without them it is the LINK that fails rather than anything in the code.
if (ANDROID)
target_link_libraries(MobileGLGbm PRIVATE log android)
endif()
+4 -419
View File
@@ -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;
@@ -214,12 +214,9 @@ namespace MobileGL::MG_Config {
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
@@ -331,418 +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
Host = 2, // a window surface draws into frames the DISPLAY HOST owns; the host presents
};
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
inline constexpr Uint32 kAdoptTierUnset = 3;
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_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
// wait per read.
Uint32 PboReadbackSync = 0;
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
// arm and the red-once of the no-reply gate (FencePollScenario).
Uint32 PollEscalate = 64;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
Uint32 AdoptTier = kAdoptTierUnset;
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// 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;
}
// P12: the other on-screen shape, and the mirror image of the one above. The display host
// (the Android side that owns the screen) allocates the frames, offers each one over the
// host frame bridge, and puts it on the glass when this side says it is drawn - so a
// client that asks for this has no window of its own either, and the server takes the
// frames rather than making a window.
inline Bool HostFrameWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Host;
}
#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
-342
View File
@@ -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");
@@ -263,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() {
@@ -315,267 +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);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// 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;
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
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): MOBILEGL_IPC_SURFACE = offscreen | server | host (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 if (surface == "host") {
ipc.Surface = MG_Config::IpcSurface::Host;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server|host, 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::Offscreen && !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=%s is IGNORED - it asks for an on-screen "
"surface (the server's own window, or the frames the display host owns) and "
"this run has no remote server to provide one: MOBILEGL_TRANSPORT is not "
"spawn. Window surfaces stay the client's own",
ipc.Surface == MG_Config::IpcSurface::Server ? "server" : "host");
}
}
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 pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%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.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
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();
+2 -9
View File
@@ -8,8 +8,6 @@
#pragma once
#include <cstdio>
// ============== Platform-specific definitions and macros ============== //
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
@@ -36,11 +34,6 @@
#ifdef _WIN32
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
#else
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
// points with. The one place where that is not enough - a vendor library handing glvnd its own
// entry points, where a reference resolving against the process instead of this library would be a
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
// here, because a second visibility on the same declaration does not compile.
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
#endif
@@ -112,8 +105,8 @@
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
fprintf(stderr, "Assertion failed" __VA_OPT__(": ") "%s", "" __VA_ARGS__); \
fprintf(stderr, " at %s:%d (%s)\n", __FILE__, __LINE__, __func__); \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
+1 -72
View File
@@ -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");
+5 -86
View File
@@ -172,15 +172,11 @@ namespace MobileGL::MG_Backend {
return false;
}
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
// refused a caller for a reason that is not there: the surface and context tables are keyed
// by the display each was created on, every operation carries its own display handle, and
// this field has no reader anywhere - it was the whole of the rule.
//
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
// probing another, so the second eglInitialize is the one that matters, and a refusal there
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
// and the real display having failed, with nothing in between that says which was refused.
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -213,28 +209,6 @@ namespace MobileGL::MG_Backend {
return true;
}
// The frame itself is the backend's business (InitHostFrameSurface); what happens here is
// the registration every surface kind shares, so the swap path and the state mirrors see
// a host-framed surface exactly like any other.
Bool BackendObject::CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("CreateEGLHostFrameSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("CreateEGLHostFrameSurface failed: invalid EGLSurface");
return false;
}
auto& state = m_eglSurfaces[surface];
state = EGLSurfaceState{
.Kind = SurfaceKind::HostFrame,
.Width = static_cast<EGLint>(std::max<Uint32>(width, 1)),
.Height = static_cast<EGLint>(std::max<Uint32>(height, 1)),
};
return ActivateEGLSurface(surface);
}
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
@@ -316,11 +290,6 @@ namespace MobileGL::MG_Backend {
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
return false;
}
} else if (surfaceState->Kind == SurfaceKind::HostFrame) {
if (!InitHostFrameSurface(surfaceState->Width, surfaceState->Height)) {
MGLOG_E("ActivateEGLSurface failed: backend InitHostFrameSurface failed");
return false;
}
} else {
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
return false;
@@ -346,29 +315,6 @@ namespace MobileGL::MG_Backend {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
// EGL_KHR_surfaceless_context. A context whose draw and read surfaces are both absent has
// no framebuffer of its own to be bound to: the caller draws into framebuffers it binds
// itself, and this side only has to have its own GL context current on this thread for the
// records that follow. Everything below this point describes a surface, so none of it
// applies - and what is recorded is exactly what was asked for, so a later swap on a
// surface that does not exist is never made to look like one that does.
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
if (!m_backendCapabilitiesInitialized) {
if (!InitCapabilities()) {
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
return false;
}
m_backendCapabilitiesInitialized = true;
}
ReleaseEGLCurrentThread(threadKey);
m_eglCurrentThreads[threadKey] = EGLCurrentState{
.Display = dpy,
.DrawSurface = EGL_NO_SURFACE,
.ReadSurface = EGL_NO_SURFACE,
.Context = ctx,
};
return true;
}
if (!m_eglSurfaceInitialized) {
if (draw != read || !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
@@ -441,12 +387,6 @@ namespace MobileGL::MG_Backend {
return false;
}
// A host-framed surface has no swapchain: presenting it means telling the host the frame
// is drawn, because the host is what puts it on the glass.
if (m_eglSurfaceKind == SurfaceKind::HostFrame) {
return PresentHostFrame();
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
MGLOG_E("SwapEGLBuffers failed: backend Present function is null");
@@ -464,27 +404,6 @@ namespace MobileGL::MG_Backend {
}
}
// A backend that has no host-framed surfaces says so by name: the server may ask for one
// (MOBILEGL_IPC_SURFACE=host) against a build that cannot serve it, and that has to be a
// refusal with a reason rather than a surface that draws nowhere.
Bool BackendObject::InitHostFrameSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
MGLOG_E("InitHostFrameSurface: this backend has no host-framed surfaces");
return false;
}
Bool BackendObject::PresentHostFrame() {
MGLOG_E("PresentHostFrame: this backend has no host-framed surfaces");
return false;
}
Bool BackendObject::PresentHostFrameIfCurrent() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglSurfaceInitialized || m_eglSurfaceKind != SurfaceKind::HostFrame) return true;
return PresentHostFrame();
}
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
if (surface == EGL_NO_SURFACE) {
return false;
+10 -71
View File
@@ -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;
@@ -595,18 +550,6 @@ namespace MobileGL {
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
// The host owns the screen: this side never gets a window of the client's to draw
// into, so the surface it draws into is a frame the host allocated and offered over
// the host frame bridge. Each swap tells the host the frame is done and takes the
// next; presentation is the host's, not a swapchain's.
virtual Bool CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool InitHostFrameSurface(EGLint width, EGLint height);
virtual Bool PresentHostFrame();
// The present RECORD's half of a host-framed swap. A split client's swap reaches the
// server as a present record (ServerVerbSink::OnPresent), never as SwapEGLBuffers, so
// without this the host was never told a frame was drawn. True when the current
// surface is not host-framed: there is nothing to present then.
Bool PresentHostFrameIfCurrent();
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// Forwards the app-requested eglSwapInterval to the backend's native
@@ -628,11 +571,7 @@ namespace MobileGL {
enum class SurfaceKind {
None,
Window,
Pbuffer,
// A surface whose drawing target is a frame the display host owns. There is no
// swapchain behind it: the host puts the frame on the glass when this side says
// it is drawn, which is why presenting one is its own call and not a Present.
HostFrame
Pbuffer
};
struct EGLCurrentState {
-41
View File
@@ -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
@@ -6,12 +6,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// The role's own backend is resolved the way Utils.cpp (C6 / ID-52) resolves it; that needs this
// header, and it has to be included at file scope - a header included inside this file's namespace
// would nest its own namespace under it.
#include <MG_Remote/Server/ServerLoop.h>
#endif
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
@@ -38,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) {}
@@ -866,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.
@@ -888,7 +821,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
maxSamples = probedCounts.front();
}
}
#endif
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
@@ -897,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() {
@@ -987,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) {
@@ -1019,256 +925,9 @@ 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
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
namespace {
// The host may still be starting when the client asks for its surface: it is a separate
// app that has to come up and allocate its pool. Waiting is better than refusing a
// surface a client is entitled to, and the wait is answered by name if it expires.
constexpr Int kHostFrameWaitMs = 30000;
}
Bool TakeHostFrameForExternalTexture(Uint width, Uint height) {
// THIS ROLE'S OWN BACKEND, resolved exactly the way ActiveBackendFormatCaps (Utils.cpp,
// C6 / ID-52) resolves it: under a live split session the SERVER's private backend owns the
// context on the apply thread, and pActiveBackendObject is the CLIENT's mirror there.
MG_Backend::BackendObject* backend = nullptr;
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
backend = MG_Remote::Server::ServerLoopInstance().Backend();
} else {
backend = MG_Backend::pActiveBackendObject.get();
}
if (backend == nullptr) return false;
return backend->InitHostFrameSurface(static_cast<EGLint>(width), static_cast<EGLint>(height));
}
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
// The EGL context comes from the same place a pbuffer session's does. What changes is not
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
// the first frame arrives, which is the only moment it is used.
if (!DirectGLES::InitPbufferSurface(width, height)) {
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
return false;
}
if (!m_hostFrameBridgeOpen) {
String why;
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
return false;
}
m_hostFrameBridgeOpen = true;
}
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::TakeHostFrame() {
String why;
struct AHardwareBuffer* frame = nullptr;
MG_Backend::HostFrameOffer offer{};
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
MGLOG_E("TakeHostFrame: %s", why.c_str());
return false;
}
HostFrameTarget target;
if (!HostFrameTargetCreate(frame, target, why)) {
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
offer.Height, why.c_str());
AHardwareBuffer_release(frame);
return false;
}
DestroyHostFrame();
m_hostFrameTarget = target;
m_hostFrameBuffer = frame;
m_hostFrameOffer = offer;
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
// as the draw target, which is how this path behaved before the canvas existed.
if (!EnsureHostFrameCanvas(target.Width, target.Height))
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
// resolve to this target, and that resolution is what this global is.
g_hostFrameFramebufferId = drawTarget.Framebuffer;
// Published together with the framebuffer, because they describe the same target: the STORAGE
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
// decides whether binding it is right at all.
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
g_hostFrameWidth = drawTarget.Width;
g_hostFrameHeight = drawTarget.Height;
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(drawTarget);
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
// over there renders into THESE frames - it runs this project's own GBM, whose
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
// down to an integer compare: the socket outlives every container and every connection,
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
// itself when the first frame arrives before any container has.
//
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
// oversight: no container running, a container that has died, or one that has taken the
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
// it would have been without a channel - the descriptor is a copy of the pixels' route,
// not a step in producing them.
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
return true;
}
void BackendObject_DirectGLES::DestroyHostFrame() {
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
// made over it stay valid between this frame being handed back and the next one arriving.
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
g_hostFrameWidth = m_hostFrameCanvas.Width;
g_hostFrameHeight = m_hostFrameCanvas.Height;
HostFrameTargetDestroy(m_hostFrameTarget);
if (m_hostFrameBuffer != nullptr) {
AHardwareBuffer_release(m_hostFrameBuffer);
m_hostFrameBuffer = nullptr;
}
// AND NOTHING IS SAID TO THE CHANNEL HERE, which is the fix rather than an omission. This
// runs once per presented frame: it used to Close() the channel (unlinking the container's
// socket node and dropping its connection sixty times a second - measured: connect()
// answered EAGAIN(11) on every attempt) and it used to tell the channel to forget the frame,
// which left the ONE window a compositor's first allocations arrive in - between this frame
// being presented and the host handing the next one over, i.e. inside Complete() above -
// with nothing to answer a dialer with. A compositor allocates its swapchain ONCE and keeps
// those buffers for the life of the process, so a dialer that finds nothing here renders
// its whole life into buffers nobody displays. The frame the channel remembers is the last
// one the host handed over: the next offer replaces it and Close() releases it.
}
Bool BackendObject_DirectGLES::PresentHostFrame() {
// When this returns, the pixels are complete: glFinish is the fence the host is told
// about, and the host's own readback - which Complete reads - is what proves the drawing
// landed where the screen will look.
BlitHostFrameCanvas();
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
// server's apply loop passes through the host channel - the swap it runs lands here for a
// host-framed surface - and a release is the container's compositor saying it is done
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
// socket: with no container connected it returns on its first test, and a container that
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
// read. Nothing here is waited for and nothing is required to arrive; the count of
// frames offered and not released is a number the channel keeps for the log, never a
// frame the render path waits on.
m_gbmFrameChannel.Poll();
String why;
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
MGLOG_E("PresentHostFrame: %s", why.c_str());
return false;
}
// The answer has been read, so the host has already put this frame on the glass: it is
// this side's to drop, and the next one becomes the drawing target.
DestroyHostFrame();
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
if (m_hostFrameCanvas.Framebuffer != 0) return true;
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
return false;
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
// framebuffer shadow and binds through it right after.
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
GLuint framebuffer = 0;
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
return false;
}
m_hostFrameCanvas.Renderbuffer = renderbuffer;
m_hostFrameCanvas.Framebuffer = framebuffer;
m_hostFrameCanvas.Width = width;
m_hostFrameCanvas.Height = height;
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
width, height, framebuffer, renderbuffer);
return true;
}
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
// shadow (which never saw this) is still right afterwards. The scissor test and
// rasterizer discard both apply to a blit, so they are lifted for it.
GLint readBinding = 0;
GLint drawBinding = 0;
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
static_cast<GLint>(m_hostFrameTarget.Width),
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
sameSize ? GL_NEAREST : GL_LINEAR);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
// The other arm's half: on a build with no host frames there is no frame to take, and the
// attach site's ordinary path (a storage-less external texture, exactly as before) is the honest
// behaviour there rather than a link error.
Bool TakeHostFrameForExternalTexture(Uint, Uint) { return false; }
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
return false;
}
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
void BackendObject_DirectGLES::DestroyHostFrame() {}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
Bool BackendObject_DirectGLES::PresentHostFrame() {
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
return false;
}
#endif
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
return DirectGLES::InitPbufferSurface(width, height);
}
@@ -1279,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);
}
@@ -1293,99 +949,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
// EGL_KHR_surfaceless_context, WHICH IS THE SHAPE A COMPOSITOR ASKS FOR. Its context has no
// draw surface of its own - every frame goes into a framebuffer it binds itself, and under
// this project that framebuffer is one of the display host's frames - so the surface checks
// below describe a surface that legitimately does not exist. What the server still needs is
// its own GL context current on this thread for the records that follow, and that is what
// the native bind underneath them does.
const Bool surfaceless = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT;
if (surfaceless && !m_eglSurfaceInitialized) {
// A DRAWABLE OF THIS SIDE'S OWN, WHICH IS WHAT A SURFACELESS CLIENT LEAVES TO US. The
// backend's native context has to be current on a surface of some kind before a single
// record of the client's can run - that is what the Init*Surface calls establish - and a
// client that makes its context current with no surface at all supplies none of its own.
// A pbuffer is the one that costs nothing to keep: it is never scanned out and never
// read, because every frame the client draws goes into a framebuffer it binds itself.
// The client is NOT told it has a surface - m_eglSurface stays EGL_NO_SURFACE - so a
// swap on one is still the refusal it was.
if (!InitPbufferSurface(1, 1)) {
MGLOG_E("MakeEGLCurrent failed: the backend's own drawable for a surfaceless context could not "
"be created");
return false;
}
m_eglSurface = EGL_NO_SURFACE;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = SurfaceKind::Pbuffer;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
}
if (!surfaceless) {
if (!m_eglSurfaceInitialized) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
return false;
}
}
#if 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 &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
(surfaceless || (m_eglSurfaceInitialized && m_eglSurface == draw &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx)));
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
NoteNativeContextGone();
if (!m_eglSurfaceInitialized) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
}
NoteNativeBoundFor(dpy, draw, read, ctx);
#else
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
return false;
}
if (!DirectGLES::MakeCurrent()) {
return false;
}
#endif
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
(void)DirectGLES::ReleaseCurrent();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return false;
}
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
// binds the surface's own default framebuffer - the pbuffer this path makes its context
// with - and that is not where these pixels go. Both the driver and the engine's binding
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
// every draw lands in a pbuffer nobody sees.
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(HostFrameDrawTarget());
}
return true;
}
@@ -1401,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 {
@@ -1542,40 +1116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary,
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
// missing checkSupported() answered false, EglContext::create() returned nullptr,
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
// rendering context" and quit before drawing a frame.
//
// The three are true statements about this backend now rather than a test unlock: every
// procedure they define is implemented in MG_Impl/GLImpl/Exporting/Definitions.cpp over
// the same shader and program objects the core entry points drive - the 39 of
// ARB_shader_objects, ARB_vertex_shader's three attrib-location entry points, and none
// at all for ARB_fragment_shader ("New Procedures and Functions: None" - it is the
// capability half, and the reason kwin's conjunction names it).
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
// NPOT textures: core since GL 2.0, and every host this backend can run on provides
// them. Full non-power-of-two support - mipmapping and every wrap mode, not ES 2.0's
// CLAMP_TO_EDGE-only exception - is required from ES 3.0 on, and the entries this list
// already advertises without any gate (compute shaders, shader storage buffers,
// indirect draws) need an ES 3.1 host. Nothing in the texture path rejects or pads a
// non-power-of-two size - storage and sampling are forwarded to that host - so the
// string describes what an application actually gets.
E_GL_ARB_texture_non_power_of_two,
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
// and this name is what makes them reachable - libepoxy in kwin resolves
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
// The render-server half of the import (fd over the GBM socket, then the server-side
// EGLImage) is the next change; until it lands an imported texture has no storage behind
// it, which is logged once at the entry point rather than left to look like a black frame.
E_GL_OES_EGL_image};
E_GL_ARB_get_program_binary};
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
@@ -1714,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;
@@ -1874,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
@@ -8,9 +8,6 @@
#pragma once
#include <Includes.h>
#include "HostFrameTarget.h"
#include "MG_Backend/HostFrameBridge.h"
#include "MG_Backend/GbmFrameChannel.h"
#include "../BackendObject.h"
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -59,46 +56,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
// A host-framed surface draws into a frame the display host owns; presenting one
// tells the host the frame is drawn, because the host is what puts it on the glass.
// NOT CreateEGLHostFrameSurface: the base class registers the surface and activates it,
// and activation is what reaches InitHostFrameSurface below. There is nothing about the
// EGL side of a host-framed surface this backend needs to do its own way.
Bool InitHostFrameSurface(EGLint width, EGLint height) override;
Bool PresentHostFrame() override;
// Takes the next frame from the host and makes it this context's framebuffer.
Bool TakeHostFrame();
void DestroyHostFrame();
// The canvas the compositor draws into, made once at the first frame's size.
Bool EnsureHostFrameCanvas(Uint width, Uint height);
// Copies the canvas into the frame about to be presented.
void BlitHostFrameCanvas();
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
const HostFrameTarget& HostFrameDrawTarget() const {
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
}
void OnEGLSurfaceReleased(EGLSurface surface) override;
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
// is what the host offers - it hands one over and waits for the answer before it
// presents it and offers the next.
MG_Backend::HostFrameBridge m_hostFrameBridge;
// The frames the bridge just took, on to the container: the Wayland compositor over there
// renders into this same memory, and the only route to it that end has is the dma-buf
// descriptor this channel offers per frame and reads a release back from.
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
HostFrameTarget m_hostFrameTarget;
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
// attachments bound to a frame's own renderbuffer named a deleted object from the second
// frame on. The attachments name this instead, and each present copies it into the
// frame. No EGLImage behind it (Image stays null).
HostFrameTarget m_hostFrameCanvas;
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
MG_Backend::HostFrameOffer m_hostFrameOffer{};
Bool m_hostFrameBridgeOpen = false;
Bool m_initialized = false;
MG_External::EGLFunctionsTable m_EGLFunctions;
MG_External::GLESFunctionsTable m_GLESFunctions;
@@ -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
+2 -36
View File
@@ -30,15 +30,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
#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);
@@ -108,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);
@@ -194,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);
@@ -249,26 +235,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
// MOBILEGL_IPC_SURFACE=host: what the client's DEFAULT framebuffer means while a host-framed
// surface is current. Zero everywhere else, because the driver's own 0 IS the EGL surface
// then - but on a host-framed surface the pixels belong to a frame the display host owns,
// and drawing into the driver's 0 would draw into a pbuffer nobody ever sees.
extern Uint g_hostFrameFramebufferId;
// WHAT THE HOST FRAME'S STORAGE IS, for the one caller that has to bind it in place of a
// storage-less attachment: an imported dma-buf is the display host's frame (same memory, same
// size), and the frame's storage is a RENDERBUFFER (HostFrameTarget.cpp binds it with
// glFramebufferRenderbuffer). A texture argument cannot name it, which is why these exist and
// why g_hostFrameFramebufferId alone was not enough. Zero whenever no frame is held.
extern Uint g_hostFrameRenderbuffer;
extern Uint g_hostFrameWidth;
extern Uint g_hostFrameHeight;
// TAKE THE FRAME WHEN NOTHING ELSE WILL. On the anland route the container's compositor never
// creates a window surface, so ActivateEGLSurface never runs and InitHostFrameSurface is never
// reached: the FIRST proof that the display host's frame is wanted is an ATTACHMENT of an
// imported external texture (Managers.cpp's 0x8D65 arm), and that is where the frame has to be
// taken. The host's own offer carries the width/height, so the numbers passed here are only
// what the bridge logs. Answers false (and changes nothing) whenever no backend is up.
Bool TakeHostFrameForExternalTexture(Uint width, Uint height);
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
@@ -1,208 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "HostFrameTarget.h"
#include "DirectGLES.h"
#include "Managers.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cstring>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
// The four entry points this is spelled in are optional everywhere: the loader's own
// tables do not carry them (a missing EGL function there is MGLOG_F), so the extension
// strings decide first and the pointers are taken only once they have.
using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES);
Bool HasExtensionWord(const char* list, const char* word) {
if (list == nullptr) return false;
const SizeT n = std::strlen(word);
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
const Bool startOk = at == list || at[-1] == ' ';
const Bool endOk = !at[n] || at[n] == ' ';
if (startOk && endOk) return true;
}
return false;
}
struct EntryPoints {
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
CreateImageFn CreateImage = nullptr;
DestroyImageFn DestroyImage = nullptr;
ImageTargetRenderbufferFn TargetRenderbuffer = nullptr;
};
// Resolved once per process and cached together with the refusal: a driver that lacks one
// of these does not grow it later, and the answer names what is missing.
const EntryPoints& Resolved(String& why) {
static EntryPoints points;
static Bool resolved = false;
static String refusal;
if (!resolved) {
resolved = true;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
!g_GLESFuncs.glGetString) {
refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString";
} else {
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
refusal = "EGL_ANDROID_get_native_client_buffer is not advertised";
} else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) {
refusal = "EGL_KHR_image_base is not advertised";
} else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
refusal = "GL_OES_EGL_image is not advertised";
} else {
points.GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
points.CreateImage =
reinterpret_cast<CreateImageFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
points.DestroyImage =
reinterpret_cast<DestroyImageFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
points.TargetRenderbuffer = reinterpret_cast<ImageTargetRenderbufferFn>(
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr ||
points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) {
refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / "
"eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES";
points = EntryPoints{};
}
}
}
}
why = refusal;
return points;
}
void DrainGLErrors() {
if (!g_GLESFuncs.glGetError) return;
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
}
}
}
Bool HostFrameTargetSupported(String& why) {
const EntryPoints& points = Resolved(why);
return points.GetNativeClientBuffer != nullptr;
}
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) {
target = HostFrameTarget{};
if (buffer == nullptr) {
why = "no buffer";
return false;
}
const EntryPoints& points = Resolved(why);
if (points.GetNativeClientBuffer == nullptr) return false;
if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers ||
!g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer ||
!g_GLESFuncs.glCheckFramebufferStatus) {
why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/"
"glFramebufferRenderbuffer/glCheckFramebufferStatus";
return false;
}
AHardwareBuffer_Desc desc{};
AHardwareBuffer_describe(buffer, &desc);
DrainGLErrors();
const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer);
if (clientBuffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return false;
}
const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
const EGLImageKHR image =
points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
clientBuffer, imageAttribs);
if (image == EGL_NO_IMAGE_KHR) {
why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame";
return false;
}
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast<GLeglImageOES>(image));
const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
if (renderbufferError != GL_NO_ERROR) {
why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
return false;
}
GLuint framebuffer = 0;
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status);
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
return false;
}
target.Image = image;
target.Renderbuffer = renderbuffer;
target.Framebuffer = framebuffer;
target.Width = desc.width;
target.Height = desc.height;
MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height,
framebuffer, desc.stride, desc.format);
return true;
}
void HostFrameTargetBind(const HostFrameTarget& target) {
if (target.Framebuffer == 0) return;
// THROUGH THE ENGINE'S SHADOW, never raw: a raw bind would leave the shadow claiming
// the previous framebuffer and false-skip the next re-bind (FramebufferImpl, Managers.h).
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, target.Framebuffer);
}
void HostFrameTargetDestroy(HostFrameTarget& target) {
if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers)
g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer);
if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers)
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
if (target.Image != nullptr) {
String why;
const EntryPoints& points = Resolved(why);
if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay)
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast<EGLImageKHR>(target.Image));
}
target = HostFrameTarget{};
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend::DirectGLES {
Bool HostFrameTargetSupported(String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
void HostFrameTargetBind(const HostFrameTarget&) {}
void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; }
}
#endif
@@ -1,52 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.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 <EGL/egl.h>
#include <EGL/eglext.h>
struct AHardwareBuffer;
namespace MobileGL::MG_Backend::DirectGLES {
// THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET.
//
// The display host (the Android side that owns the screen) allocates its frames itself and
// offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured
// on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side
// allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames
// a render server can draw into are the ones the host made for it.
//
// What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a
// framebuffer that is the drawing target while that frame is the current one. None of
// Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is
// the point: the host puts the frame on the glass when this side says it is drawn.
struct HostFrameTarget {
void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types
Uint Renderbuffer = 0;
Uint Framebuffer = 0;
Uint Width = 0;
Uint Height = 0;
};
// Whether the driver advertises what this is spelled in. False is answered with the missing
// name in `why`, never an abort: a server that cannot take host frames has to be able to
// say so.
Bool HostFrameTargetSupported(String& why);
// Imports `buffer` as a render target. On failure `target` is left empty and `why` says
// which call refused; no GL name is leaked.
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why);
// Binds the target as the current draw framebuffer, called when its frame becomes current.
void HostFrameTargetBind(const HostFrameTarget& target);
// Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which
// releases it; this never touches it.
void HostFrameTargetDestroy(HostFrameTarget& target);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52 -275
View File
@@ -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,173 +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) {
// The applier has already consumed the persistent-map blocks for this
// resource before the draw verb replayed; the one Sync* this arm owes is the
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
// a shader wrote on this side is read back from this server's own GL buffer,
// because no writeback ever reaches the server's staged 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 = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
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
@@ -258,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;
}
@@ -361,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);
@@ -377,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;
}
@@ -394,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;
@@ -419,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;
@@ -564,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;
@@ -618,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) {
@@ -636,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;
}
@@ -709,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);
@@ -731,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",
@@ -745,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;
}
@@ -914,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;
@@ -953,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);
@@ -1108,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
-785
View File
@@ -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
+2 -71
View File
@@ -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
View File
@@ -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:
@@ -280,184 +280,3 @@
bytes = Move(result);
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
// its FBO attachment is incomplete - which used to end the session at
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent) {
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
GLenum binding = GL_NONE;
switch (nativeTarget) {
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return true;
}
// A cube's levels are per face; every other target answers for the whole level.
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
DrainESErrors();
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(binding, &previous);
g_GLESFuncs.glBindTexture(nativeTarget, texture);
GLint width = 0, height = 0, depth = 0;
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
if (!answered) return true;
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
static const IntVec3 s_refused = [] {
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
if (value == nullptr) return IntVec3{0, 0, 0};
char* end = nullptr;
const long width = std::strtol(value, &end, 10);
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
const long height = std::strtol(end + 1, nullptr, 10);
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
}();
return s_refused.x() > 0 && extent == s_refused;
}
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
// written by the driver except by copies the store followed (!IsDriverWritten - see
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
// Anything else DECLINES BY NAME: the reply is an error, and the client's
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
// since overwritten.
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
GLenum format, GLenum type, Vector<Uint8>& bytes) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = static_cast<Uint16>(uploadTarget);
const auto mip = static_cast<Uint16>(level);
ReadbackChannelMapping mapping{};
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
static_cast<SizeT>(std::max(extent.z(), 0));
const char* declined = nullptr;
if (!store.CopiesIntoServerStorage()) {
declined = "this arm keeps no staged store";
} else if (store.IsDriverWritten(key)) {
declined = "the driver has written the texture since its bytes crossed";
} else if (!store.IsCovered(key, target, mip)) {
declined = "not every byte of the level crossed";
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
declined = "the staged level is not the extent asked for";
} else if (pixelBytes == 0 || texels == 0) {
declined = "the format/type pair has no readback mapping";
}
Vector<Uint8> out;
if (declined == nullptr) {
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
declined = "the staged words are not the client's word size";
} else {
out.assign(staged, staged + texels * pixelBytes);
}
} else {
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
isInteger, isSigned)) {
declined = "the staged format has no wide decoding";
} else if (mapping.isInteger != isInteger) {
declined = "an integer read of a normalized level, or the reverse";
} else {
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
const SizeT rowTexels = static_cast<SizeT>(extent.x());
const SizeT rows = texels / rowTexels;
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
out.resize(texels * pixelBytes);
for (SizeT row = 0; row < rows; ++row) {
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
out.data() + row * rowTexels * pixelBytes, rowTexels,
wideType, mapping, type);
}
}
}
}
if (declined != nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
res.Slot, res.Gen, level, declined);
return false;
}
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
"answered from the server's staged store (texture-readback-from-store)",
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
bytes = Move(out);
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)};
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
// ReadTextureLevelTight's first choice for exactly these reads).
const Bool packedWordRead =
!twin->RequiresImageBindableStorage() &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
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 (refusedForTesting ||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
info.Format, info.Type, slice)) return false;
}
result.insert(result.end(), slice.begin(), slice.end());
}
bytes = Move(result);
return true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
@@ -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,29 +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();
}
// And the swap interval it asked for (DirectVulkan.h).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
ForgetRequestedSwapInterval();
}
}
#else
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
#endif
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
@@ -389,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;
}
@@ -422,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());
@@ -556,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
@@ -659,38 +625,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary,
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
// missing checkSupported() answered false, EglContext::create() returned nullptr,
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
// rendering context" and quit before drawing a frame.
//
// The three are true statements about this backend too, and the entry points behind
// them are the same ones the DirectGLES half of this list is advertised with: the
// frontend implements all 39 ARB_shader_objects procedures, ARB_vertex_shader's three
// attrib-location entry points, and nothing for ARB_fragment_shader, whose "New
// Procedures and Functions" section reads "None". The shader and program objects live in
// MG_Impl/MG_State, above the backend that draws with them, so a name that is true on
// one backend is true on the other.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
// NPOT textures: Vulkan puts no power-of-two restriction on an image, its mip chain or
// its samplers, so this is the strongest form of the promise the string makes. Kept
// identical to the DirectGLES group so the two backends do not disagree about what
// MobileGL is.
E_GL_ARB_texture_non_power_of_two,
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
// and this name is what makes them reachable - libepoxy in kwin resolves
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
// The render-server half of the import (fd over the GBM socket, then the server-side
// EGLImage) is the next change; until it lands an imported texture has no storage behind
// it, which is logged once at the entry point rather than left to look like a black frame.
E_GL_OES_EGL_image};
E_GL_ARB_get_program_binary};
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
@@ -751,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;
}
@@ -815,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;
@@ -858,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();
}
@@ -1012,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);
@@ -77,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
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);
@@ -151,10 +141,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
void SetSwapInterval(Int interval);
Optional<Int> GetRequestedSwapInterval();
#if MOBILEGL_BUILD_DISAGGREGATED
// The swap interval an ended server session asked for is not the next one's, the same rule as
// DirectGLES::ForgetRequestedSwapInterval: under a transport ~BackendObject_DirectVulkan forgets
// it, so the next session starts at the default until it asks.
void ForgetRequestedSwapInterval();
#endif
} // 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
@@ -13,11 +13,6 @@
#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>
@@ -307,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:
@@ -322,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
@@ -341,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
}
File diff suppressed because it is too large Load Diff
@@ -9,11 +9,9 @@
#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>
@@ -56,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; }
@@ -102,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);
@@ -111,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
@@ -137,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,
@@ -158,7 +130,7 @@ 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);
// ---- per-frame descriptor pools ----------------------------------------------------
@@ -215,66 +187,6 @@ 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
@@ -283,12 +195,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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;
@@ -298,7 +204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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
@@ -307,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;
@@ -315,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.
@@ -362,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,
@@ -390,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
@@ -415,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);
@@ -545,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;
@@ -563,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
File diff suppressed because it is too large Load Diff
@@ -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,56 +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);
// P8-D: whether a shader write was marked on this store since the last indirect draw
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
// barrier before the draw when this answers true. Transfer writes need no mark: their own
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// 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);
// P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer
// `res`'s store - a VkBuffer on memory imported through
// VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every
// wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers)
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
// imported (the T2 store is untouched and the client is DECLINED).
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
// The POST self-test of the sustained-lock pattern on this device (see the definition).
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
#endif
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex);
@@ -225,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.
@@ -239,131 +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;
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
// no bytes (the client reads its own pages once the wait below it is done).
Bool imported = false;
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
Bool indirectReadBarrierPending = false;
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
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);
// P11 B2: the serial a T0 store's release waits behind (never "destroy at once").
Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const;
// 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);
@@ -376,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);
@@ -397,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;
@@ -8,45 +8,13 @@
#include "VkBufferObject.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Transport/AdoptT0.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
m_device = other.m_device;
m_externalMemory = other.m_externalMemory;
m_externalAhb = other.m_externalAhb;
m_externalCoherent = other.m_externalCoherent;
other.m_device = VK_NULL_HANDLE;
other.m_externalMemory = VK_NULL_HANDLE;
other.m_externalAhb = nullptr;
other.m_externalCoherent = true;
}
void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb) {
Destroy();
m_device = device;
m_buffer = buffer;
m_externalMemory = memory;
m_mappedData = mapped;
m_size = size;
m_externalCoherent = coherent;
m_externalAhb = ahb;
}
#endif
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -67,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -124,24 +89,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Destroy() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
// P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's
// deferred release, i.e. once no submission can still name it.
if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory);
if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr);
vkFreeMemory(m_device, m_externalMemory, nullptr);
MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb);
m_mappedData = nullptr;
m_buffer = VK_NULL_HANDLE;
m_externalMemory = VK_NULL_HANDLE;
m_externalAhb = nullptr;
m_device = VK_NULL_HANDLE;
m_externalCoherent = true;
m_size = 0;
return;
}
#endif
Unmap();
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
@@ -170,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Unmap() {
#if MOBILEGL_BUILD_DISAGGREGATED
// An imported store stays mapped for its life (Destroy unmaps it).
if (m_externalMemory != VK_NULL_HANDLE) return;
#endif
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
@@ -200,16 +143,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
@@ -234,16 +167,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resolvedSize == 0) {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
@@ -64,25 +64,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
#endif
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
// like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and
// the memory and drops `ahb` - which is why the store goes through the wire arm's
// serial-gated deferred release like every other: the AHardwareBuffer is let go only once
// no submission can still name it. `coherent` = the memory type is HOST_COHERENT.
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb);
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
#endif
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
private:
VmaAllocator m_allocator = nullptr;
@@ -90,12 +72,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
VkDevice m_device = VK_NULL_HANDLE;
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
void* m_externalAhb = nullptr;
Bool m_externalCoherent = true;
void MoveExternalFrom(VkBufferObject& other);
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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;
@@ -350,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;
@@ -390,9 +371,6 @@ public:
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() {
@@ -457,9 +435,6 @@ public:
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
syncedWireSerial = 0;
#endif
}
~TextureResource() {
@@ -486,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.
@@ -551,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,
@@ -768,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,42 +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; }
// P8-D: the wire arm's indirect draw families, issued as vkCmdDraw[Indexed]Indirect[Count]
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
// words on the CPU; TRUE = issued, or declined by name.
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
Bool indexed, Bool counted, Uint64 countOffset);
// P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from
// this device. False when the device did not take VK_ANDROID_external_memory_android_
// hardware_buffer at creation (not Android, not advertised, or a monolith device).
struct WireAhbImport {
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
Uint32 queueFamily = 0;
VkPhysicalDeviceMemoryProperties memory{};
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
};
Bool GetWireAhbImport(WireAhbImport& out) const;
#endif
// FrameContext::IRecordingObserver: prepares the frame's timer-query
// pool (harvest + reset) right after the frame command buffer begins
@@ -252,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).
@@ -286,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.
@@ -445,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;
@@ -466,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
@@ -513,200 +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;
// P11 B2 (T0): the device took the AHardwareBuffer import extensions at creation, and the
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
Bool m_wireAhbImport = false;
void* m_wireGetAhbProperties = nullptr;
// 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;
// P8-D: set only while DrawWireIndirectNative is inside SetupDraw, so SetupWireDraw can
// tell the wire arm's own indirect draw from a monolith indirect entry point reached under
// a transport (still the `buffer-legacy-arm` role violation).
Bool m_wireNativeIndirectDraw = false;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
void DispatchWireComputeIndirect(GLintptr offset);
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);
@@ -753,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;
@@ -936,12 +660,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
#endif
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
// (and its multiDraw feature) is enabled; null otherwise.
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
@@ -985,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{};
@@ -1006,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
@@ -1121,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
@@ -1159,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
@@ -1200,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
@@ -1361,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;
@@ -1453,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
@@ -1734,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
@@ -1767,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);
@@ -1843,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,
@@ -1855,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);
@@ -1969,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, &copy);
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,128 +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 (and DispatchWireComputeIndirect, P8-SV) -------------------------
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)
// ---- VulkanRenderer::GenerateWireMipmap (P8-F) -----------------------------------------
// `mipmap-shader-format-or-shape` was a Fatal on this arm and a decline on the monolith arm; it
// is the monolith's decline now. Counted once per refused glGenerateMipmap, whose chain is left
// as it was.
MGL_WIRE_DECLINE(MipmapShaderFormatOrShape)
// ---- VulkanRenderer::DrawWireIndirectNative (P8-D), in statement order -----------------------
MGL_WIRE_DECLINE(IndirectBufferUnbound)
MGL_WIRE_DECLINE(IndirectCommandRange)
MGL_WIRE_DECLINE(IndirectCountRange)
MGL_WIRE_DECLINE(IndirectIndexStore)
// ---- P8-SV -------------------------------------------------------------------------------
// VulkanRenderer::GenerateWireMipmap: a combined depth/stencil (or stencil-only) chain, which no
// arm generates (CONTRACT-P7 §3.2). It logged but was never counted, so MGWIRE-DECLINES could
// not see it. Counted once per refused glGenerateMipmap, whose chain is left as it was.
MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
// VulkanRenderer::DispatchWireComputeIndirect, in statement order: the dispatch-indirect store
// is missing, or the three group-count words lie outside it or are misaligned.
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
MGL_WIRE_DECLINE(DispatchIndirectRange)
@@ -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,328 +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;
// P8-SV: THE PASS MAKES 2D VIEWS (makeView below), which a 1D, 1D-array or 3D image does
// not admit. A caller that fills viewType from the image gets a refusal here, and the
// wire arm's GenerateWireMipmap asks the same question before it grows the chain.
for (const VkImageViewType viewType : {source.viewType, destination.viewType}) {
if (viewType == VK_IMAGE_VIEW_TYPE_1D || viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
viewType == VK_IMAGE_VIEW_TYPE_3D)
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, &copy);
};
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,849 +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;
}
// P8-D: the wire arm's own indirect draw (DrawWireIndirectNative) is the one caller that
// may pass this aspect; a monolith indirect entry point reached here is still the role
// violation.
if ((aspects & DrawSetupAspect::IndirectDrawBuffer) && !m_wireNativeIndirectDraw)
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];
// P8-F: A RECORD INVARIANT, NOT AN UNMIGRATED VERB (it carried a P8 phase tag).
// The client turns every enabled client-memory array into an owned buffer before the
// draw record is built, or refuses the draw itself (MG_Impl/Pipe/OwnedDrawInputs.h),
// so an attribute the program reads with no buffer handle is a foreign peer's record.
if (MG_Pipe::MGPipeHandleIsNull(resource.Res))
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an "
"attribute the program reads carries no buffer handle", location);
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. So may an
// indirect draw (P8-D): its vertex range is in GPU words nobody read.
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) &&
!(aspects & DrawSetupAspect::IndirectDrawBuffer) && !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;
}
// ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ----------------------
//
// The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect
// and MultiDrawElementsIndirectCount), with the wire store's own VkBuffer in place of the
// frontend buffer's resident slice. Before this, every wire indirect draw read its commands
// through ReadWireBuffer and expanded them on the CPU, and a store a shader had written made
// that read a whole-GPU wait (WaitForWireBufferHostAccess) per draw.
//
// THE ONE FALSE: a COUNT form this device cannot issue - the monolith arm's own condition for
// vkCmdDrawIndexedIndirectCount (MultiDrawElementsIndirectCount), extended to the non-indexed
// twin the monolith never loads. Everything else is issued here or declined by name.
Bool VulkanRenderer::DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted, Uint64 countOffset) {
if (drawcount <= 0) return true;
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
if (!m_drawIndirectCountExtensionEnabled || countEntry == nullptr ||
(!m_multiDrawIndirectFeatureEnabled && drawcount != 1))
return false;
}
const auto& state = MG_Pipe::MGPipeApplier();
// GL's command sizes (GL 4.6 10.3.10), which are also Vulkan's.
const Uint64 commandSize = indexed ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32);
if (stride == 0) stride = static_cast<GLsizei>(commandSize);
BufferSlice commands{};
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectBuffer, commands)) {
MGL_WIRE_DECLINE_AT(IndirectBufferUnbound, "an indirect draw names no live indirect buffer store");
return true;
}
const Uint64 span = static_cast<Uint64>(stride) * static_cast<Uint64>(drawcount - 1) + commandSize;
// GL refuses a stride that is not a multiple of four (INVALID_VALUE), and Vulkan requires
// it for drawCount > 1; a peer's record that breaks either is declined, not issued.
if (stride < static_cast<GLsizei>(commandSize) || (stride & 3) != 0 || (offset & 3u) != 0 ||
offset > commands.size || span > commands.size - offset) {
MGL_WIRE_DECLINE_AT(IndirectCommandRange, "the indirect commands lie outside their store or are misaligned");
return true;
}
BufferSlice parameters{};
if (counted) {
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectParameterBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectParameterBuffer, parameters) ||
(countOffset & 3u) != 0 || countOffset > parameters.size || parameters.size - countOffset < sizeof(Uint32)) {
MGL_WIRE_DECLINE_AT(IndirectCountRange, "the indirect draw count lies outside its parameter store");
return true;
}
}
// The index view is the WHOLE bound element store past its binding offset, as the monolith
// arm binds it: the commands name their own firstIndex, which only the GPU reads.
IndexBufferView indices{};
DrawCmdParam vertexRange{};
vertexRange.instanceCount = 1;
Flags<DrawSetupAspect> aspects = DrawSetupAspect::IndirectDrawBuffer;
if (indexed) {
const SizeT width = MG_Util::GetGLTypeSize(type);
BufferSlice elements{};
if (width == 0 || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, elements) ||
state.IndexBuffer.Offset > elements.size) {
MGL_WIRE_DECLINE_AT(IndirectIndexStore, "an indexed indirect draw names no usable element store");
return true;
}
indices.indexType = type;
indices.indexByteOffset = 0;
indices.indexByteSize = static_cast<SizeT>(elements.size - state.IndexBuffer.Offset);
vertexRange.vertexCount = static_cast<Uint32>(indices.indexByteSize / width);
aspects |= DrawSetupAspect::IndexBuffer;
}
// A shader write the store was marked with (MarkWireBufferGpuWritten) is made visible to
// the indirect read here, once per write, so a program that omits GL_COMMAND_BARRIER_BIT
// is not left racing a read the old CPU path used to take after a whole-GPU wait. The
// application's own glMemoryBarrier is BuildMemoryBarrierForGlBarriers, as on the monolith
// arm; transfer writes carry their own MEMORY_READ after-barrier (VkBufferManager.cpp).
const Bool commandsWritten = m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectBuffer);
const Bool countWritten = counted && m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectParameterBuffer);
if (commandsWritten || countWritten) {
RetireWireDrawPass();
auto& barrierFrame = m_frameContext.GetCurrent();
if (!barrierFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
VkRenderPassManager::EndRenderPass(barrierFrame.commandBuffer);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
vkCmdPipelineBarrier(barrierFrame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
}
auto& frame = m_frameContext.GetCurrent();
m_wireNativeIndirectDraw = true;
const Bool ready = SetupDraw(frame, mode, aspects, vertexRange, indexed ? &indices : nullptr);
m_wireNativeIndirectDraw = false;
if (!ready) return true;
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
const VkCommandBuffer commandBuffer = frame.commandBuffer;
const VkDeviceSize first = commands.offset + offset;
const Uint32 count = static_cast<Uint32>(drawcount);
const Uint32 byteStride = static_cast<Uint32>(stride);
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
countEntry(commandBuffer, commands.buffer, first, parameters.buffer, parameters.offset + countOffset,
count, byteStride);
} else if (count == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled)) {
// maxDrawIndirectCount bounds one call; GL's drawcount does not.
const Uint32 limit = std::max<Uint32>(1u, m_physicalDevice.properties.limits.maxDrawIndirectCount);
for (Uint32 base = 0; base < count; base += limit) {
const Uint32 chunk = std::min(count - base, limit);
const VkDeviceSize at = first + static_cast<VkDeviceSize>(base) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
}
} else {
// No multiDrawIndirect: one indirect draw per command, still read by the GPU
// (the monolith arm's own fallback).
for (Uint32 i = 0; i < count; ++i) {
const VkDeviceSize at = first + static_cast<VkDeviceSize>(i) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, 1, 0);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, 1, 0);
}
}
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDraws, 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);
}
// ---- P8-SV: THE WIRE ARM'S INDIRECT DISPATCH READS ITS GROUP COUNTS ON THE GPU ----------
//
// The monolith arm's shape (VulkanRenderer.cpp's DispatchComputeIndirect) with the wire
// store's own VkBuffer in place of the frontend buffer's resident slice. Before this the wire
// arm read the three words through ReadWireBuffer and dispatched them from the CPU, and a store
// a shader had written made that read a whole-GPU wait (WaitForWireBufferHostAccess) per call.
// There is no shape the device cannot issue, so there is no CPU path left: a record whose
// store or words are unusable is declined by name.
//
// The barrier is P8-D's draw rule (DrawWireIndirectNative): a store marked shader-written gets
// one MEMORY_WRITE -> INDIRECT_COMMAND_READ barrier before its next indirect read, whether or
// not the application issued GL_COMMAND_BARRIER_BIT; DRAW_INDIRECT is the stage that consumes
// VkDispatchIndirectCommand too. It is TAKEN before the bindings below, because binding this
// dispatch's own storage buffers marks them written, and a store that is both this dispatch's
// parameters and one of its outputs must keep that mark for the next reader.
void VulkanRenderer::DispatchWireComputeIndirect(GLintptr offset) {
RetireWireDrawPass();
const auto& state = MG_Pipe::MGPipeApplier();
constexpr Uint64 kCommandSize = 3 * sizeof(Uint32); // GL 4.6 10.3.10 / VkDispatchIndirectCommand
BufferSlice parameters{};
if (MG_Pipe::MGPipeHandleIsNull(state.VerbDispatchIndirectBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbDispatchIndirectBuffer, parameters)) {
MGL_WIRE_DECLINE_AT(DispatchIndirectBufferUnbound, "an indirect dispatch names no live dispatch-indirect store");
return;
}
// GL refuses an offset that is negative or not a multiple of four (INVALID_VALUE) and a
// command past the end of the buffer (INVALID_OPERATION); a peer's record that breaks
// either is declined, not issued.
if (offset < 0 || (static_cast<Uint64>(offset) & 3u) != 0 || static_cast<Uint64>(offset) > parameters.size ||
parameters.size - static_cast<Uint64>(offset) < kCommandSize) {
MGL_WIRE_DECLINE_AT(DispatchIndirectRange, "the dispatch group counts lie outside their store or are misaligned");
return;
}
auto& frame = m_frameContext.GetCurrent();
if (m_bufferManager.TakeWireIndirectReadBarrier(state.VerbDispatchIndirectBuffer)) {
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
}
const auto program = WireProgramSource(state.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;
}
// PrepareWireTextureResources may have flushed and begun a new recording.
auto& dispatchFrame = m_frameContext.GetCurrent();
if (!dispatchFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (!m_uniformManager->BindProgramUniformBuffers(dispatchFrame.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(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer,
parameters.offset + static_cast<VkDeviceSize>(offset));
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1);
}
#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
-737
View File
@@ -1,737 +0,0 @@
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GbmFrameChannel.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cerrno>
#include <cstring>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
// WHAT THIS FILE IS WRITTEN AGAINST, AND WHAT HAS NOT BEEN RUN.
//
// The other end exists and is tested on this machine: MobileGL/MG_Gbm/GbmHostChannel.cpp dials
// this socket with SOCK_SEQPACKET, reads one message per frame, computes that message's expected
// length from the handle's own two counts, and refuses any other length. Every byte below is
// written to that parser. There is no phone attached to the machine this was written on, so the
// file is compile-verified for aarch64 Android and nothing more: the first run on a device is what
// will say whether the relay in RelayHandle() reads back the handle's bytes exactly as
// AHardwareBuffer_sendHandleToUnixSocket writes them, which is why that path is written to
// survive both layouts the platform's serializer could have (see the note there).
namespace MobileGL::MG_Backend {
namespace {
// THE LAYOUT IS A PROTOCOL, SO ITS SIZES ARE ASSERTED AND NOT ASSUMED. The container
// derives the length of a frame message from these three sizes plus the handle's counts,
// so a field added here, or the packing attribute dropped, would not be a compile error
// over there - it would be every frame skipped with a message about its length.
static_assert(sizeof(GbmFrameOffer) == 10 * sizeof(Uint32), "the offer is ten 32 bit fields on the wire");
static_assert(sizeof(GbmFrameRelease) == 3 * sizeof(Uint32), "the release is three 32 bit fields on the wire");
static_assert(sizeof(GbmNativeHandleHeader) == 3 * sizeof(Int32),
"a native handle begins with a version and two counts, on every Android ABI");
// The container reads one message per frame into a 4 KiB buffer and skips a message that
// does not fit in it (GbmHostMessageMax, GbmHostChannel.cpp), so a message larger than this
// is refused here rather than built and dropped there.
constexpr SizeT kMessageMax = 4096;
// The descriptors one handle may carry, which is the container's own cap (GbmHostMaxFds).
// Every AHardwareBuffer handle this project has seen carries one - the dma-buf - and both
// ends refuse a frame whose handle names more than they can take rather than forward a
// truncated list into a buffer of unknown memory.
constexpr Int kMaxHandleFds = 8;
// How many release messages one call reads before it returns to the frame. A cap and not a
// loop to exhaustion: the messages left behind stay in the socket, whose buffer the kernel
// bounds, and the next frame drains them.
constexpr Int kDrainMax = 64;
// HOW MANY DIALERS THE KERNEL MAY HOLD FOR THIS LISTENER. One container is the protocol,
// but the queue is not the connection: every compositor that was restarted dials once more,
// and a listener that only ever has one slot leaves its queue full for the next one. With
// listen(fd, 1) that is exactly what was measured - connect() from the container answered
// EAGAIN(11) on four attempts out of four after two restarts, and no dialer reached this
// side again for as long as the server lived.
constexpr Int kDialerBacklog = 16;
// The acceptor's poll timeout. It is a wake-up and not a schedule, so it only has to be
// short enough that a container dialling into a socket nothing else touches is taken
// promptly; a dialer that arrives while the thread is waiting wakes it at once.
constexpr Int kAcceptPollMs = 100;
// How long the relay read waits for bytes the send one statement earlier has already put in
// the socket. It exists only so that an invariant broken somewhere else cannot park the
// apply thread; on a local socketpair the wait is zero.
constexpr Int kRelayWaitMs = 2;
// close() may overwrite the errno a caller is in the middle of reporting, and every caller
// here is reporting a failure errno describes.
void CloseQuietly(Int fd) {
if (fd < 0) return;
const Int savedErrno = errno;
::close(fd);
errno = savedErrno;
}
}
GbmFrameChannel::~GbmFrameChannel() { Close(); }
Bool GbmFrameChannel::Open() {
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_listen >= 0) return true;
const char* configured = ::getenv(kGbmFrameSocketEnv);
const char* path = (configured != nullptr && configured[0] != 0) ? configured : kGbmFrameSocketPath;
constexpr SizeT pathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
if (std::strlen(path) >= pathMax) {
MGLOG_E_ONCE("gbm frame channel: the container socket path %s does not fit in sun_path (%zu bytes); no "
"container can be served",
path, pathMax);
return false;
}
// SOCK_SEQPACKET because that is what the container dials with (GbmHostChannel::EnsureOpen
// asks for exactly this), and a stream listener answers it EPROTOTYPE - the container
// prints that as "a socket of a different type". SOCK_NONBLOCK because the accept below
// runs on the apply thread and may not wait for a container that is not there; SOCK_CLOEXEC
// because every process this server starts would otherwise inherit a socket that receives
// the phone's frames.
const Int fd = ::socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) {
MGLOG_E_ONCE("gbm frame channel: socket(AF_UNIX, SOCK_SEQPACKET): %s; the frames are drawn and presented "
"without a container",
std::strerror(errno));
return false;
}
// A node left behind by a server that died is not a reason to refuse a container: bind
// would answer EADDRINUSE for a socket nobody is listening on.
::unlink(path);
struct sockaddr_un address = {};
address.sun_family = AF_UNIX;
// The terminator travels with the name: sun_path is a C string for a pathname socket, and a
// name without one is a name the kernel reads past the end of.
std::memcpy(address.sun_path, path, std::strlen(path) + 1);
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0) {
MGLOG_E_ONCE("gbm frame channel: bind(%s): %s; the frames are drawn and presented without a container",
path, std::strerror(errno));
CloseQuietly(fd);
return false;
}
// connect() to a pathname socket needs write permission on the node, and the container is
// not this process's user, so the umask's usual 0755 would leave it refused for a reason it
// could not see. 0666 is what makes the node reachable by the other half of a split run; a
// chmod that fails is not fatal, because a peer running as this uid still dials it.
if (::chmod(path, 0666) != 0) {
MGLOG_W_ONCE("gbm frame channel: chmod(%s, 0666): %s; a container running as another user cannot dial "
"this socket",
path, std::strerror(errno));
}
if (::listen(fd, kDialerBacklog) != 0) {
MGLOG_E_ONCE("gbm frame channel: listen(%s): %s; the frames are drawn and presented without a container",
path, std::strerror(errno));
::unlink(path);
CloseQuietly(fd);
return false;
}
m_listen = fd;
m_path = path;
// THE ACCEPTOR IS STARTED WITH THE SOCKET AND NOT WITH THE FIRST FRAME. A compositor dials
// at its device creation and keeps the connection, so the dial a frame would have answered
// has already happened by the time that frame is offered: an accept on the offer path is an
// accept that runs after the thing it exists for. The descriptor is copied into the thread
// as an argument - it is the one piece of this object the thread reads without the lock,
// which is what lets Close() join it before closing that descriptor.
m_accepting.store(true, std::memory_order_release);
try {
m_acceptor = std::thread(&GbmFrameChannel::AcceptLoop, this, fd);
} catch (...) {
// A thread that cannot be made is a phone that is out of them, not a reason to refuse
// the frames the container can still be given one at a time.
m_accepting.store(false, std::memory_order_release);
MGLOG_W_ONCE("gbm frame channel: the acceptor thread could not be started; a container is only taken in "
"when a frame is offered, and one that dials between frames waits for the next one");
}
MGLOG_I("gbm frame channel: listening on %s for one container (SOCK_SEQPACKET, one message per frame)", path);
return true;
}
Bool GbmFrameChannel::EnsureRelay() {
if (m_relay[0] >= 0 && m_relay[1] >= 0) return true;
// Both ends are dropped before a new pair is made: half a socketpair is a socket with bytes
// in it that nothing will ever read, and the next handle read from it would be the tail of
// the last one.
DropRelay();
Int pair[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) != 0) {
MGLOG_E_ONCE("gbm frame channel: socketpair for the handle relay: %s; no frame can be offered until one "
"can be made",
std::strerror(errno));
return false;
}
m_relay[0] = pair[0];
m_relay[1] = pair[1];
return true;
}
void GbmFrameChannel::DropRelay() {
CloseQuietly(m_relay[0]);
CloseQuietly(m_relay[1]);
m_relay[0] = -1;
m_relay[1] = -1;
}
Bool GbmFrameChannel::RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
Int* fds, Int& fdCount) {
payloadBytes = 0;
fdCount = 0;
if (!EnsureRelay()) return false;
// WHY A FRAME'S HANDLE GOES THROUGH A SOCKETPAIR OF THIS CHANNEL'S OWN.
//
// The container takes ONE message per frame - the offer, then the handle's counts, then the
// arrays those counts describe, with the descriptors in that same message's SCM_RIGHTS
// control data - and it refuses a message of any other length. Two sends cannot be one
// message on a message-oriented socket, so AHardwareBuffer_sendHandleToUnixSocket, which
// sends a handle as a message of its own, is pointed at a socketpair this channel owns, and
// what comes out of the other end is both halves of what the container wants: the handle's
// own bytes, read here, and its descriptors, as SCM_RIGHTS.
//
// The alternative does not exist: the NDK's <android/hardware_buffer.h> declares no way to
// ask a buffer for its descriptors. AHardwareBuffer_getNativeHandle is a platform API and
// is absent from the NDK's copy of that header (checked against NDK 27.3.13750724 in this
// tree's build-termux toolchain), so this is the one route from an AHardwareBuffer to a
// dma-buf descriptor that an app can take.
const Int code = AHardwareBuffer_sendHandleToUnixSocket(buffer, m_relay[0]);
if (code != 0) {
MGLOG_W_ONCE("gbm frame channel: the frame's handle did not cross the relay socket (rc %d, errno %d); the "
"frame is not offered to the container",
code, errno);
// The relay is dropped rather than kept: a send that failed part way through would
// leave bytes that the next handle's read would take for its own header.
DropRelay();
return false;
}
struct iovec vector = {};
vector.iov_base = payload;
vector.iov_len = capacity;
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
struct msghdr message = {};
message.msg_iov = &vector;
message.msg_iovlen = 1;
message.msg_control = control;
message.msg_controllen = sizeof(control);
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC so a descriptor
// read out of the relay does not survive into a child of this process, which would then hold
// the phone's frame until it died.
ssize_t got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
if (got < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
struct pollfd waiting = {};
waiting.fd = m_relay[1];
waiting.events = POLLIN;
if (::poll(&waiting, 1, kRelayWaitMs) > 0) {
// The kernel rewrites these on every call, so a retry starts from the same state as
// the first attempt.
message.msg_controllen = sizeof(control);
message.msg_flags = 0;
got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
}
}
if (got < 0) {
MGLOG_W_ONCE("gbm frame channel: a relayed handle could not be read back (%d: %s); the frame is not "
"offered to the container",
errno, std::strerror(errno));
DropRelay();
return false;
}
if (got == 0) {
// Zero bytes on a connected socket is the end of it. Only this process holds either end
// of this socketpair, so this is a bug being reported rather than a peer going away.
MGLOG_W_ONCE("gbm frame channel: the handle relay is closed; the frame is not offered to the container");
DropRelay();
return false;
}
// The descriptors come out of the control data before anything is decided about the
// message, because they belong to this process from the moment they were received: a path
// that returns without closing them leaks a descriptor per frame.
Int collected = 0;
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
header = CMSG_NXTHDR(&message, header)) {
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS) continue;
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
const Int count = (Int)(bytes / sizeof(Int));
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
for (Int i = 0; i < count; ++i) {
if (collected < kMaxHandleFds) {
fds[collected++] = descriptors[i];
} else {
// More descriptors than a frame can carry: they are this process's now, so they
// are closed here rather than leaked.
CloseQuietly(descriptors[i]);
}
}
}
if ((message.msg_flags & MSG_CTRUNC) != 0) {
MGLOG_W_ONCE("gbm frame channel: a frame's control data did not fit in %zu bytes; the frame is not "
"offered because its descriptor list is incomplete",
sizeof(control));
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
if ((message.msg_flags & MSG_TRUNC) != 0 || (SizeT)got < sizeof(GbmNativeHandleHeader)) {
// A message longer than a handle can be, or shorter than the three counts every handle
// begins with. Either way it cannot describe a frame, and a relay that has been read
// past the end of one message must not carry the next frame's header: it is recreated.
MGLOG_W_ONCE("gbm frame channel: a relayed handle is %zd bytes and %s; the frame is not offered", got,
((message.msg_flags & MSG_TRUNC) != 0) ? "longer than a handle can be"
: "too short to hold its own counts");
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
// THE BYTES THAT COME BACK ARE THE HANDLE'S INTEGER ARRAY, NOT A handle AND ITS COUNTS.
// Measured on the phone: parsing the front of this payload as {version, numFds, numInts}
// produced a NumFds taken from the buffer's own integer data, and every frame was refused
// with "its handle carries more descriptors than either end takes" - which is what the
// container saw as a connection that was accepted and then closed without a frame. The two
// counts are known HERE and are not in those bytes: the descriptors came out of the control
// data (collected of them) and the payload is their integer array. So the header is BUILT
// rather than copied, and what leaves this process is a real native_handle_t layout -
// version, numFds, numInts, then the ints - which is exactly what the container's parser
// reads (GbmInternal.h's AHBNativeHandleHeader).
if (payloadBytes + sizeof(GbmNativeHandleHeader) > capacity) {
MGLOG_W_ONCE("gbm frame channel: a handle's integer array does not fit in a frame message with its "
"header; the frame is not offered to the container");
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
std::memmove(payload + sizeof(GbmNativeHandleHeader), payload, payloadBytes);
GbmNativeHandleHeader header = {};
header.Version = (Int32)sizeof(GbmNativeHandleHeader);
header.NumFds = (Int32)collected;
header.NumInts = (Int32)(payloadBytes / sizeof(Int32));
std::memcpy(payload, &header, sizeof(header));
payloadBytes += sizeof(header);
fdCount = collected;
return true;
}
// The acceptor. It touches the listener - its own copy of the descriptor, which nothing else
// closes while this loop runs - and, once per dialer, the channel under its lock.
void GbmFrameChannel::AcceptLoop(Int listen) {
while (m_accepting.load(std::memory_order_acquire)) {
struct pollfd waiting = {};
waiting.fd = listen;
waiting.events = POLLIN;
const Int ready = ::poll(&waiting, 1, kAcceptPollMs);
if (ready < 0) {
if (errno == EINTR) continue;
MGLOG_W_ONCE("gbm frame channel: poll on the listening socket failed (%d: %s); no container can be "
"taken in until the socket is opened again",
errno, std::strerror(errno));
return;
}
if (ready == 0) continue;
// accept4, so the connection is close-on-exec and non-blocking from the moment it
// exists: a container that stops reading must not be able to park a send on either
// thread.
const Int fd = ::accept4(listen, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
if (fd < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED) continue;
MGLOG_W_ONCE("gbm frame channel: accept failed (%d: %s); no container can be taken in until the "
"socket is opened again",
errno, std::strerror(errno));
return;
}
const std::lock_guard<std::mutex> lock(m_mutex);
AdoptLocked(fd);
}
}
void GbmFrameChannel::AdoptLocked(Int fd) {
if (ClientAliveLocked()) {
// ONE CLIENT, which is what the protocol is written for, and a dialer that arrives while
// a live one is connected is closed rather than left connected - it learns it is not the
// one being served instead of holding a connection whose frames nobody reads.
CloseQuietly(fd);
MGLOG_W_ONCE("gbm frame channel: a second peer dialled %s while a container is connected; it is closed "
"and the frames stay with the connected one",
m_path.c_str());
return;
}
m_client = fd;
MGLOG_I("gbm frame channel: a container is connected on %s; every frame the host offers is sent to it as "
"the dma-buf descriptor behind it",
m_path.c_str());
// AND THE FRAME IT DIALED FOR GOES OUT FROM HERE. A compositor creates its GBM device before
// it asks for a surface, so the frame that is the draw target right now was offered before
// this connection existed; a container that is not told about it allocates every buffer of
// its new swapchain locally, and keeps them for the life of the process.
ReofferFrameLocked();
}
Bool GbmFrameChannel::ClientAliveLocked() {
if (m_client < 0) return false;
Uint8 peek = 0;
// MSG_PEEK, because a release waiting in the socket belongs to the render path: this test
// only asks whether the peer is still there, and a release it consumed would be a frame the
// channel believed was still outstanding.
const ssize_t got = ::recv(m_client, &peek, sizeof(peek), MSG_DONTWAIT | MSG_PEEK);
if (got > 0) return true;
if (got == 0) {
// Zero bytes on a connected socket is the end of it, whether the container exited or
// the kernel gave up on it. A compositor that crashed and is being restarted is
// exactly this, and its successor is the dialer standing right here.
MarkDown("the container closed the connection", 0);
return false;
}
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return true;
MarkDown("recv", errno);
return false;
}
void GbmFrameChannel::MarkDown(const char* what, Int error) {
if (m_client < 0) return;
CloseQuietly(m_client);
m_client = -1;
if (error != 0) {
MGLOG_W("gbm frame channel: the container connection is down: %s failed (%d: %s); the frames are still "
"drawn and presented, and the container stops receiving them until it dials again",
what, error, std::strerror(error));
} else {
MGLOG_W("gbm frame channel: the container connection is down: %s; the frames are still drawn and "
"presented, and the container stops receiving them until it dials again",
what);
}
}
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer) {
if (buffer == nullptr) return false;
// Open() does nothing once the socket exists, so this is an integer test in the common case.
Open();
const std::lock_guard<std::mutex> lock(m_mutex);
// The releases the container has sent since the last frame, which is also how a container
// that died is noticed. Whoever replaced it was taken by the acceptor's own thread the
// moment it dialled: that thread is what makes a container's connect() succeed at all.
PollLocked();
// AND THE FRAME IS REMEMBERED BEFORE IT IS SENT. A container that dials while this call is
// running is one the acceptor hands the frame it finds here to, so the two threads must not
// be able to disagree about which frame is the one being drawn into.
const Uint32 seq = m_nextSeq;
RememberFrameLocked(buffer, offer, seq);
if (m_client < 0) return false;
return SendFrameLocked(buffer, offer, seq, /*counting=*/true);
}
void GbmFrameChannel::RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq) {
if (m_frame == buffer) {
// The host redrawing into the buffer it already handed over: the same frame under a new
// sequence number. Acquiring it again would be a reference this channel takes and never
// gives back.
m_frameOffer = offer;
m_frameSeq = seq;
return;
}
// The reference is this channel's own, and it is what makes the frame a container that dials
// late is owed safe to hold: the backend drops its handle the moment the frame is presented,
// and a frame owed to a dialer arriving in the same millisecond may not be memory that has
// already gone back to the host's pool.
AHardwareBuffer_acquire(buffer);
ReleaseFrameLocked();
m_frame = buffer;
m_frameOffer = offer;
m_frameSeq = seq;
}
void GbmFrameChannel::ReleaseFrameLocked() {
if (m_frame == nullptr) return;
AHardwareBuffer_release(m_frame);
m_frame = nullptr;
m_frameOffer = {};
m_frameSeq = 0;
}
void GbmFrameChannel::DropFrame() {
const std::lock_guard<std::mutex> lock(m_mutex);
ReleaseFrameLocked();
}
void GbmFrameChannel::ReofferFrameLocked() {
if (m_frame == nullptr) return;
// The same frame, under the sequence number it was offered with: it IS the frame that is the
// draw target now, and a second number for it would be this channel claiming the host had
// handed over a frame it never handed over.
MGLOG_I("gbm frame channel: a container dialled after frame %u was offered; it is sent that frame, which is "
"still the draw target",
m_frameSeq);
SendFrameLocked(m_frame, m_frameOffer, m_frameSeq, /*counting=*/false);
}
Bool GbmFrameChannel::SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq,
Bool counting) {
if (m_client < 0) return false;
Uint8 payload[kMessageMax];
SizeT payloadBytes = 0;
Int fds[kMaxHandleFds];
for (Int i = 0; i < kMaxHandleFds; ++i) fds[i] = -1;
Int fdCount = 0;
if (!RelayHandle(buffer, payload, sizeof(payload), payloadBytes, fds, fdCount)) return false;
GbmNativeHandleHeader handle = {};
std::memcpy(&handle, payload, sizeof(handle));
const SizeT tail = ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
const SizeT messageBytes = sizeof(GbmFrameOffer) + sizeof(GbmNativeHandleHeader) + tail;
// EVERY REASON THIS CHANNEL REFUSES A FRAME, IN ONE PLACE. Each of them is a frame the
// container would refuse anyway - it checks the same counts and the same geometry - and a
// frame that is refused here costs nothing: no message, no sequence number, and no entry in
// the count of frames waiting to be released.
const char* refusal = nullptr;
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0) {
refusal = "the host described it with a width, height or stride of zero, and a buffer with no size is "
"not one a compositor can render into";
} else if (handle.NumFds < 1) {
refusal = "its handle carries no descriptor, and a frame without a dma-buf is not one the container "
"can render into";
} else if (handle.NumFds > kMaxHandleFds) {
refusal = "its handle carries more descriptors than either end takes";
} else if (handle.NumInts < 0) {
refusal = "its handle claims a negative number of integers";
} else if ((SizeT)handle.NumInts > kMessageMax / sizeof(Int32)) {
// This is what keeps the message's length arithmetic from wrapping on a 32 bit ABI: a
// handle whose integer array alone is longer than a whole message cannot be carried by
// this protocol whatever the rest of it says, and the check below would otherwise be
// reading a number that had already turned into a small and plausible one.
refusal = "its handle names more integers than a whole message could carry";
} else if (fdCount < handle.NumFds) {
refusal = "fewer descriptors arrived with it than its handle names";
} else if (messageBytes > kMessageMax) {
refusal = "the message it makes is longer than the container reads";
}
Bool offered = false;
if (refusal != nullptr) {
// ONCE, not per frame: a host that describes every frame the same broken way must not
// turn this into a line per frame on the apply thread.
MGLOG_W_ONCE("gbm frame channel: frame %u (%ux%u) is not offered to the container: %s", offer.Index,
offer.Width, offer.Height, refusal);
} else {
// One message, built whole: the offer, then the handle's own three counts, then the two
// arrays the counts describe. What came back from the relay is copied in as far as it
// goes and the remainder is left zero, because the container checks the LENGTH it
// derives from those counts and never reads the arrays themselves - it takes the
// descriptors from the control data and hands the first one to GBM. So a handle whose
// bytes are shorter than its own counts imply is padded rather than refused, and one
// longer than they imply is cut to what the counts say; either way the length the
// container computes is the length it receives.
Uint8 message[kMessageMax] = {};
GbmFrameOffer out = {};
out.Magic = kGbmFrameOfferMagic;
out.Version = kGbmFrameVersion;
out.Seq = seq;
out.Index = offer.Index;
out.Width = offer.Width;
out.Height = offer.Height;
out.StrideBytes = offer.StrideBytes;
out.Format = offer.Format;
out.UsageLo = offer.UsageLo;
out.UsageHi = offer.UsageHi;
std::memcpy(message, &out, sizeof(out));
const SizeT handleBytes = (payloadBytes < sizeof(GbmNativeHandleHeader) + tail)
? payloadBytes
: (sizeof(GbmNativeHandleHeader) + tail);
std::memcpy(message + sizeof(out), payload, handleBytes);
struct iovec vector = {};
vector.iov_base = message;
vector.iov_len = messageBytes;
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
struct msghdr outgoing = {};
outgoing.msg_iov = &vector;
outgoing.msg_iovlen = 1;
outgoing.msg_control = control;
outgoing.msg_controllen = CMSG_SPACE(sizeof(Int) * (SizeT)handle.NumFds);
struct cmsghdr* rights = CMSG_FIRSTHDR(&outgoing);
rights->cmsg_level = SOL_SOCKET;
rights->cmsg_type = SCM_RIGHTS;
rights->cmsg_len = CMSG_LEN(sizeof(Int) * (SizeT)handle.NumFds);
std::memcpy(CMSG_DATA(rights), fds, sizeof(Int) * (SizeT)handle.NumFds);
// MSG_DONTWAIT for the promise at the top of this file, and MSG_NOSIGNAL because a
// server killed by SIGPIPE when the container goes away takes the frames it was drawing
// with it. A full socket, a reset peer and a broken pipe all say the same thing here:
// this connection can no longer be trusted with a frame, so it is closed and the next
// container to dial is served.
const ssize_t sent = ::sendmsg(m_client, &outgoing, MSG_DONTWAIT | MSG_NOSIGNAL);
if (sent != (ssize_t)messageBytes) {
MarkDown("sendmsg", (sent < 0) ? errno : 0);
} else {
// A frame offered for the first time is the next sequence number and one more frame
// outstanding. The same frame sent again to a container that has just dialled is
// neither: the host handed over nothing new, and a count of frames waiting to be
// released may not be moved by a message that describes one already in it.
if (counting) {
m_nextSeq = seq + 1;
++m_offered;
++m_outstanding;
}
offered = true;
// The first offer is the one worth a line, and it carries the host's own numbers:
// the format in particular is the field the two ends could disagree about without
// either failing, so the value the host actually sends is put where a run on a
// device can be read afterwards.
MGLOG_I_ONCE("gbm frame channel: the first frame is offered to the container (seq %u index %u %ux%u "
"stride %u format 0x%08x)",
out.Seq, out.Index, out.Width, out.Height, out.StrideBytes, out.Format);
MGLOG_D("gbm frame channel: frame seq=%u index=%u offered (%zu bytes, %d descriptor(s), %llu "
"outstanding)",
out.Seq, out.Index, messageBytes, handle.NumFds, (unsigned long long)m_outstanding);
}
}
// The kernel took its own copies of these when the message was sent, and one that was not
// sent is not the container's to hold: every path closes what the relay handed over.
for (Int i = 0; i < fdCount; ++i) CloseQuietly(fds[i]);
return offered;
}
void GbmFrameChannel::Poll() {
const std::lock_guard<std::mutex> lock(m_mutex);
PollLocked();
}
void GbmFrameChannel::PollLocked() {
if (m_client < 0) return;
for (Int drained = 0; drained < kDrainMax; ++drained) {
GbmFrameRelease release = {};
struct iovec vector = {};
vector.iov_base = &release;
vector.iov_len = sizeof(release);
struct msghdr message = {};
message.msg_iov = &vector;
message.msg_iovlen = 1;
// recvmsg and not recv, for MSG_TRUNC: a message longer than a release can only be a
// peer speaking something else, and a release read out of the first twelve bytes of it
// would name a frame nobody released.
const ssize_t got = ::recvmsg(m_client, &message, MSG_DONTWAIT);
if (got < 0) {
if (errno == EINTR) continue;
// Nothing said, which is what a container that is between frames looks like.
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
MarkDown("recvmsg", errno);
return;
}
if (got == 0) {
// Zero bytes on a connected socket is the end of it, whether the container exited or
// the kernel gave up on it. Its replacement is taken by the next frame.
MarkDown("the container closed the connection", 0);
return;
}
if ((message.msg_flags & MSG_TRUNC) != 0 || got != (ssize_t)sizeof(release)) {
// The message is skipped, not the connection: on a message-oriented socket a
// message too long for the buffer is consumed whole, so the next one is still a
// message rather than the tail of this one.
MGLOG_W_ONCE("gbm frame channel: the container sent a message that is not a %zu byte release; it is "
"skipped",
sizeof(release));
continue;
}
if (release.Magic != kGbmFrameReleaseMagic) {
MGLOG_W_ONCE("gbm frame channel: a message from the container begins with 0x%08x rather than 'MGGR'; "
"it is skipped",
release.Magic);
continue;
}
if (release.Version != kGbmFrameVersion) {
// Refused rather than acted on: the version is what says how the rest of the message
// is laid out, and a release read with the wrong layout names a frame the container
// did not mean.
MGLOG_E_ONCE("gbm frame channel: the container speaks protocol version %u where this server speaks "
"version %u; its releases are refused until the two agree",
release.Version, (Uint32)kGbmFrameVersion);
continue;
}
++m_released;
if (m_outstanding > 0) --m_outstanding;
// The first release is the only one worth a line: it is the proof that the container
// took the descriptor and its compositor retired the frame, where every later one says
// the same thing sixty times a second.
MGLOG_I_ONCE("gbm frame channel: the container is releasing frames (first: seq %u)", release.Seq);
}
// Anything past this cap stays in the socket, whose buffer the kernel bounds, and goes out
// with the next frame: a peer that says nothing but releases may not spend the apply thread
// here.
}
void GbmFrameChannel::Close() {
// THE ACCEPTOR IS STOPPED AND JOINED FIRST, and outside the lock. It may be inside that
// lock when this is called - a teardown that waited for it while holding it would wait for
// ever - and it polls the listening descriptor, which may not be closed under it: the next
// open() in this process hands out the descriptor number a moment later, and the thread
// would then be polling somebody else's socket.
m_accepting.store(false, std::memory_order_release);
if (m_acceptor.joinable()) m_acceptor.join();
const std::lock_guard<std::mutex> lock(m_mutex);
// The frame is this channel's reference and is released with it: the backend has already
// dropped its own by the time a teardown here is a teardown of the frame as well.
ReleaseFrameLocked();
// The connection is closed quietly rather than marked down: this is a teardown, not a
// failure, and a warning about it would be noise in a log read for the failures.
CloseQuietly(m_client);
m_client = -1;
DropRelay();
if (m_listen >= 0) {
// The node is this channel's to remove only while it still IS this channel's socket: a
// server that was restarted has bound the same path with a socket of its own, and a
// teardown that unlinked it would take the successor's node away with it. st_dev and
// st_ino against the listening descriptor is the cheapest way to ask, and a check that
// fails only leaves a name behind that the next Open() unlinks anyway.
struct stat node = {};
struct stat socket = {};
if (!m_path.empty() && ::stat(m_path.c_str(), &node) == 0 && ::fstat(m_listen, &socket) == 0 &&
node.st_dev == socket.st_dev && node.st_ino == socket.st_ino) {
::unlink(m_path.c_str());
}
CloseQuietly(m_listen);
m_listen = -1;
}
m_path.clear();
// The counters are history and stay: a caller that logs Outstanding() after a teardown is
// asking what happened, and IsUp() is what answers what is connected now.
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend {
// A build that is not the Android render server has no AHardwareBuffer to offer and no
// container beside it. The class exists so that callers do not have to be compiled
// differently, and every entry point answers the way an absent container does.
GbmFrameChannel::~GbmFrameChannel() = default;
Bool GbmFrameChannel::Open() { return false; }
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer*, const HostFrameOffer&) { return false; }
void GbmFrameChannel::Poll() {}
void GbmFrameChannel::Close() {}
}
#endif
// End of Source File Header
-281
View File
@@ -1,281 +0,0 @@
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.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 "HostFrameBridge.h"
struct AHardwareBuffer;
namespace MobileGL::MG_Backend {
// THE FRAME THE HOST HANDED OVER, HANDED ON TO THE CONTAINER AS A dma-buf.
//
// The display host owns a pool of AHardwareBuffers: it allocates them, this side imports the
// one it offers as a draw target (HostFrameBridge, DirectGLES::HostFrameTarget), and the host
// puts it on the glass when this side answers that it is drawn. The Wayland compositor in the
// Linux container has to render into THOSE SAME frames - it runs this project's own GBM, and a
// gbm_bo_create there answers with the phone's frame instead of a dma-heap allocation of its
// own - so every frame this side takes has to reach the container as that frame's dma-buf
// descriptor. That, and the release that comes back, is all this channel carries.
//
// The container's end is MobileGL/MG_Gbm/GbmHostChannel.cpp. It reads exactly the bytes
// declared below and answers with a release once its compositor no longer reads the frame.
// The three shapes ARE that file's GbmFrameOffer/GbmFrameRelease and GbmInternal.h's
// AHBNativeHandleHeader, field for field and packing included. They are restated here rather
// than shared because the two ends are built into different libraries from different build
// configurations: MG_Backend is the Android render server and never sees libgbm's headers,
// while MG_Gbm is built for the container and never sees Android's. A change on either side
// is a protocol change, and the magic is what makes a mismatched pair fail loudly instead of
// being read as plausible numbers.
//
// ONE MESSAGE PER FRAME, on SOCK_SEQPACKET, and that is part of the protocol rather than an
// implementation detail: an offer and its handle only mean anything together, and the message
// boundary is what lets the reader take the two as one thing. The container derives the
// length of the message it expects from the handle's own two counts and refuses any other
// length, so this end sends the offer immediately followed by the handle's counts and its two
// arrays, with the descriptors in the SCM_RIGHTS control data of that same sendmsg.
//
// The channel is OPTIONAL, and that is the rule the implementation is built around: a
// container that is not running, has died, or has stopped reading may cost the render path a
// syscall or two, and may never block it, fail it, or grow without bound. Every entry point
// below is non-blocking and answers silence with silence.
inline constexpr Uint32 kGbmFrameOfferMagic = 0x4647474Du; // 'M','G','G','F' in memory order
inline constexpr Uint32 kGbmFrameReleaseMagic = 0x5247474Du; // 'M','G','G','R' in memory order
inline constexpr Uint32 kGbmFrameVersion = 1u;
// WHERE THE CONTAINER DIALS. /data/local/tmp is the one directory both halves of a split run
// can be pointed at on a device, and the default lives on this end because this end is the
// server. The container's compiled-in default is /run/mobilegl-gbm.sock - the same protocol
// with the path each side can reach unaided - so a deployment that puts the two in different
// mount namespaces sets MOBILEGL_GBM_SOCKET on both; it is read here and in
// GbmHostChannelCreate, from the same variable, for that reason.
inline constexpr const char* kGbmFrameSocketPath = "/data/local/tmp/mobilegl-gbm.sock";
inline constexpr const char* kGbmFrameSocketEnv = "MOBILEGL_GBM_SOCKET";
// One frame the host offered, described for the container. Width, Height, StrideBytes and
// Format are the host's own numbers for the frame (HostFrameOffer), carried through
// unchanged: they describe the memory the descriptor points at, and this end is not the place
// where a number the other two ends agreed on gets rewritten.
struct GbmFrameOffer {
Uint32 Magic;
Uint32 Version;
Uint32 Seq; // this channel's, one per frame offered
Uint32 Index; // the host's own buffer index, echoed so the phone can match a frame
Uint32 Width;
Uint32 Height;
Uint32 StrideBytes;
Uint32 Format;
Uint32 UsageLo;
Uint32 UsageHi;
} __attribute__((packed));
// What the container says once it is done with a frame. It may arrive late, it may never
// arrive, and both are cheap here: the channel keeps a count and no per-frame record (see
// Outstanding below), so nothing is leaked and nothing grows when one is missed.
struct GbmFrameRelease {
Uint32 Magic;
Uint32 Version;
Uint32 Seq;
} __attribute__((packed));
// THE OTHER DIRECTION: A dma-buf THE CONTAINER WANTS THIS SIDE TO BACK A TEXTURE WITH.
//
// The compositor's scene reaches the display daemon's dma-bufs through glEGLImageTargetTexture2DOES
// (MG_Impl/GLImpl/Exporting/Definitions.cpp, MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp both
// name this half as the next change). eglCreateImageKHR has already held a dup() of every plane's
// descriptor since the image was made; what was missing is the render server's half - telling THIS
// side which texture that dma-buf backs, over the one channel that can carry a descriptor across the
// container boundary. This message is that half: the client names the texture target and the
// texture's own name, describes the buffer exactly as drmModeAddFB2 would, and the plane descriptors
// ride in the SCM_RIGHTS control data of the same sendmsg. One message per import, as SEQPACKET
// requires, so a descriptor and its description are never read apart.
//
// The reply is what the client waits for before a framebuffer is allowed to be complete over the
// texture: Ok=0 means this side could not make the import real, and the client keeps treating the
// texture as having no storage rather than letting it render into nothing.
inline constexpr Uint32 kGbmImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
inline constexpr Uint32 kGbmImportReplyMagic = 0x52474749u; // 'I','G','G','R' in memory order
inline constexpr Uint32 kGbmImportVersion = 1u;
inline constexpr Uint32 kGbmImportMaxPlanes = 4u;
struct GbmImportRequest {
Uint32 Magic;
Uint32 Version;
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
Uint32 TexName; // the CLIENT's texture name; this side resolves its twin
Uint32 Width;
Uint32 Height;
Uint32 FourCC;
Uint32 PlaneCount; // 1..kGbmImportMaxPlanes; the descriptors are the message's control data
Int32 Stride[kGbmImportMaxPlanes];
Int32 Offset[kGbmImportMaxPlanes];
Uint64 Modifier[kGbmImportMaxPlanes];
Uint32 HasModifier[kGbmImportMaxPlanes];
} __attribute__((packed));
struct GbmImportReply {
Uint32 Magic;
Uint32 Version;
Uint32 Ok;
Uint32 Reserved;
} __attribute__((packed));
// An AHardwareBuffer native handle as it appears on the wire: the platform's native_handle_t
// (version, then the two counts, then the descriptor array followed by the integer array),
// written by AHardwareBuffer_sendHandleToUnixSocket and read by the container's
// AHBNativeHandleHeader. The counts are 32 bit on every Android ABI, which is why they are
// spelled out here rather than taken from a platform header this library cannot include.
struct GbmNativeHandleHeader {
Int32 Version;
Int32 NumFds;
Int32 NumInts;
} __attribute__((packed));
// ONE CONTAINER, ONE LISTENING SOCKET, TWO THREADS: the render path's, and an acceptor's.
//
// A compositor dials ONCE - when it creates its GBM device - and keeps that connection for the
// life of the process, while this side offers a frame per drawn frame. An accept that only ran
// inside OfferFrame therefore ran after the dial it was supposed to answer, and a dialer that
// nobody took kept the listener's queue full for every later one. That is measured on the
// phone rather than argued: with accept() only on the offer path, connect() from the container
// answered EAGAIN(11) on four attempts out of four while the node sat there listening.
//
// So the accept has a thread of its own, started by Open() and stopped by Close(), and it
// adopts what it takes on the spot - including sending it the frame the host last handed over,
// because a compositor's first allocation is the one its swapchain is built from. The
// connected socket, the frame that is remembered, the relay and the counters are shared between
// the two threads and guarded by the one mutex below; both entry points are non-blocking, so
// neither thread can park the other for longer than a syscall.
class GbmFrameChannel {
public:
GbmFrameChannel() = default;
GbmFrameChannel(const GbmFrameChannel&) = delete;
GbmFrameChannel& operator=(const GbmFrameChannel&) = delete;
~GbmFrameChannel();
// Creates the listening socket if it is not there yet and starts the acceptor, and does
// nothing otherwise. It never waits for a container: the socket is bound, a backlog is
// given to listen(), and the acceptor takes whoever dials from then on - whether or not a
// frame is being drawn. A caller that wants the container to find the socket before the
// first frame - the container dials once at its own device creation and retries about once
// a second - can call this when the host-framed surface is initialized, beside
// HostFrameBridge::Open.
Bool Open();
// Offers one frame the host just handed over, as the dma-buf descriptor behind it.
// False means the frame did not go out, for any of the reasons this channel treats as
// ordinary: no container is connected, the container just died, or the frame could not be
// described. None of them is the caller's problem and none of them is fatal - the frame
// is drawn and presented exactly as it would have been without a channel at all.
Bool OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer);
// Reads whatever the container has said since the last call and marks the channel down if
// the connection has ended. OfferFrame already does this once per frame, so a caller only
// needs it if it wants releases drained on a path that has no new frame in it.
void Poll();
// The frame the host handed over is gone - it was presented, or it was replaced - and this
// channel may no longer hold it or offer it to anybody. THE CHANNEL ITSELF STAYS OPEN, and
// that is the point of the call: the listening socket, its node and the container's
// connection outlive every frame, and a channel that was closed and reopened per frame took
// the node away sixty times a second - a container dialling at the wrong moment got ENOENT
// for a socket that was there a millisecond earlier and would be there a millisecond later.
void DropFrame();
// True while a container is connected and the frames are being offered to it.
Bool IsUp() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_client >= 0;
}
// How many frames have been offered and not released, as a count of messages and not a
// record of them. It is here to be logged, and it is deliberately the only per-frame state
// the channel keeps: a release that never arrives costs a number that is too high, and
// nothing else, which is what makes a container that stops reading harmless.
Uint64 Outstanding() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_outstanding;
}
// Closes the connection, the listening socket and the node it created. Idempotent, safe
// from a destructor, and safe to call while a container is connected: the container sees
// the connection end and goes back to allocating its frames locally, which is the state it
// starts in.
void Close();
private:
// The socketpair a frame's handle is handed to and read back from. Recreated on demand,
// because the one call the NDK offers for a buffer's descriptors sends them as a message
// of its own and the container takes one message per frame - see the note in the .cpp.
Bool EnsureRelay();
void DropRelay();
Bool RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
Int* fds, Int& fdCount);
// Takes the one container the protocol allows, and closes anyone else who dials while a
// LIVE one is connected rather than leaving them to believe they are being served. A
// connection whose peer has exited is not a live one: it is closed here and its dialer's
// replacement is taken, which is what a compositor that crashed and was restarted is.
void AdoptLocked(Int fd);
// True while the connected container is still there. recvmsg with MSG_PEEK is what asks:
// zero bytes is the peer's close, EAGAIN is a peer that is between frames, and a release
// waiting in the socket is a peer that is alive - peeked at rather than read, because the
// render path is the one that spends releases.
Bool ClientAliveLocked();
// Takes the one container the protocol allows out of whoever dialled.
void AcceptLoop(Int listen);
// Sends the frame that is the draw target to a container that has just connected.
void ReofferFrameLocked();
// Remembers the frame the host last handed over, holding a reference to it so that a
// container which dials after the offer can still be given it. The previous one is
// released here; nothing else in the channel holds a frame.
void RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq);
void ReleaseFrameLocked();
// The offer itself: the handle relayed out of the buffer, the message built, one sendmsg.
// A frame that has already been offered once is sent again with the sequence number it was
// offered under and does not move the counters, because it is the same frame and not a new
// one.
Bool SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq, Bool counting);
// The body of Poll(), for a caller that is already holding the lock.
void PollLocked();
// Closes the connection and says why, once. The channel stays open for the next container.
void MarkDown(const char* what, Int error);
Int m_listen = -1;
Int m_client = -1;
Int m_relay[2] = {-1, -1};
String m_path;
// Bumped only for a frame that actually went out, so a sequence number is never reused and
// a release can never be read as belonging to a frame of an earlier connection.
Uint32 m_nextSeq = 0;
Uint64 m_offered = 0;
Uint64 m_released = 0;
Uint64 m_outstanding = 0;
// The frame the host handed over most recently, held by this channel's own reference for as
// long as it is the draw target: it is what a container that dials late is sent.
struct AHardwareBuffer* m_frame = nullptr;
HostFrameOffer m_frameOffer = {};
Uint32 m_frameSeq = 0;
// The acceptor, and the flag that ends it. The listening descriptor is handed to the thread
// as its own copy, so it is the only thing in here that thread reads without the lock - and
// Close() joins the thread before it closes that descriptor.
std::thread m_acceptor;
std::atomic<Bool> m_accepting{false};
// Guards every member above, from both threads. Mutable because the two const questions a
// caller asks about the channel - is a container connected, how many frames are outstanding
// - are answers that have to be read under it.
mutable std::mutex m_mutex;
};
}
-189
View File
@@ -1,189 +0,0 @@
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "HostFrameBridge.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cerrno>
#include <cstring>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace MobileGL::MG_Backend {
namespace {
Bool ReadExactly(Int fd, void* out, SizeT n) {
Uint8* at = static_cast<Uint8*>(out);
while (n > 0) {
const ssize_t got = ::read(fd, at, n);
if (got <= 0) {
if (got < 0 && errno == EINTR) continue;
return false;
}
at += got;
n -= static_cast<SizeT>(got);
}
return true;
}
Bool WriteExactly(Int fd, const void* in, SizeT n) {
const Uint8* at = static_cast<const Uint8*>(in);
while (n > 0) {
const ssize_t put = ::write(fd, at, n);
if (put <= 0) {
if (put < 0 && errno == EINTR) continue;
return false;
}
at += put;
n -= static_cast<SizeT>(put);
}
return true;
}
}
HostFrameBridge::~HostFrameBridge() { Close(); }
Bool HostFrameBridge::Open(const char* path, Int acceptTimeoutMs, String& why) {
Close();
const char* name = (path != nullptr && path[0] != 0) ? path : kHostFrameSocket;
m_listen = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (m_listen < 0) {
why = std::format("socket(AF_UNIX): {}", std::strerror(errno));
return false;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
if (std::strlen(name) >= sizeof(address.sun_path)) {
why = std::format("the socket name is longer than the kernel takes: {}", name);
Close();
return false;
}
// "@name" is the abstract namespace: sun_path[0] is NUL and the name follows it.
// Nothing is created on disk, so nothing is left behind and no file permission stands
// between two apps that may not write each other's directories.
if (name[0] == '@') {
address.sun_path[0] = 0;
std::strncpy(address.sun_path + 1, name + 1, sizeof(address.sun_path) - 2);
} else {
std::strncpy(address.sun_path, name, sizeof(address.sun_path) - 1);
// A leftover socket file from a server that died is not a reason to refuse the host.
::unlink(name);
}
if (::bind(m_listen, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) {
why = std::format("bind({}): {}", name, std::strerror(errno));
Close();
return false;
}
if (::listen(m_listen, 1) != 0) {
why = std::format("listen({}): {}", name, std::strerror(errno));
Close();
return false;
}
pollfd waiting{};
waiting.fd = m_listen;
waiting.events = POLLIN;
const Int ready = ::poll(&waiting, 1, acceptTimeoutMs);
if (ready <= 0) {
why = (ready == 0) ? std::format("no display host dialled {} within {} ms", name, acceptTimeoutMs)
: std::format("poll({}): {}", name, std::strerror(errno));
Close();
return false;
}
m_fd = ::accept(m_listen, nullptr, nullptr);
if (m_fd < 0) {
why = std::format("accept({}): {}", name, std::strerror(errno));
Close();
return false;
}
MGLOG_I("host frame bridge: a display host is on %s", name);
return true;
}
Bool HostFrameBridge::Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why) {
*buffer = nullptr;
offer = HostFrameOffer{};
if (m_fd < 0) {
why = "the bridge is not open";
return false;
}
AHardwareBuffer* received = nullptr;
if (AHardwareBuffer_recvHandleFromUnixSocket(m_fd, &received) != 0 || received == nullptr) {
why = "the host stopped offering frames, or the handle did not cross";
return false;
}
if (!ReadExactly(m_fd, &offer, sizeof(offer)) || offer.Magic != kHostFrameMagic) {
// The handle is already this side's by the time this fails, so it goes back before
// the failure does: a reference held past the connection starves the host's pool.
AHardwareBuffer_release(received);
why = std::format("the frame's description did not follow its handle (magic 0x{:08x})", offer.Magic);
return false;
}
*buffer = received;
return true;
}
Bool HostFrameBridge::Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why) {
if (m_fd < 0) {
why = "the bridge is not open";
return false;
}
HostFrameAck ack{};
ack.Magic = kHostFrameMagic;
ack.Version = kHostFrameVersion;
ack.Index = offer.Index;
ack.Imported = imported ? 1u : 0u;
ack.Drawn = drawn ? 1u : 0u;
ack.FenceOk = fenceOk ? 1u : 0u;
if (!WriteExactly(m_fd, &ack, sizeof(ack))) {
why = std::format("the answer to frame {} did not go out: {}", offer.Index, std::strerror(errno));
return false;
}
// The host answers with its own view of the pixels. It is what closes the round trip, so
// it is read here and kept rather than left in the socket for the next frame to trip over.
m_lastSeen = HostFrameSeen{};
if (!ReadExactly(m_fd, &m_lastSeen, sizeof(m_lastSeen)) || m_lastSeen.Magic != kHostFrameMagic) {
why = std::format("frame {} was answered but the host's readback did not arrive", offer.Index);
m_lastSeen = HostFrameSeen{};
return false;
}
return true;
}
void HostFrameBridge::Close() {
if (m_fd >= 0) ::close(m_fd);
if (m_listen >= 0) ::close(m_listen);
m_fd = -1;
m_listen = -1;
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend {
HostFrameBridge::~HostFrameBridge() = default;
Bool HostFrameBridge::Open(const char*, Int, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameBridge::Acquire(struct AHardwareBuffer**, HostFrameOffer&, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameBridge::Complete(const HostFrameOffer&, Bool, Bool, Bool, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
void HostFrameBridge::Close() {}
}
#endif
-103
View File
@@ -1,103 +0,0 @@
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.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>
struct AHardwareBuffer;
namespace MobileGL::MG_Backend {
// THE DISPLAY HOST'S FRAMES, AS THEY ARRIVE.
//
// The Android side that owns the screen allocates its frames itself and offers each one over
// an AF_UNIX socket: the handle travels ahead of a fixed-size description on the same
// connection, and the host then waits for this side to say the frame is drawn before it puts
// it on the glass. Why the host allocates rather than this side asking for the display's own
// buffers is measured, not chosen: AHardwareBuffer_createFromHandle accepts an
// AHardwareBuffer this process allocated and refuses one the display queue handed over
// (gralloc BAD_VALUE), so the only frames a render server can draw into are the ones the host
// made for it.
//
// The three message shapes below ARE the host's (its ahb_bridge.h), field for field and
// packing included. They are restated here rather than shared because the two ends are built
// by different projects; a change on either side is a protocol change, and the magic is what
// makes a mismatched pair fail loudly instead of being read as plausible numbers.
inline constexpr Uint32 kHostFrameMagic = 0x4D474C41u; // the host spells it "ALGM" in memory
inline constexpr Uint32 kHostFrameVersion = 1u;
// AN ABSTRACT NAME, not a path: the two ends are separate apps, and neither may create a
// socket file in /data/local/tmp (its directory grants others traverse only). An abstract
// name lives in the kernel namespace instead of the filesystem, so there is no file
// permission to get wrong - the same reason this project names its own endpoints @mgl-*.
inline constexpr const char* kHostFrameSocket = "@mobilegl-host-frame";
struct HostFrameOffer {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Width;
Uint32 Height;
Uint32 StrideBytes;
Uint32 Format;
Uint32 UsageLo;
Uint32 UsageHi;
char Note[64];
} __attribute__((packed));
struct HostFrameAck {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Imported;
Uint32 Drawn;
Uint32 FenceOk;
Uint8 Color[4];
char Note[64];
} __attribute__((packed));
struct HostFrameSeen {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Locked;
Uint8 Observed[4];
Uint8 ObservedCorner[4];
char Note[64];
} __attribute__((packed));
// One host connection at a time, which is what the host offers: it dials, hands over a frame,
// waits for the answer, and hands over the next.
class HostFrameBridge {
public:
HostFrameBridge() = default;
HostFrameBridge(const HostFrameBridge&) = delete;
HostFrameBridge& operator=(const HostFrameBridge&) = delete;
~HostFrameBridge();
// Listens on `path` (the well-known name by default) and waits for the host. A host that
// never dials is answered with false and a reason, never an abort.
Bool Open(const char* path, Int acceptTimeoutMs, String& why);
// Takes the next frame. Ownership of the returned AHardwareBuffer is the caller's, who
// must hand it back through ReleaseOwned() once the frame is off the glass.
Bool Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why);
// Tells the host what became of the frame it offered, and reads back what the host's own
// view of the pixels holds. `drawn` is what the host waits for before it presents.
Bool Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why);
// The readback the host returned for the last completed frame, as the host reported it.
const HostFrameSeen& LastSeen() const { return m_lastSeen; }
void Close();
private:
Int m_fd = -1;
Int m_listen = -1;
HostFrameSeen m_lastSeen{};
};
}
-390
View File
@@ -11,29 +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_Pipe/PipeMutation.h> // MGPipeForgetAllPublications, at the end of ShutdownSplitRoles
#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) {
@@ -68,376 +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 its wire resource op table off monolith only
// (VkBufferManager::RegisterWireResourceOps, VkBufferManager.cpp): under a transport it
// owns buffer stores and claims bit 7 (the P4a mask, see ConsumedSubsystemsFor below);
// under monolith it registers none and its buffers keep the legacy BufferBackendOps path.
// 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, kCapNeedsHostUboBytes and kCapViewportArray are RESERVED
// (P8-F) and never set here: the first two are the only things that ask for an
// MGHostSpan, and 0 keeps every one of them off the wire (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();
// The server's twins died with the session; the frontend objects that are destroyed after
// this (the GL context's) have nothing left on the other side to delete.
MG_Pipe::MGPipeForgetAllPublications();
}
#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>();
-715
View File
@@ -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
-45
View File
@@ -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
+1 -47
View File
@@ -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

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