mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
299
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4aab8d0bc | ||
|
|
745f59bc3f | ||
|
|
5f9c3d6947 | ||
|
|
79afd8a8e0 | ||
|
|
b733eed45c | ||
|
|
166f47f115 | ||
|
|
96037fc502 | ||
|
|
2b277b01d4 | ||
|
|
71e442de02 | ||
|
|
fa56e1c671 | ||
|
|
2175ee08cf | ||
|
|
add15f0a17 | ||
|
|
f4f2bec711 | ||
|
|
f66d31c881 | ||
|
|
a6028ff38a | ||
|
|
3b01067a9b | ||
|
|
b5f9ab51ca | ||
|
|
f7550322d4 | ||
|
|
575be3532d | ||
|
|
0a206b0963 | ||
|
|
8ebb69d895 | ||
|
|
9a1d3062cb | ||
|
|
a759b4b72d | ||
|
|
4d3626215f | ||
|
|
e0b8053e28 | ||
|
|
a44a877080 | ||
|
|
d5ff64ded8 | ||
|
|
b48f55edfd | ||
|
|
1accc7f421 | ||
|
|
dce02ae80e | ||
|
|
e70925d35b | ||
|
|
e9201891f5 | ||
|
|
cee9178e22 | ||
|
|
0650608ddc | ||
|
|
0b536de3fb | ||
|
|
e072176c24 | ||
|
|
d4688d0037 | ||
|
|
13e8e095b4 | ||
|
|
286e4af582 | ||
|
|
4e055e4686 | ||
|
|
9f61016776 | ||
|
|
9f4d9095a9 | ||
|
|
6ffd2084fb | ||
|
|
c0ea142955 | ||
|
|
fe748773f3 | ||
|
|
1b2e817a97 | ||
|
|
c4032d2022 | ||
|
|
09ae776836 | ||
|
|
2b8c04373d | ||
|
|
fcf5ae5659 | ||
|
|
2bbb67e528 | ||
|
|
db346f2844 | ||
|
|
c85c9439a5 | ||
|
|
485dff1421 | ||
|
|
9ff97c2e7e | ||
|
|
afcc313d9f | ||
|
|
9eed5a27d7 | ||
|
|
8941eae008 | ||
|
|
533a256482 | ||
|
|
65d4b808c1 | ||
|
|
98b2c37384 | ||
|
|
d2acb75766 | ||
|
|
6933228d59 | ||
|
|
d786f1c072 | ||
|
|
79b6deacaf | ||
|
|
262c8985a9 | ||
|
|
e5ab9d4f06 | ||
|
|
129355961e | ||
|
|
6cecfcd22f | ||
|
|
16867c5c64 | ||
|
|
0b96a3efbf | ||
|
|
2fbcc22410 | ||
|
|
1771381057 | ||
|
|
2a1f1df6d1 | ||
|
|
cc5a4c0a60 | ||
|
|
7acf3ea8e1 | ||
|
|
db915f76f0 | ||
|
|
657722ff57 | ||
|
|
58a97bb7b8 | ||
|
|
e00db8e64f | ||
|
|
19a96929aa | ||
|
|
500b63ea58 | ||
|
|
c8a5abe581 | ||
|
|
49c6394dfd | ||
|
|
b4e209e12b | ||
|
|
6985e47025 | ||
|
|
3c61689dac | ||
|
|
efa415b00c | ||
|
|
4e27342e81 | ||
|
|
4a8ecf9aaf | ||
|
|
748ffc31c5 | ||
|
|
855b3b5009 | ||
|
|
4984da923f | ||
|
|
c7360b47e2 | ||
|
|
91f04ebc7c | ||
|
|
84ec06f3fc | ||
|
|
408b9fcade | ||
|
|
ddfbdab0e8 | ||
|
|
80c6aa39da | ||
|
|
92da2486c5 | ||
|
|
a0f4aa830e | ||
|
|
781a7446c4 | ||
|
|
86ab28355c | ||
|
|
e058a4507a | ||
|
|
ec973fba84 | ||
|
|
67f4f311d3 | ||
|
|
c24b3428d6 | ||
|
|
553eb86d31 | ||
|
|
785df78d77 | ||
|
|
0bd0abf652 | ||
|
|
ff53ba3b2b | ||
|
|
94705adccd | ||
|
|
81bdfb1f80 | ||
|
|
4fe2edb6fa | ||
|
|
032dcd240e | ||
|
|
c1497bb83e | ||
|
|
114bd70122 | ||
|
|
0158a115de | ||
|
|
8883e496bd | ||
|
|
0cc54748f0 | ||
|
|
80dc9d45d9 | ||
|
|
d07ec0255b | ||
|
|
4f4297f347 | ||
|
|
5788a6023e | ||
|
|
afcf8d96f9 | ||
|
|
e6281b26de | ||
|
|
eafe1179b3 | ||
|
|
4ef9705ee0 | ||
|
|
b556ec3134 | ||
|
|
0b0089e599 | ||
|
|
8346d80999 | ||
|
|
549a34bb1a | ||
|
|
2b0857b016 | ||
|
|
a9108e1a0b | ||
|
|
267211e8b4 | ||
|
|
51c1411696 | ||
|
|
d56179d876 | ||
|
|
f8a9466b93 | ||
|
|
242663c9ae | ||
|
|
4bac6255fd | ||
|
|
f4e98710ff | ||
|
|
4379ceed97 | ||
|
|
87e915382c | ||
|
|
db8484cbb9 | ||
|
|
f7360fe210 | ||
|
|
45513aef51 | ||
|
|
cd4f2db53c | ||
|
|
de6ab16756 | ||
|
|
9987e57f64 | ||
|
|
fa77b1874f | ||
|
|
556ff46601 | ||
|
|
9fa61b84a5 | ||
|
|
3fb0054e79 | ||
|
|
922507f8f8 | ||
|
|
9f88379141 | ||
|
|
a4c78fe10a | ||
|
|
5011c3d868 | ||
|
|
737ef5e980 | ||
|
|
dab827d54f | ||
|
|
ec7fc40ba5 | ||
|
|
8bc39c79d2 | ||
|
|
8abae647d0 | ||
|
|
dc1e95b4c3 | ||
|
|
887547c707 | ||
|
|
e822c933c9 | ||
|
|
3aed7ae047 | ||
|
|
e0113503af | ||
|
|
742d43aeb4 | ||
|
|
740dd301b9 | ||
|
|
d61a4240ee | ||
|
|
4c3e469ae4 | ||
|
|
c1e985c893 | ||
|
|
7fb18840dc | ||
|
|
a0b9ca0772 | ||
|
|
a37212eac9 | ||
|
|
acfee266f5 | ||
|
|
942bb6d7dd | ||
|
|
bc8595f27a | ||
|
|
6d73529f70 | ||
|
|
e0910ebd85 | ||
|
|
8efb7f492a | ||
|
|
a71c23bcf3 | ||
|
|
1cd35d3922 | ||
|
|
e9a353c324 | ||
|
|
557975bf2d | ||
|
|
aa5c12b08c | ||
|
|
7ac3ce0376 | ||
|
|
956e3847cd | ||
|
|
4ec65e801a | ||
|
|
23593c9f15 | ||
|
|
1961029043 | ||
|
|
dd7fbbd7d1 | ||
|
|
851251671c | ||
|
|
b1978890b4 | ||
|
|
ad47dc8c6f | ||
|
|
0b5e7fad82 | ||
|
|
8e76f9dcb7 | ||
|
|
ed7800a550 | ||
|
|
e04c6edb76 | ||
|
|
fe5324c900 | ||
|
|
f4be8c380f | ||
|
|
7f194a8579 | ||
|
|
bb45648c41 | ||
|
|
c285f350b1 | ||
|
|
0f5658f56e | ||
|
|
17e36589fa | ||
|
|
b13af729e4 | ||
|
|
b050c60b32 | ||
|
|
cb81610b2b | ||
|
|
1956d6cd6f | ||
|
|
c879776a26 | ||
|
|
8a2ddc8150 | ||
|
|
d00d5c9416 | ||
|
|
b26167a095 | ||
|
|
73a185f6d1 | ||
|
|
e544121eed | ||
|
|
c04347ef88 | ||
|
|
80bfc7ce02 | ||
|
|
a891d0f514 | ||
|
|
b965a18e15 | ||
|
|
22739a033c | ||
|
|
036d1ac36e | ||
|
|
2a8bb7e659 | ||
|
|
759bd8ffa5 | ||
|
|
06a4ebb645 | ||
|
|
b16dc20473 | ||
|
|
1d0f908b4a | ||
|
|
0fbcaed454 | ||
|
|
a6c02a1741 | ||
|
|
2f2a515d04 | ||
|
|
b3a8a1f47f | ||
|
|
6baa1bcdb4 | ||
|
|
462318c636 | ||
|
|
c3af0719f0 | ||
|
|
c641ed92fb | ||
|
|
3b441531b7 | ||
|
|
534f14e3f2 | ||
|
|
ec92ae4d08 | ||
|
|
5fdc1e46e2 | ||
|
|
cfd6c8730b | ||
|
|
4f5b4fa25d | ||
|
|
96c3aff78f | ||
|
|
88a11b812f | ||
|
|
f3a2fd9308 | ||
|
|
c86be0c274 | ||
|
|
2af4728e70 | ||
|
|
a7409e1e9a | ||
|
|
af84eac8ee | ||
|
|
081854653a | ||
|
|
a32ca0017d | ||
|
|
be86ca175e | ||
|
|
c0404018ec | ||
|
|
6cb6fa88f1 | ||
|
|
7643822606 | ||
|
|
75a9ea3f98 | ||
|
|
e35965d187 | ||
|
|
cef29781c5 | ||
|
|
520bafe823 | ||
|
|
3e2aff1d1d | ||
|
|
883b861490 | ||
|
|
938f4b111f | ||
|
|
3e36bd6c56 | ||
|
|
1d380a6ede | ||
|
|
4d2a244382 | ||
|
|
daf2b44c71 | ||
|
|
e2c9098648 | ||
|
|
a4d10dd9b6 | ||
|
|
640c73243d | ||
|
|
06362251b0 | ||
|
|
b9c1291f10 | ||
|
|
30ebae4346 | ||
|
|
5e547add80 | ||
|
|
a0bff7cee1 | ||
|
|
fc57a79bf1 | ||
|
|
915c87056c | ||
|
|
943aac6afb | ||
|
|
b981244672 | ||
|
|
7184a40a47 | ||
|
|
9e14c16d4c | ||
|
|
55de98a006 | ||
|
|
3e11c848e8 | ||
|
|
9f05e66591 | ||
|
|
68b56165da | ||
|
|
0a47038ae3 | ||
|
|
c6cb5dea68 | ||
|
|
5ff33cbb77 | ||
|
|
138a14019a | ||
|
|
c9085533a1 | ||
|
|
0cbb1a84d8 | ||
|
|
cde6512b08 | ||
|
|
959f518669 | ||
|
|
aee0632a82 | ||
|
|
1d1cfa6603 | ||
|
|
d409731632 | ||
|
|
039b342b97 | ||
|
|
4eee6ae251 | ||
|
|
ee9919ba39 | ||
|
|
24889447cc | ||
|
|
d8edc8baa2 |
@@ -108,7 +108,7 @@ Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any proces
|
||||
|
||||
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
|
||||
|
||||
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created and for `LIBGL_ALWAYS_SOFTWARE=1` - that is how a software Xwayland (`ANLAND_XWAYLAND_GLAMOR=0`, KWin's patch sets it then) keeps the software stack. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
|
||||
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created. It no longer steps aside for `LIBGL_ALWAYS_SOFTWARE=1` (918d40dd): that is Mesa's software-renderer selection, not a reason to disable MobileGL's entry points, so a process that sets it (a software Xwayland included) is still served by MobileGL while a server answers. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
|
||||
```sh
|
||||
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
|
||||
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Configure Test runner
|
||||
description: Select the installed compiler and graphics stack without modifying the host
|
||||
inputs:
|
||||
gpu:
|
||||
description: Require the native GPU stack when running on 8845
|
||||
default: "false"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
env:
|
||||
NEED_GPU: ${{ inputs.gpu }}
|
||||
run: |
|
||||
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ] && [ "$NEED_GPU" = true ]; then
|
||||
for tool in clang clang++ cmake ninja ccache; do command -v "$tool"; done
|
||||
test -r /usr/share/vulkan/icd.d/dzn_icd.json
|
||||
test -e /dev/dxg
|
||||
{
|
||||
echo 'MOBILEGL_CI_NATIVE_GPU=1'
|
||||
echo 'MOBILEGL_CI_CC=clang'
|
||||
echo 'MOBILEGL_CI_CXX=clang++'
|
||||
echo 'MOBILEGL_CI_LLD=ld.lld'
|
||||
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=OFF'
|
||||
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/dzn_icd.json'
|
||||
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json'
|
||||
echo 'EGL_PLATFORM=surfaceless'
|
||||
echo 'LIBGL_ALWAYS_SOFTWARE=0'
|
||||
echo 'GALLIUM_DRIVER=d3d12'
|
||||
echo 'MESA_LOADER_DRIVER_OVERRIDE=d3d12'
|
||||
} >> "$GITHUB_ENV"
|
||||
else
|
||||
{
|
||||
echo 'MOBILEGL_CI_NATIVE_GPU=0'
|
||||
echo 'MOBILEGL_CI_CC=clang-20'
|
||||
echo 'MOBILEGL_CI_CXX=clang++-20'
|
||||
echo 'MOBILEGL_CI_LLD=ld.lld-20'
|
||||
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=ON'
|
||||
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json'
|
||||
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json'
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
@@ -46,7 +46,7 @@ require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
@@ -76,12 +76,25 @@ jobs:
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
timeout-minutes: 45
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ccache
|
||||
ccache --version
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup84
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup84.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -92,8 +105,13 @@ jobs:
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
|
||||
- name: Install Android NDK
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
sdkmanager "ndk;27.3.13750724"
|
||||
for attempt in 1 2 3; do
|
||||
sdkmanager "ndk;27.3.13750724" && break
|
||||
[ "$attempt" = 3 ] && exit 1
|
||||
echo "sdkmanager attempt $attempt failed; retrying"; sleep 20
|
||||
done
|
||||
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
|
||||
|
||||
- name: Update glslang external sources
|
||||
@@ -106,7 +124,6 @@ jobs:
|
||||
-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)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
@@ -155,7 +172,6 @@ jobs:
|
||||
-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)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
@@ -336,6 +352,18 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup336
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup336.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -415,6 +443,18 @@ jobs:
|
||||
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Android SDK
|
||||
id: netsetup415
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Setup Android SDK (retry)
|
||||
timeout-minutes: 15
|
||||
if: ${{ steps.netsetup415.outcome == 'failure' }}
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
@@ -492,10 +532,11 @@ jobs:
|
||||
# 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
|
||||
# value would override that. --use-pbuffer is NOT optional here: a spawned
|
||||
# server cannot be handed the client's window, 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).
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow:use-MOBILEGL_IPC_SURFACE=server"} (Rule H, permanent;
|
||||
# a windowed client uses MOBILEGL_IPC_SURFACE=server instead).
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
|
||||
--use-pbuffer
|
||||
@@ -735,7 +776,8 @@ jobs:
|
||||
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" \
|
||||
# 50 rows per page: the jobs endpoint answers HTTP 502 for 90+ rows on a re-run attempt of a run this size.
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=50" \
|
||||
--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" \
|
||||
|
||||
+790
-502
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
name: Arch WSL GPU smoke
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- ci/arch-wsl-gpu-runner
|
||||
paths:
|
||||
- .github/workflows/wsl-gpu-smoke.yml
|
||||
- docs/ci/arch-wsl-gpu-runner.md
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: arch-wsl-gpu-smoke
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
gpu-smoke:
|
||||
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Report runner identity
|
||||
shell: bash
|
||||
run: |
|
||||
printf 'Runner: %s\n' "$RUNNER_NAME"
|
||||
uname -n
|
||||
uname -a
|
||||
- name: Verify hardware GLES and Vulkan
|
||||
shell: bash
|
||||
run: /usr/local/libexec/mobilegl-runner/gpu-smoke
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: c8036190fc...1f1100e320
+249
-135
@@ -31,18 +31,25 @@ option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process,
|
||||
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)
|
||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2) is finished: since P13 every build pushes, and
|
||||
# there is neither an option nor a macro for it (W3a removed the option, W3b the `#if` guards and
|
||||
# the definition). A -DMOBILEGL_PIPE_PUSH=OFF (or the OFF that every pre-P13 build tree holds in its
|
||||
# cache, the old default) is named and dropped rather than honoured: a warning, not an error, so an
|
||||
# existing tree reconfigures. The RUNTIME MOBILEGL_PIPE_PUSH environment bitmask is unrelated.
|
||||
if (DEFINED CACHE{MOBILEGL_PIPE_PUSH} AND NOT "$CACHE{MOBILEGL_PIPE_PUSH}")
|
||||
message(WARNING "MobileGL: MOBILEGL_PIPE_PUSH=OFF asks for the pull build, which P13 retired; "
|
||||
"ignored - every build pushes now")
|
||||
endif()
|
||||
unset(MOBILEGL_PIPE_PUSH CACHE)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6) - MOBILEGL_PIPE_LEGACY_MEMOS - retired with the
|
||||
# pull path in P13 W3b: the pre-handle memo arm is no longer compiled anywhere. Named and dropped,
|
||||
# like MOBILEGL_PIPE_PUSH=OFF above (every pre-P13 tree holds the old default ON in its cache).
|
||||
if (DEFINED CACHE{MOBILEGL_PIPE_LEGACY_MEMOS} AND "$CACHE{MOBILEGL_PIPE_LEGACY_MEMOS}")
|
||||
message(WARNING "MobileGL: MOBILEGL_PIPE_LEGACY_MEMOS=ON asks for the pre-handle memo arm, "
|
||||
"which P13 retired; ignored")
|
||||
endif()
|
||||
unset(MOBILEGL_PIPE_LEGACY_MEMOS CACHE)
|
||||
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")
|
||||
|
||||
@@ -111,12 +118,15 @@ if (ANDROID)
|
||||
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
|
||||
"check that minSdk instead of adding an override).")
|
||||
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
|
||||
# A higher level is a separate artifact of the SAME source (gradle -Pmobilegl.androidApi=29:
|
||||
# native ELF TLS instead of emulated TLS). Any post-${MOBILEGL_ANDROID_API_LEVEL} API use must
|
||||
# sit behind a compile-time __ANDROID_API__ gate and never replace the API
|
||||
# ${MOBILEGL_ANDROID_API_LEVEL} path; the minSdk-${MOBILEGL_ANDROID_API_LEVEL} build stays the
|
||||
# enforcing compile.
|
||||
message(STATUS
|
||||
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
|
||||
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
|
||||
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
|
||||
"compile.")
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}): a separate artifact; "
|
||||
"post-${MOBILEGL_ANDROID_API_LEVEL} APIs only behind a compile-time __ANDROID_API__ gate.")
|
||||
endif()
|
||||
|
||||
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
|
||||
@@ -126,13 +136,14 @@ if (ANDROID)
|
||||
unset(_mobilegl_api_triple)
|
||||
endif()
|
||||
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO and the release optimization flags" OFF)
|
||||
|
||||
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||
# Check if ThinLTO or LTO is suppported
|
||||
include(CheckIPOSupported)
|
||||
include(CheckCCompilerFlag)
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(CheckLinkerFlag)
|
||||
|
||||
check_ipo_supported(RESULT LTOSupported OUTPUT LTOError)
|
||||
|
||||
@@ -157,11 +168,49 @@ if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MO
|
||||
message(STATUS "IPO not supported: ${LTOError}")
|
||||
endif()
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MATCHES "AppleClang")
|
||||
add_compile_options(-O3 -ffunction-sections -fdata-sections)
|
||||
add_link_options(-Wl,--gc-sections)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
# add_compile_options(/O2)
|
||||
# Every flag below is probed first, so a compiler or linker without it simply goes without.
|
||||
# add_compile_options/add_link_options set directory properties, so they act from a function.
|
||||
function(mobilegl_add_compile_flag_if_supported flag)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_C${flag}" c_var)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_CXX${flag}" cxx_var)
|
||||
check_c_compiler_flag("${flag}" ${c_var})
|
||||
check_cxx_compiler_flag("${flag}" ${cxx_var})
|
||||
if (${c_var} AND ${cxx_var})
|
||||
add_compile_options("${flag}")
|
||||
endif()
|
||||
endfunction()
|
||||
function(mobilegl_add_link_flag_if_supported flag)
|
||||
string(MAKE_C_IDENTIFIER "MGL_HAS_LINK${flag}" link_var)
|
||||
check_linker_flag(CXX "${flag}" ${link_var})
|
||||
if (${link_var})
|
||||
add_link_options("${flag}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (MSVC)
|
||||
# One COMDAT per function (/Gy) and per datum (/Gw), so /OPT:REF can drop the unreferenced
|
||||
# ones and /OPT:ICF fold the identical ones - the sections/gc/icf trio below, MSVC-spelled.
|
||||
add_compile_options(/Gy /Gw)
|
||||
add_link_options(/OPT:REF /OPT:ICF)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_compile_options(-O3)
|
||||
mobilegl_add_compile_flag_if_supported(-ffunction-sections)
|
||||
mobilegl_add_compile_flag_if_supported(-fdata-sections)
|
||||
# Hidden visibility and -Bsymbolic-functions already keep the library's calls inside it;
|
||||
# this lets the optimizer rely on that and inline across what it would otherwise have to
|
||||
# treat as interposable definitions.
|
||||
mobilegl_add_compile_flag_if_supported(-fno-semantic-interposition)
|
||||
if (APPLE)
|
||||
mobilegl_add_link_flag_if_supported(LINKER:-dead_strip)
|
||||
elseif (NOT WIN32)
|
||||
mobilegl_add_link_flag_if_supported(LINKER:--gc-sections)
|
||||
# Calls into libc and the GPU driver load their target from the GOT instead of going
|
||||
# through a PLT stub. Bionic binds everything at load time anyway.
|
||||
mobilegl_add_compile_flag_if_supported(-fno-plt)
|
||||
# Identical code folding (lld, gold). Safe mode never folds a function whose address
|
||||
# is taken, so function-pointer identity holds.
|
||||
mobilegl_add_link_flag_if_supported(LINKER:--icf=safe)
|
||||
endif()
|
||||
else ()
|
||||
add_compile_options(-O2)
|
||||
endif()
|
||||
@@ -421,6 +470,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ShaderObjectsARB.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
@@ -545,63 +595,34 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
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()
|
||||
# P13 W5 (notes/p13/W5-DESIGN.md): the record arm is part of every library - the one without
|
||||
# MG_Remote (FCL's) runs it too. Its temporary guard, MOBILEGL_BUILD_RECORD_ARM, was folded away at
|
||||
# S8 (scripts/ci/retired_switches.py keeps it from coming back).
|
||||
|
||||
# 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()
|
||||
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
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
@@ -670,7 +691,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# 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
|
||||
# P13 W5: the adoption tier's verdict (the tracker itself moved to the record arm below).
|
||||
MobileGL/MG_Remote/Client/AdoptTierChoice.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
@@ -698,14 +720,25 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/Server/ServerDisplay.cpp
|
||||
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
|
||||
MobileGL/MG_Remote/Server/EmbeddedServer.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()
|
||||
|
||||
# P13 W5: the record arm's own sources, compiled into every library (MG_Remote or not).
|
||||
list(APPEND SOURCE_FILES
|
||||
# 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.
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
# P5 b1: the block-granularity persistent-map push the record arm makes mandatory (T2).
|
||||
MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp
|
||||
# P13 W5: the verb emitters and the monolith verb port, and the client GPU-write set
|
||||
# they mark before a draw or a dispatch (MG_Record; EmitTables.cpp resolves the session).
|
||||
MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp
|
||||
MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp
|
||||
# The record verbs' consumer (ServerVerbSink under a transport is this plus the session's).
|
||||
MobileGL/MG_Backend/Record/RecordVerbSink.cpp
|
||||
)
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -768,15 +801,9 @@ 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}")
|
||||
|
||||
@@ -802,13 +829,87 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# 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()
|
||||
# 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. Every build compiles the two GlvndVendor.cpp
|
||||
# files, so the path is every build's (P13 W5: the Android library without MG_Remote - FCL's - did not
|
||||
# find <glvnd/libeglabi.h>; a Linux host only built because its system has the header).
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
|
||||
|
||||
# ---- P13 W7 (D12): MODULE TARGETS ------------------------------------------------------------
|
||||
#
|
||||
# One flat SOURCE_FILES list used to feed MobileGL and MobileGL_s separately (every file compiled
|
||||
# twice). It is split here into OBJECT libraries along the link ratchet's partition
|
||||
# (scripts/link_ratchet.py PARTITION, which is the written form of these boundaries):
|
||||
#
|
||||
# mg_util MG_Util, Init / GlobalObjects / ConfigLoader, MG_Remote/FatalFunnel SHARED
|
||||
# mg_pipe MG_Pipe SERVER
|
||||
# mg_backend MG_Backend (both backends, the record arm's staged stores) SERVER
|
||||
# mg_frontend MG_Impl + MG_State FRONTEND
|
||||
# mg_remote_transport MG_Remote/{Transport,Protocol,Wire}, CapsCodec SERVER
|
||||
# mg_remote_client MG_Remote/Client FRONTEND
|
||||
# mg_remote_server MG_Remote/Server SERVER
|
||||
#
|
||||
# Both MobileGL and MobileGL_s link the same objects. mgl_build_iface carries every compile-side
|
||||
# usage requirement (include directories, definitions, the link libraries whose headers the
|
||||
# sources include), so a property set on it reaches every module and both libraries; a property
|
||||
# that only matters at link time may still be set on the libraries themselves.
|
||||
add_library(mgl_build_iface INTERFACE)
|
||||
target_include_directories(mgl_build_iface INTERFACE ${MOBILEGL_INCLUDE_DIR})
|
||||
target_link_libraries(mgl_build_iface INTERFACE ${MOBILEGL_LINK_LIBRARIES})
|
||||
target_compile_definitions(mgl_build_iface INTERFACE
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||
)
|
||||
|
||||
set(MGL_MODULES util pipe backend frontend remote_transport remote_client remote_server)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
set(MGL_MODULE_SOURCES_${MGL_MODULE})
|
||||
endforeach()
|
||||
foreach(MGL_SOURCE IN LISTS SOURCE_FILES)
|
||||
if (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Client/")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_client ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Server/")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_server ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/(Transport|Protocol|Wire)/" OR
|
||||
MGL_SOURCE STREQUAL "MobileGL/MG_Remote/CapsCodec.cpp")
|
||||
list(APPEND MGL_MODULE_SOURCES_remote_transport ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_(Impl|State)/")
|
||||
list(APPEND MGL_MODULE_SOURCES_frontend ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Backend/")
|
||||
list(APPEND MGL_MODULE_SOURCES_backend ${MGL_SOURCE})
|
||||
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Pipe/")
|
||||
list(APPEND MGL_MODULE_SOURCES_pipe ${MGL_SOURCE})
|
||||
else()
|
||||
list(APPEND MGL_MODULE_SOURCES_util ${MGL_SOURCE})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(MGL_MODULE_OBJECTS)
|
||||
set(MGL_SERVER_MODULE_OBJECTS)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
if (NOT MGL_MODULE_SOURCES_${MGL_MODULE})
|
||||
continue() # MG_Remote's modules are empty without MOBILEGL_BUILD_DISAGGREGATED
|
||||
endif()
|
||||
add_library(mg_${MGL_MODULE} OBJECT ${MGL_MODULE_SOURCES_${MGL_MODULE}})
|
||||
target_link_libraries(mg_${MGL_MODULE} PUBLIC mgl_build_iface)
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES
|
||||
C_VISIBILITY_PRESET default CXX_VISIBILITY_PRESET default VISIBILITY_INLINES_HIDDEN OFF)
|
||||
else()
|
||||
set_target_properties(mg_${MGL_MODULE} PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)
|
||||
endif()
|
||||
list(APPEND MGL_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
if (NOT MGL_MODULE STREQUAL "frontend" AND NOT MGL_MODULE STREQUAL "remote_client")
|
||||
list(APPEND MGL_SERVER_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
${MGL_MODULE_OBJECTS}
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
@@ -833,21 +934,7 @@ else()
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||
)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC mgl_build_iface)
|
||||
|
||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
foreach(MOBILEGL_LOADER_ALIAS
|
||||
@@ -874,8 +961,8 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(NOT ANDROID)
|
||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||
${SOURCE_FILES}
|
||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||
${MGL_MODULE_OBJECTS}
|
||||
)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -892,27 +979,12 @@ if(NOT ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s
|
||||
PUBLIC
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC mgl_build_iface)
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC -DTRACY_ENABLE)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
|
||||
target_link_libraries(mgl_build_iface INTERFACE Tracy::TracyClient)
|
||||
target_compile_definitions(mgl_build_iface INTERFACE -DTRACY_ENABLE)
|
||||
endif ()
|
||||
|
||||
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
|
||||
@@ -937,6 +1009,51 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
add_subdirectory(MobileGL/MG_Gbm)
|
||||
endif()
|
||||
|
||||
# ---- P13 W7 (D12): THE SERVER-ONLY LINK CHECK ---------------------------------------------------
|
||||
# The server and shared modules linked WITHOUT mg_frontend / mg_remote_client, with --no-undefined:
|
||||
# the server half has to link from its own half of the tree. The frontend references it still makes
|
||||
# are the link ratchet's baseline, each a named exemption that resolves to an aborting stub through
|
||||
# a generated linker response file (scripts/ci/server_link_check.py refuses, before the link, any
|
||||
# server reference the baseline does not name). Built with the split shape on Linux hosts; never
|
||||
# installed or loaded. NOT in a verify build: the G4 comparator reads both halves by design (its
|
||||
# MGPipeVerifyReadHook and the program-artifact codec are frontend code the server half calls), the
|
||||
# baseline is the shipped split shape's, and a verify build never ships.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_VERIFY AND UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if (Python3_Interpreter_FOUND)
|
||||
set(MGL_SERVER_LINK_RSP "${CMAKE_BINARY_DIR}/mgl_server_link_exemptions.rsp")
|
||||
set(MGL_ALL_MODULE_OBJECT_DEPS)
|
||||
foreach(MGL_MODULE IN LISTS MGL_MODULES)
|
||||
if (TARGET mg_${MGL_MODULE})
|
||||
list(APPEND MGL_ALL_MODULE_OBJECT_DEPS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
|
||||
endif()
|
||||
endforeach()
|
||||
add_custom_command(
|
||||
OUTPUT "${MGL_SERVER_LINK_RSP}"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
|
||||
--build-dir "${CMAKE_BINARY_DIR}"
|
||||
--baseline "${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
|
||||
--out "${MGL_SERVER_LINK_RSP}"
|
||||
DEPENDS ${MGL_ALL_MODULE_OBJECT_DEPS}
|
||||
"${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
|
||||
"${CMAKE_SOURCE_DIR}/scripts/link_ratchet.py"
|
||||
"${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
|
||||
COMMENT "Server-only link check: naming the frontend references the baseline exempts"
|
||||
VERBATIM)
|
||||
add_custom_target(mgl_server_link_exemptions DEPENDS "${MGL_SERVER_LINK_RSP}")
|
||||
add_library(MobileGL_server_linkcheck SHARED
|
||||
${MGL_SERVER_MODULE_OBJECTS}
|
||||
tools/server_link_check/Exempt.cpp)
|
||||
target_link_libraries(MobileGL_server_linkcheck PRIVATE mgl_build_iface)
|
||||
target_link_options(MobileGL_server_linkcheck PRIVATE
|
||||
"LINKER:--no-undefined" "@${MGL_SERVER_LINK_RSP}")
|
||||
set_property(TARGET MobileGL_server_linkcheck APPEND PROPERTY LINK_DEPENDS "${MGL_SERVER_LINK_RSP}")
|
||||
add_dependencies(MobileGL_server_linkcheck mgl_server_link_exemptions)
|
||||
else()
|
||||
message(STATUS "MobileGL: no Python3 interpreter - the server-only link check target is not configured")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
android
|
||||
@@ -977,7 +1094,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
||||
endif()
|
||||
|
||||
if (APPLE AND MOBILEGL_IOS)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_compile_definitions(mgl_build_iface INTERFACE MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
"-framework CoreGraphics"
|
||||
"-framework Foundation"
|
||||
@@ -988,7 +1105,6 @@ if (APPLE AND MOBILEGL_IOS)
|
||||
endif()
|
||||
|
||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||
"-framework CoreGraphics"
|
||||
"-framework Foundation"
|
||||
@@ -1003,10 +1119,8 @@ endif()
|
||||
if (NOT ANDROID AND NOT MOBILEGL_IOS)
|
||||
find_package(Vulkan)
|
||||
if (Vulkan_FOUND)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Vulkan::Vulkan Vulkan::Headers)
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
|
||||
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR})
|
||||
target_link_libraries(mgl_build_iface INTERFACE Vulkan::Vulkan Vulkan::Headers)
|
||||
target_include_directories(mgl_build_iface INTERFACE ${Vulkan_INCLUDE_DIR})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
|
||||
+113
-56
@@ -120,6 +120,37 @@ namespace MobileGL::MG_Config {
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the
|
||||
// attachments of the draw pass for the bound framebuffer is recorded inside that pass
|
||||
// (vkCmdClearAttachments) rather than in a render pass of its own built for the clear.
|
||||
// Auto is ON; ForceOff restores the standalone clear for every surface.
|
||||
QuirkOverride MagmaInPassClear = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT: the same for the default framebuffer. Auto is OFF
|
||||
// (an unexplained openra regression on Adreno 750, perf-p15/HANDOFF.md); ForceOn enables it.
|
||||
QuirkOverride MagmaInPassClearDefault = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PASS_CACHE: wire draw passes (render pass, framebuffer, views) are kept
|
||||
// across frames while their key's image epochs hold. Auto is ON; ForceOff drops every
|
||||
// cached pass whose last use completed at each frame begin, as before P15.
|
||||
QuirkOverride MagmaPassCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE: a wire vertex stream that needs a CPU format
|
||||
// conversion is converted once per frame slot per (buffer content, layout) and reused by
|
||||
// every draw that names it, instead of once per draw. Auto is ON; ForceOff converts per draw.
|
||||
QuirkOverride MagmaVertexConversionCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_WRITE_RENAME: a glBufferSubData into a small wire store the GPU may still
|
||||
// be reading gives the store a fresh buffer filled from a host shadow, instead of a staged
|
||||
// copy that ends the render pass. Auto is ON; ForceOff keeps the staged copy.
|
||||
QuirkOverride MagmaWriteRename = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_VAO_SHADOW: DirectGLES emits a vertex attribute's enable flag, divisor and
|
||||
// pointer to the driver VAO only when they differ from what that VAO already holds (a
|
||||
// shadow on the VAO twin). Auto is ON; ForceOff re-emits the whole configuration per walk.
|
||||
QuirkOverride EsprytVaoShadow = QuirkOverride::Auto;
|
||||
// MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP: a VAO configuration change whose vertex-elements
|
||||
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
|
||||
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
|
||||
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
|
||||
// P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it
|
||||
// reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState).
|
||||
QuirkOverride PipeFramebufferStateMemo = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
@@ -135,8 +166,22 @@ namespace MobileGL::MG_Config {
|
||||
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
|
||||
// into device loss. For diagnosing a driver that copes with a specific module only.
|
||||
Bool MagmaAllowInvalidSpirv = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_FRAMES_IN_FLIGHT (P15; MOBILEGL_MAGMA_FRAMESINFLIGHT is its deprecated alias):
|
||||
// how many frames may be queued ahead of the GPU, default 3, both backends.
|
||||
// - Magma: its frame contexts (frame S+1 reuses S+1-N's), clamped to the surface.
|
||||
// - Espryt: after presenting S the backend waits for S+1-N to complete (N = 1 is a full
|
||||
// CPU/GPU sync; N >= 2 is what Android's own queueBuffer throttle already gives).
|
||||
// - inproc / spawn / TCP: the present credit, how many presents the GL thread may run
|
||||
// ahead of the server, is derived from it: max(1, N - 2), so the default 3 keeps the
|
||||
// shipped credit of 1 and N = 4 is credit 2 (one more frame of latency).
|
||||
Uint32 FramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES: swapchain images Magma asks for beyond the
|
||||
// surface's minimum (the frames-in-flight count still raises the request when it is
|
||||
// larger), clamped to the surface's maximum. With more images, the one the display has
|
||||
// just released is reused later, by when its release has usually signaled, so fewer
|
||||
// frames start their window pass behind it. Each image costs a full-size colour buffer
|
||||
// plus the default framebuffer's depth/stencil buffer of the same size.
|
||||
Uint32 MagmaExtraSwapchainImages = 0;
|
||||
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
|
||||
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
|
||||
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
|
||||
@@ -370,6 +415,10 @@ namespace MobileGL::MG_Config {
|
||||
// 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.
|
||||
// P13 W3b (ID-P13-3): bits 0-13 are FIXED ON. Their pre-handle arms are gone, so a mask
|
||||
// that clears one is refused by name at startup (Fatal{PipeSubsystemsFixedOn},
|
||||
// ConfigLoader.cpp); bit 63 is the one switch left. Everything above about "refused,
|
||||
// runs its legacy arm" describes code that is unreachable since then and goes in W6.
|
||||
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
|
||||
@@ -377,7 +426,6 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
@@ -396,31 +444,9 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
@@ -469,6 +495,42 @@ namespace MobileGL::MG_Config {
|
||||
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||
};
|
||||
|
||||
// P13 W5: THE RECORD ARM'S OWN KNOBS. They kept their MOBILEGL_IPC_* names (logs, CI lanes and
|
||||
// notes spell them so), but they tune the record arm - the persistent-map push and Magma's wire
|
||||
// stores - which runs in a library without a transport too, so they are not IpcTable's.
|
||||
struct RecordArmTable {
|
||||
// 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_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_PIPE_PERSISTENT_MPROTECT (P13 W5): 1 (default) = the persistent-map push finds
|
||||
// written pages through mprotect and a chained SIGSEGV handler where the platform has
|
||||
// one (Linux, Android); 0 = no handler is ever installed and every persistent write map
|
||||
// takes the hash scan. The switch exists for the hosts whose own SIGSEGV use the
|
||||
// chained handler has not met yet - FCL is a JVM, whose implicit null checks fault on
|
||||
// purpose - so a bring-up can rule the handler out without a rebuild.
|
||||
Uint32 PersistentMprotect = 1;
|
||||
};
|
||||
extern RecordArmTable RecordArm;
|
||||
|
||||
#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
|
||||
@@ -527,20 +589,6 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
@@ -558,14 +606,6 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
@@ -677,14 +717,19 @@ namespace MobileGL::MG_Config {
|
||||
// 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".
|
||||
// The 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. Derived from MOBILEGL_FRAMES_IN_FLIGHT (max(1, N - 2)); the
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT override stays for experiments. Credit 2 is a real trade,
|
||||
// not a measurement arm: on FCL inproc (cpuhunt, 2026-10-08) it brought Magma 309 -> 320
|
||||
// and Espryt 299 -> 319 fps, at one frame of added latency.
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_WAIT_STATS (P15, diagnostic): every 600 presents, log how many times per
|
||||
// frame the run-ahead client waited, by record op (applied/reply), by quiesce reason and
|
||||
// barriered verb, for ring space and for the present credit. Off by default.
|
||||
Bool WaitStats = false;
|
||||
// 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
|
||||
@@ -713,8 +758,9 @@ namespace MobileGL::MG_Config {
|
||||
// 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;
|
||||
// without it (P5F §6). P15: the default is ON (unset = on, 0 = off); the negative control
|
||||
// sets 0 explicitly.
|
||||
Bool RoleSplitState = true;
|
||||
// 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.
|
||||
@@ -742,10 +788,21 @@ namespace MobileGL::MG_Config {
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||
#endif
|
||||
|
||||
// P13: THE DATA-ARM QUESTION, ANSWERED ONCE FOR EVERY BUILD. The backends read RECORDS (the
|
||||
// applier's twins, the staged stores, the verb's own handles), never frontend objects - over a
|
||||
// wire and on monolith alike ("monolith is the push variant"). W4 switched monolith family by
|
||||
// family behind MOBILEGL_PIPE_DATA_ARM; W5 built the record arm into every library; W6 deleted
|
||||
// the frontend arm and the knob. The predicate stays as a name for the sites that read it.
|
||||
inline constexpr Bool DataArmIsRecord() { return true; }
|
||||
// ...and the record arm runs in this process over stores that ALIAS the frontend's shadows
|
||||
// (monolith) instead of copying them (every wire).
|
||||
inline Bool RecordArmAliasesFrontend() { return Transport == TransportMode::Monolith; }
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+106
-36
@@ -10,12 +10,10 @@
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
#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 <cstdio>
|
||||
@@ -41,6 +39,7 @@ namespace MobileGL::MG_Config {
|
||||
Bool SplitTransportRequestedByConfig = false;
|
||||
IpcTable Ipc;
|
||||
#endif
|
||||
RecordArmTable RecordArm;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
namespace MobileGL::MG_ConfigLoader {
|
||||
@@ -143,6 +142,28 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
if (IsServerProcess()) return;
|
||||
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
|
||||
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
|
||||
#if defined(__ANDROID__)
|
||||
// An app that hosts the library (FCL) owns its process environment, so a developer
|
||||
// has no way to set a MOBILEGL_* knob in it. `adb shell setprop debug.mobilegl.env
|
||||
// "MOBILEGL_A=1;MOBILEGL_B=0"` does it, below the environment like the Linux config
|
||||
// file (PROP_VALUE_MAX bounds it to a few keys).
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (__system_property_get("debug.mobilegl.env", property) > 0) {
|
||||
String all(property);
|
||||
SizeT start = 0;
|
||||
while (start < all.size()) {
|
||||
SizeT end = all.find(';', start);
|
||||
if (end == String::npos) end = all.size();
|
||||
const String item = all.substr(start, end - start);
|
||||
const SizeT eq = item.find('=');
|
||||
if (eq != String::npos && eq > 0) {
|
||||
out.emplace(item.substr(0, eq), item.substr(eq + 1));
|
||||
if (log) MGLOG_I("Config: debug.mobilegl.env sets %s", item.c_str());
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -346,10 +367,26 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR");
|
||||
features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT");
|
||||
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
|
||||
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
|
||||
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
|
||||
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
|
||||
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
|
||||
features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
// P15: one backend-neutral knob; the Magma-only spelling stays as a deprecated alias.
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_FRAMES_IN_FLIGHT") != 0) {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_FRAMES_IN_FLIGHT", 3, 1, 64);
|
||||
} else {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_MAGMA_FRAMESINFLIGHT") != 0)
|
||||
MGLOG_W("Config: MOBILEGL_MAGMA_FRAMESINFLIGHT is deprecated; use MOBILEGL_FRAMES_IN_FLIGHT");
|
||||
}
|
||||
features.MagmaExtraSwapchainImages = QueryEnvUint32("MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES", 0, 0, 16);
|
||||
features.MagmaMaxDrawsPerCommandBuffer =
|
||||
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
|
||||
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
|
||||
@@ -386,34 +423,34 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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
|
||||
// P13 W3b (ID-P13-3): bits 0-13 are fixed on. The arms a cleared bit used to select
|
||||
// (the pre-handle memos, the pull build's frontend reads) are gone, so a cleared bit has
|
||||
// nothing to run; it is refused here, by name, before any backend is brought up, rather
|
||||
// than discovered as an armless subsystem at its first draw. Bit 63 (CSO content
|
||||
// addressing off) stays a switch.
|
||||
if ((features.PipePush & MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) !=
|
||||
MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) {
|
||||
MGLOG_F("MGPipe: Fatal{PipeSubsystemsFixedOn, \"MOBILEGL_PIPE_PUSH=0x%llx clears subsystem "
|
||||
"bit(s) 0x%llx; bits 0-13 are fixed on since P13 - only bit 63 may be set or "
|
||||
"cleared\"}",
|
||||
static_cast<unsigned long long>(features.PipePush),
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP5e &
|
||||
~features.PipePush));
|
||||
std::abort();
|
||||
}
|
||||
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, "");
|
||||
@@ -459,6 +496,15 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitTransport() {
|
||||
String value;
|
||||
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||
#if defined(__ANDROID__)
|
||||
// As debug.mobilegl.backend above: a developer picks the transport of an app-hosted
|
||||
// library (a launcher's game process) without rebuilding it or its launcher.
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (__system_property_get("debug.mobilegl.transport", property) > 0) {
|
||||
MGLOG_I("Config: debug.mobilegl.transport=%s overrides MOBILEGL_TRANSPORT=%s", property, value.c_str());
|
||||
value = property;
|
||||
}
|
||||
#endif
|
||||
String lowered = value;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
@@ -478,7 +524,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"ring to an apply thread");
|
||||
return;
|
||||
}
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "rejected"
|
||||
// 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
|
||||
@@ -502,9 +548,10 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
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.
|
||||
// unix: and pipe: are NOT MOBILEGL_TRANSPORT values and are rejected here. Connecting
|
||||
// to a server somebody else is listening on is `spawn` plus
|
||||
// MOBILEGL_IPC_CONTROL=unix:<path>|unix:@<name>|tcp://host:port (InitIpc); there is
|
||||
// no pipe: form. 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());
|
||||
@@ -531,20 +578,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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
|
||||
@@ -583,19 +621,31 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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);
|
||||
// P15: derived from MOBILEGL_FRAMES_IN_FLIGHT (Config.h states the mapping); the explicit
|
||||
// knob stays as an override for experiments.
|
||||
{
|
||||
const Uint32 derived = std::max<Uint32>(1u, std::min<Uint32>(8u, MG_Config::Features.FramesInFlight > 2
|
||||
? MG_Config::Features.FramesInFlight - 2
|
||||
: 1u));
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", derived, 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.WaitStats = QueryEnvFlag("MOBILEGL_IPC_WAIT_STATS");
|
||||
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");
|
||||
// P15: ON BY DEFAULT. Every split lane and the on-device retrace gate already ran with it,
|
||||
// and the dual-block census is empty (no apply reads a client-filled field), so the
|
||||
// shipped arm now matches what is tested - and with two blocks the client's fill is its
|
||||
// own and needs no applier quiesce. 0 still turns it off (the negative control does).
|
||||
ipc.RoleSplitState = QueryEnvQuirkOverride("MOBILEGL_IPC_ROLE_SPLIT_STATE") != MG_Config::QuirkOverride::ForceOff;
|
||||
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); "
|
||||
@@ -668,8 +718,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"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.RingMb, ipc.StageMb, MG_Config::RecordArm.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
MG_Config::RecordArm.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,
|
||||
@@ -681,6 +731,26 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"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");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// P13 W5: the record arm's knobs (Config.h RecordArmTable), in every build that has the arm.
|
||||
inline void InitRecordArm() {
|
||||
MG_Config::RecordArmTable& ipc = MG_Config::RecordArm;
|
||||
// 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);
|
||||
// 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.PersistentMprotect = QueryEnvUint32("MOBILEGL_PIPE_PERSISTENT_MPROTECT", 1, 0, 1);
|
||||
if (ipc.PersistentMprotect == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 - no SIGSEGV handler is installed "
|
||||
"for persistent write maps; every one takes the hash scan");
|
||||
}
|
||||
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 "
|
||||
@@ -691,7 +761,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Loading configuration from environment variables...");
|
||||
@@ -699,6 +768,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
|
||||
InitBackendType();
|
||||
InitFeatures();
|
||||
InitRecordArm();
|
||||
#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.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
|
||||
#endif
|
||||
#include <MG_Impl/Pipe/Verb/VerbPort.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/ServerProbe.h>
|
||||
#endif
|
||||
@@ -182,6 +183,13 @@ namespace MobileGL {
|
||||
#endif
|
||||
{
|
||||
MG_Backend::Init();
|
||||
// P13 W4: monolith's record arm reads the verb's own state, which only a verb sink
|
||||
// writes; the port routes the ported verbs through one (MG_Impl/Pipe/Verb/VerbPort.cpp).
|
||||
// Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the
|
||||
// port is client code, and the server image must not need it to link.
|
||||
if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) {
|
||||
MG_Record::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable);
|
||||
}
|
||||
}
|
||||
MGLOG_D("MG_Backend initialized");
|
||||
MG_Impl::Init();
|
||||
|
||||
+4
-3
@@ -33,10 +33,11 @@ namespace MobileGL {
|
||||
} // namespace MG_ConfigLoader
|
||||
|
||||
// Whether this process can be served right now. False when MobileGL is configured to reach a
|
||||
// server that is not there (or a split session already failed to come up), or when the process
|
||||
// asked for software rendering (LIBGL_ALWAYS_SOFTWARE, desktop Linux), so a
|
||||
// server that is not there (or a split session already failed to come up), so a
|
||||
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
|
||||
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
|
||||
// decline rather than hand out something that cannot draw. LIBGL_ALWAYS_SOFTWARE does NOT
|
||||
// make it false: that variable selects the system GL's software renderer and is not a reason
|
||||
// to disable MobileGL's entry points. Cheap: once true it stays true
|
||||
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
|
||||
Bool ImplementationAvailable();
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
#include <Includes.h>
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
@@ -37,22 +35,16 @@ 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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +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)
|
||||
@@ -78,7 +77,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
|
||||
if (!gl.glGetError) return;
|
||||
@@ -860,7 +858,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();
|
||||
@@ -872,17 +869,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
|
||||
formatIndex < kFormatCapabilityFormatCount) {
|
||||
// Descending, so the head is the largest count this device actually allocated.
|
||||
const Vector<Int>& probedCounts =
|
||||
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||
if (!probedCounts.empty()) {
|
||||
maxSamples = probedCounts.front();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (maxSamples <= 0) {
|
||||
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
|
||||
}
|
||||
@@ -995,6 +981,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
// The colour-attachment cap the framebuffer family checks is known from here on.
|
||||
DirectGLES::ResolveRecordArmFamilies();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
return true;
|
||||
@@ -1085,7 +1073,6 @@ 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);
|
||||
@@ -1098,9 +1085,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
NoteNativeContextGone();
|
||||
}
|
||||
return created;
|
||||
#else
|
||||
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
@@ -1139,9 +1123,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!DirectGLES::ReleaseCurrent()) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
}
|
||||
|
||||
@@ -1212,9 +1194,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
(void)DirectGLES::ReleaseCurrent();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1267,9 +1247,7 @@ 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
|
||||
m_suspendedWindowSurfaces.clear();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
@@ -1295,10 +1273,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
m_suspendedWindowSurfaces.push_back(client);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The native binding moved onto the placeholder: the next make-current binds for real.
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
|
||||
"contexts and their objects stay",
|
||||
onWindow.size(), window);
|
||||
@@ -1327,9 +1303,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (surface == EGL_NO_SURFACE) return false;
|
||||
native->second = surface;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
|
||||
window, width, height);
|
||||
return true;
|
||||
@@ -1366,9 +1340,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// every object the client had built - which a multi-window client hit on its first closed
|
||||
// popup.
|
||||
(void)surface;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
@@ -1633,7 +1605,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
|
||||
funcsTable.GL.MultiDrawElements = MultiDrawElements;
|
||||
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||
@@ -1661,18 +1632,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.ClearBufferfv = ClearBufferfv;
|
||||
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||
funcsTable.GL.CopyImageSubData = CopyImageSubData;
|
||||
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
||||
funcsTable.GL.ReadPixels = ReadPixels;
|
||||
funcsTable.GL.GetTexImage = GetTexImage;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
funcsTable.GL.WaitSync = WaitSync;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +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
|
||||
@@ -38,7 +37,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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);
|
||||
@@ -48,8 +46,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count);
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount);
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
@@ -73,21 +69,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLuint baseinstance);
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect);
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter);
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
@@ -100,7 +83,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void GenerateMipmap(GLenum target);
|
||||
const GLubyte* GetString(GLenum name);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
@@ -284,7 +266,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
|
||||
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
|
||||
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
|
||||
@@ -323,7 +304,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
|
||||
// also the answer when no probe has ever been installed.
|
||||
Bool CurrentTwinKey(TwinKey* outKey);
|
||||
#endif
|
||||
|
||||
// Builds the native context for `contextToken` under the calling thread's session. A
|
||||
// `shareGroupToken` whose group already has a live native context in this session hands that
|
||||
@@ -347,7 +327,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// The calling thread's current native context as a lifetime serial: assigned when the context
|
||||
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
|
||||
// made current through MakeCurrent.
|
||||
Uint64 CurrentNativeContextSerial();
|
||||
//
|
||||
// P15: it lives in one constant-initialized thread_local with the PerNativeContext lookup
|
||||
// cache below, so the draw path's "this context's entry" question is one TLS address
|
||||
// computation, not two calls each paying their own.
|
||||
struct NativeContextThreadState {
|
||||
struct CacheEntry {
|
||||
const void* Owner = nullptr;
|
||||
Uint64 Serial = 0;
|
||||
Uint64 Epoch = 0;
|
||||
void* Entry = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kCacheEntries = 8;
|
||||
Uint64 BoundSerial = 0;
|
||||
// One entry per PerNativeContext instance slot (instances sharing a slot evict each other).
|
||||
CacheEntry Cache[kCacheEntries]{};
|
||||
};
|
||||
inline constinit thread_local NativeContextThreadState t_nativeContextThread;
|
||||
inline Uint64 CurrentNativeContextSerial() { return t_nativeContextThread.BoundSerial; }
|
||||
|
||||
class PerNativeContextBase {
|
||||
public:
|
||||
@@ -360,6 +357,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
protected:
|
||||
~PerNativeContextBase();
|
||||
// This instance's entry in NativeContextThreadState::Cache.
|
||||
const Uint32 m_cacheSlot;
|
||||
};
|
||||
|
||||
// Called where a native context is destroyed.
|
||||
@@ -374,12 +373,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
class PerNativeContext final : public PerNativeContextBase {
|
||||
public:
|
||||
T& Current() {
|
||||
const Uint64 serial = CurrentNativeContextSerial();
|
||||
NativeContextThreadState& thread = t_nativeContextThread;
|
||||
const Uint64 serial = thread.BoundSerial;
|
||||
// The draw path asks for the same entry over and over from one thread: a one-entry
|
||||
// thread-local cache, invalidated by any drop anywhere (rare).
|
||||
thread_local CurrentCache cache;
|
||||
// thread-local cache per instance, invalidated by any drop anywhere (rare) and by an
|
||||
// instance's destruction (so a later instance at the same address cannot hit).
|
||||
NativeContextThreadState::CacheEntry& cache = thread.Cache[m_cacheSlot];
|
||||
const Uint64 epoch = PerNativeContextEpoch();
|
||||
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
|
||||
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) {
|
||||
return *static_cast<T*>(cache.Entry);
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
|
||||
cache = {this, serial, epoch, &entry};
|
||||
@@ -402,12 +405,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
private:
|
||||
struct CurrentCache {
|
||||
const PerNativeContext* Owner = nullptr;
|
||||
Uint64 Serial = 0;
|
||||
Uint64 Epoch = 0;
|
||||
T* Entry = nullptr;
|
||||
};
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<Uint64, T> m_bySerial;
|
||||
};
|
||||
@@ -434,12 +431,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// native context already current on this thread IS that one - so the single-context world pays
|
||||
// nothing for it. Called from the bind_context path on the apply thread.
|
||||
Bool MakeNativeContextCurrentForBoundToken();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
|
||||
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
|
||||
// binding shadow, which still describes the previous holder's native context.
|
||||
void OnBackendTurnHandoff();
|
||||
#endif
|
||||
// How many native contexts this process holds, and how many of them belong to one session. The
|
||||
// test observables for "a second context did not tear the first one down".
|
||||
Uint64 NativeContextCount();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -44,14 +44,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_Pipe::gPipeInputs->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -95,7 +95,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// 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
|
||||
// `gPipeInputs->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.
|
||||
@@ -125,7 +125,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
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
|
||||
@@ -175,81 +174,45 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
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;
|
||||
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 (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.Id = resource->id;
|
||||
if (question == IndexBufferQuestion::DriverNameAndSize) {
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
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;
|
||||
}
|
||||
#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;
|
||||
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -269,7 +232,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
//
|
||||
// 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
|
||||
// `gPipeInputs->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
|
||||
@@ -293,23 +256,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
#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);
|
||||
return resource ? resource->id : 0;
|
||||
return resource->id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
// translation units that include it without copying Managers.h's include order.
|
||||
#include "DirectGLES.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#endif
|
||||
@@ -45,66 +42,34 @@
|
||||
// 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;
|
||||
// from their destructor carrying {kind, lifetimeId, handle}; the backend consumes it in
|
||||
// Managers.cpp and releases the twin BY THAT HANDLE in every holder of the kind
|
||||
// (ReleaseTwinByHandle below - the same release the wire's object_death runs on a server), and
|
||||
// the CLIENT returns the slot after the notice (PipeFill.cpp's NotifyAndFree). So:
|
||||
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind;
|
||||
// * 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.)
|
||||
// length of a test, and a context reset does the same in reverse. Every table therefore links
|
||||
// itself into a per-table-type list at construction and out at destruction, and one release
|
||||
// drops the twin in each holder by handle. (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.)
|
||||
//
|
||||
// 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.
|
||||
// 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).
|
||||
//
|
||||
// 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.
|
||||
// P13: THE TABLE IS HANDLE-ONLY. The frontend-keyed half - a GetOrCreate that minted handles
|
||||
// off a frontend object's lifetime id from inside MG_Backend, the object-keyed Find / HandleOf
|
||||
// with their lifetime-id memo, the weak per-entry state note and the lifetime-id death walk -
|
||||
// was monolith glue, unreachable once every family ran the record arm, and is deleted. A handle
|
||||
// is minted by the CLIENT and arrives in the record; nothing on this side touches the client's
|
||||
// slot allocator (the link ratchet's P13 exit gate pins that).
|
||||
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
|
||||
@@ -122,9 +87,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
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
|
||||
@@ -140,6 +102,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||
|
||||
void DiagnoseEsprytSlotArm();
|
||||
|
||||
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
|
||||
@@ -151,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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
|
||||
// INLINE on purpose. Every FindByHandle / GetOrCreate / 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
|
||||
@@ -184,7 +149,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// are two clients, and nothing may be shared with either from the other.
|
||||
//
|
||||
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
|
||||
// OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach
|
||||
// OBJECT, not about a bucket: ReleaseTwinByHandle must reach
|
||||
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
|
||||
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
|
||||
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
|
||||
@@ -242,6 +207,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Table& ForCallingThread() const {
|
||||
Key key;
|
||||
CurrentTwinKey(&key);
|
||||
// The memo hit without the SharedPtr copy Shared() hands out: two atomic reference
|
||||
// count operations per twin lookup, several lookups per draw (P14). The map keeps the
|
||||
// table alive; only Drop/DropSession/Reset release it, and they clear the memo.
|
||||
if (m_memoTable != nullptr && m_memoKey == key) return *m_memoTable;
|
||||
return *Shared(key);
|
||||
}
|
||||
|
||||
@@ -331,15 +300,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return Shared(key)->GetOrCreate(handle);
|
||||
}
|
||||
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
return ForCallingThread().HandleOf(stateObj);
|
||||
}
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
ForCallingThread().NoteStateForHandle(handle, stateObj);
|
||||
}
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
return ForCallingThread().StateForHandle(handle);
|
||||
}
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
|
||||
@@ -364,9 +324,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return Table::ReleaseTwinByHandle(handle);
|
||||
}
|
||||
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||
return Table::OnFrontendObjectDestroyed(lifetimeId);
|
||||
}
|
||||
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
|
||||
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
|
||||
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
|
||||
@@ -408,8 +365,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
@@ -439,14 +394,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
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;
|
||||
@@ -456,27 +403,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
// ENTRIES; the links are the table's own and are never copied.
|
||||
BackendSlotTable() { LinkHolder(); }
|
||||
BackendSlotTable(const BackendSlotTable& other):
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
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) {
|
||||
m_nullTwin(other.m_nullTwin) {
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
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) {
|
||||
m_nullTwin(std::move(other.m_nullTwin)) {
|
||||
other.m_slots.clear();
|
||||
other.m_band.clear();
|
||||
other.ForgetHandle();
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable& operator=(const BackendSlotTable& other) {
|
||||
@@ -484,8 +426,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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;
|
||||
}
|
||||
@@ -494,11 +434,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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;
|
||||
}
|
||||
@@ -510,88 +447,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
|
||||
Uint64 OwnerSession() const { return m_ownerSession; }
|
||||
|
||||
// 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.
|
||||
// P3a: resolve-or-create BY HANDLE - since P13 the only resolve-or-create there is. The
|
||||
// handle ARRIVED, in the call's payload, already minted by the client; this never
|
||||
// touches the allocator. It indexes the slot, notices a generation that no longer
|
||||
// matches (the slot was recycled, so the twin at it describes driver ids the new
|
||||
// resource never made) and hands back the twin pointer.
|
||||
//
|
||||
// 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.
|
||||
// Death stays ANNOUNCED: the family's own destroy call or the shared death notice
|
||||
// releases the twin by handle, and the slot is freed by the CLIENT after that returns.
|
||||
//
|
||||
// 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");
|
||||
@@ -621,15 +485,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#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.
|
||||
// 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,
|
||||
// THE TWO DIRECTIONS ARE NOT SYMMETRIC. 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
|
||||
@@ -671,31 +531,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
@@ -711,29 +546,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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);
|
||||
@@ -741,65 +561,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
@@ -851,14 +612,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
@@ -898,7 +651,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<Entry> slots = std::move(m_slots);
|
||||
Vector<Entry> band = std::move(m_band);
|
||||
BackendPtr nullTwin = std::move(m_nullTwin);
|
||||
ForgetHandle();
|
||||
slots.clear();
|
||||
band.clear();
|
||||
nullTwin.reset();
|
||||
@@ -934,13 +686,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
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.
|
||||
// the kind, not to the table, and the client returns it once, after the release.
|
||||
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,
|
||||
@@ -951,7 +698,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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();
|
||||
@@ -986,8 +732,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
// Grows the right space to hold `slot`. Every caller bounds `slot` first - 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.
|
||||
@@ -998,15 +743,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
@@ -1044,31 +780,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -37,21 +37,19 @@
|
||||
#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 MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
|
||||
return &server->GetFormatCapabilities();
|
||||
@@ -66,9 +64,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"on the apply thread");
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
|
||||
@@ -108,26 +106,16 @@ 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 +159,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 +253,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 +293,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));
|
||||
@@ -2362,15 +2334,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();
|
||||
SplitReadbackPackBuffer();
|
||||
|
||||
// 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_Pipe::gPipeInputs->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,7 +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
|
||||
@@ -24,7 +23,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (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 {
|
||||
|
||||
@@ -281,7 +281,6 @@
|
||||
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
|
||||
@@ -352,8 +351,8 @@
|
||||
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);
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
|
||||
const auto target = static_cast<Uint16>(uploadTarget);
|
||||
const auto mip = static_cast<Uint16>(level);
|
||||
ReadbackChannelMapping mapping{};
|
||||
@@ -361,7 +360,7 @@
|
||||
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()) {
|
||||
if (!store.Holds()) {
|
||||
declined = "this arm keeps no staged store";
|
||||
} else if (store.IsDriverWritten(key)) {
|
||||
declined = "the driver has written the texture since its bytes crossed";
|
||||
@@ -460,4 +459,3 @@
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -32,11 +33,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return MG_Config::Features.MagmaR11G11B10FFallback;
|
||||
}
|
||||
|
||||
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
(void)dpy;
|
||||
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
Bool IsFormatIndexValid(TextureInternalFormat format) {
|
||||
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
|
||||
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
|
||||
@@ -423,17 +419,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
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
|
||||
@@ -441,7 +432,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
}
|
||||
@@ -464,9 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MG_Pipe::MGPipeNotifyCompileEnvChanged();
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -998,7 +986,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
|
||||
funcsTable.GL.MultiDrawElements = MultiDrawElements;
|
||||
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||
@@ -1019,19 +1006,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.ClearBufferfv = ClearBufferfv;
|
||||
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||
funcsTable.GL.CopyImageSubData = CopyImageSubData;
|
||||
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
||||
funcsTable.GL.ReadPixels = ReadPixels;
|
||||
funcsTable.GL.GetTexImage = GetTexImage;
|
||||
funcsTable.GL.GetTextureImage = GetTextureImage;
|
||||
funcsTable.GL.DispatchCompute = DispatchCompute;
|
||||
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
|
||||
funcsTable.GL.MemoryBarrier = MemoryBarrier;
|
||||
@@ -1081,9 +1061,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MG_Pipe::MGPipeNotifyCompileEnvChanged();
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,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
|
||||
@@ -55,7 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
@@ -67,21 +65,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLfloat* value);
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLint* value);
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLuint* value);
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void Clear(GLbitfield mask);
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count);
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount);
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex);
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
@@ -107,11 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter);
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
@@ -131,9 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
|
||||
// GL fence sync objects, mapped onto the renderer's frame-serial busy
|
||||
// tracking: a fence captures the frame serial current at creation and is
|
||||
// signaled once every command recorded under that serial has completed on
|
||||
|
||||
@@ -26,11 +26,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
std::atomic<Uint64> g_nextSessionId{1};
|
||||
|
||||
thread_local MagmaSession* t_boundSession = nullptr;
|
||||
|
||||
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
|
||||
// monolith's renderer down from a static destructor after the driver is gone.
|
||||
MagmaSession& ProcessSession() {
|
||||
MagmaSession& ProcessSessionInstance() {
|
||||
static auto* session = new MagmaSession();
|
||||
return *session;
|
||||
}
|
||||
@@ -42,12 +40,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return static_cast<Uint32>(registry.size() - 1);
|
||||
}
|
||||
|
||||
MagmaSession& Detail::CurrentMagmaSession() {
|
||||
MagmaSession* bound = t_boundSession;
|
||||
return bound != nullptr ? *bound : ProcessSession();
|
||||
}
|
||||
MagmaSession& Detail::ProcessMagmaSession() { return ProcessSessionInstance(); }
|
||||
|
||||
void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
|
||||
void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index) {
|
||||
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
|
||||
void*& slot = session.m_slots[index];
|
||||
if (slot == nullptr) slot = Registry()[index].construct();
|
||||
@@ -63,25 +58,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
MagmaSession::~MagmaSession() {
|
||||
MagmaSession* const previous = t_boundSession;
|
||||
t_boundSession = this;
|
||||
MagmaSession* const previous = Detail::t_boundMagmaSession;
|
||||
Detail::t_boundMagmaSession = this;
|
||||
const auto& registry = Registry();
|
||||
for (SizeT i = m_slots.size(); i-- > 0;) {
|
||||
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
|
||||
m_slots[i] = nullptr;
|
||||
}
|
||||
// Never back to this session: it is gone.
|
||||
t_boundSession = previous == this ? nullptr : previous;
|
||||
Detail::t_boundMagmaSession = previous == this ? nullptr : previous;
|
||||
}
|
||||
|
||||
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(t_boundSession) {
|
||||
t_boundSession = session;
|
||||
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(Detail::t_boundMagmaSession) {
|
||||
Detail::t_boundMagmaSession = session;
|
||||
}
|
||||
|
||||
MagmaSessionScope::~MagmaSessionScope() { t_boundSession = m_previous; }
|
||||
MagmaSessionScope::~MagmaSessionScope() { Detail::t_boundMagmaSession = m_previous; }
|
||||
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session) { t_boundSession = session; }
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session) { Detail::t_boundMagmaSession = session; }
|
||||
|
||||
MagmaSession* BoundMagmaSession() { return t_boundSession; }
|
||||
MagmaSession* BoundMagmaSession() { return Detail::t_boundMagmaSession; }
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -34,9 +34,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using SessionLocalDestroy = void (*)(void*);
|
||||
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
|
||||
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
|
||||
// The calling thread's bound session (MagmaSessionScope, BindMagmaSessionToThisThread), null
|
||||
// when it has none. P15: visible here, and constant-initialized, so the read inlines into
|
||||
// every SessionLocal access and one function's accesses can share a single TLS address
|
||||
// computation instead of paying one out-of-line call each.
|
||||
inline constinit thread_local MagmaSession* t_boundMagmaSession = nullptr;
|
||||
MagmaSession& ProcessMagmaSession();
|
||||
// The calling thread's session, or the process-wide default.
|
||||
MagmaSession& CurrentMagmaSession();
|
||||
void* SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
inline MagmaSession& CurrentMagmaSession() {
|
||||
MagmaSession* const bound = t_boundMagmaSession;
|
||||
return bound != nullptr ? *bound : ProcessMagmaSession();
|
||||
}
|
||||
inline void* SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
void* SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
|
||||
} // namespace Detail
|
||||
|
||||
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
|
||||
@@ -55,12 +65,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
friend void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
|
||||
Vector<void*> m_slots;
|
||||
Uint64 m_id = 0;
|
||||
};
|
||||
|
||||
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
|
||||
// previous binding (usually none) afterwards.
|
||||
// A built slot is returned in line; a missing one is built out of line.
|
||||
inline void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
|
||||
if (index < session.m_slots.size()) {
|
||||
if (void* const slot = session.m_slots[index]) return slot;
|
||||
}
|
||||
return SessionLocalSlotSlow(session, index);
|
||||
}
|
||||
|
||||
class MagmaSessionScope {
|
||||
public:
|
||||
explicit MagmaSessionScope(MagmaSession* session);
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
// 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 +126,12 @@ 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) {
|
||||
if (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,7 @@
|
||||
// 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) {
|
||||
@@ -110,7 +108,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = flags;
|
||||
// P15: a frame recording is submitted once and then reset, so the driver may skip whatever
|
||||
// it keeps for re-submission.
|
||||
beginInfo.flags = flags | VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
beginInfo.pInheritanceInfo = pInheritanceInfo;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
|
||||
|
||||
@@ -139,6 +139,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo beginInfo{};
|
||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
|
||||
"BeginPreCommandRecording, vkBeginCommandBuffer");
|
||||
frame.isPreCommandRecording = true;
|
||||
@@ -224,13 +225,10 @@ 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.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -243,12 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
@@ -309,7 +302,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& frame = GetCurrent();
|
||||
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
|
||||
VK_NULL_HANDLE, &outImageIndex);
|
||||
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false;
|
||||
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) {
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -340,6 +336,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
||||
@@ -483,6 +480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.hasCommandBufferRecorded = false;
|
||||
frame.isCommandRecording = false;
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -501,5 +499,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.isCommandRecording = false;
|
||||
frame.hasCommandBufferRecorded = false;
|
||||
frame.imageAvailableSemaphoreConsumed = false;
|
||||
frame.acquiredImageReferenced = false;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -25,8 +25,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
|
||||
};
|
||||
|
||||
// The stages the acquire wait blocks. Every first access of an acquired swapchain image is
|
||||
// a layout transition out of PRESENT_SRC_KHR or UNDEFINED whose source scope includes these
|
||||
// stages (TransitionWireImage, TransitionToPresent), and that chain is what orders the
|
||||
// access after the presentation engine's release. Work at other stages, in this and later
|
||||
// submissions, does not wait for the release.
|
||||
static constexpr VkPipelineStageFlags kAcquireWaitStages =
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
|
||||
struct SubmitInfoPacket {
|
||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkPipelineStageFlags waitDstStageMask = kAcquireWaitStages;
|
||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
|
||||
// [0] = pre-pass command buffer (when recorded), then the frame
|
||||
@@ -67,6 +75,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool isPreCommandRecording = false;
|
||||
Bool hasPreCommandBufferRecorded = false;
|
||||
Bool imageAvailableSemaphoreConsumed = false;
|
||||
// The open recording references the acquired swapchain image. Only a submission that
|
||||
// does (or the present's) waits on imageAvailableSemaphore: the frame's work recorded
|
||||
// before its first use of the image goes to the queue without the wait, so it never
|
||||
// queues behind the release of an image the display still scans out.
|
||||
Bool acquiredImageReferenced = false;
|
||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
||||
// appended in submit order; freed once their submission is known
|
||||
// complete (fence wait or completion poll).
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire() {
|
||||
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire(Uint64 completedSubmitIndex) {
|
||||
Bracket bracket;
|
||||
if (m_device == VK_NULL_HANDLE) return bracket;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -106,7 +106,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (slot.Claimed) continue;
|
||||
// Retire what finished since the watch last looked: frames come faster than its polls.
|
||||
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
|
||||
if (!slot.InFlight && free == nullptr) free = &slot;
|
||||
// Free once its submission is complete by fence, not by the event read (see the header).
|
||||
if (!slot.InFlight && slot.SubmitIndex <= completedSubmitIndex && free == nullptr) free = &slot;
|
||||
}
|
||||
if (free == nullptr) {
|
||||
if (!AddSlot()) return bracket;
|
||||
@@ -125,7 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
void GpuProgressMarkers::Submitted(const Bracket& bracket) {
|
||||
void GpuProgressMarkers::Submitted(const Bracket& bracket, Uint64 submitIndex) {
|
||||
if (!bracket.Valid()) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (bracket.Slot >= m_slots.size()) return;
|
||||
@@ -133,6 +134,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
slot.Claimed = false;
|
||||
slot.InFlight = true;
|
||||
slot.Dirty = true;
|
||||
slot.SubmitIndex = submitIndex;
|
||||
slot.Id = m_nextId++;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// The two buffers to put first and last in one submission (an invalid bracket when off or
|
||||
// when every slot is in flight: the submission then goes unwatched).
|
||||
Bracket Acquire();
|
||||
// The submission carrying `bracket` was accepted by vkQueueSubmit / was not submitted.
|
||||
void Submitted(const Bracket& bracket);
|
||||
//
|
||||
// A slot is reused only once the submission it last bracketed is COMPLETE by its fence
|
||||
// (`completedSubmitIndex`, the renderer's contiguous completed-submit floor), not merely once
|
||||
// the host has read its `finished` event as set. A host read of an event gives no device-side
|
||||
// ordering: nothing orders the bracket's `started` set (TOP_OF_PIPE, first buffer) against
|
||||
// its `finished` set, so a host vkResetEvent plus a re-set in a later submission could
|
||||
// overlap a set the earlier submission has not executed yet. The synchronization validator
|
||||
// reports that as SYNC-vkCmdSetEvent-missingbarrier-set. Event reads still drive the watch.
|
||||
Bracket Acquire(Uint64 completedSubmitIndex);
|
||||
// The submission carrying `bracket` (renderer submit index `submitIndex`) was accepted by
|
||||
// vkQueueSubmit / was not submitted.
|
||||
void Submitted(const Bracket& bracket, Uint64 submitIndex);
|
||||
void Abandon(const Bracket& bracket);
|
||||
|
||||
// The watch named one of this device's submissions as hanging the GPU.
|
||||
@@ -74,6 +83,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool InFlight = false; // submitted and not yet seen finished
|
||||
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
|
||||
Bool Dirty = false; // its events were set by a past submission: reset before reuse
|
||||
Uint64 SubmitIndex = 0; // the renderer submit index of the submission it last bracketed
|
||||
};
|
||||
static constexpr Uint32 kMaxSlots = 16;
|
||||
|
||||
|
||||
@@ -10,18 +10,17 @@
|
||||
#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.
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). (The {slot, gen}
|
||||
// mint that used to live here keyed the frontend arm's VAO memos and went with that arm in P13
|
||||
// W6; the record arm keys every object on the applier's own handles.)
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
@@ -44,7 +43,6 @@
|
||||
// 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;
|
||||
@@ -82,8 +80,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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)) {
|
||||
if (!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 "
|
||||
@@ -93,18 +90,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 "
|
||||
@@ -115,497 +103,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#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"
|
||||
@@ -12,112 +11,110 @@
|
||||
// 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(); }
|
||||
Uint GetExternalIndex() const { return m_handle.Slot; }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
return (Uint64(m_handle.Gen) << 32) | m_handle.Slot;
|
||||
}
|
||||
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 GetLinkStatus() const { return m_record->Desc.LinkStatus != 0; }
|
||||
Bool GetSpirvStatus() const { return m_record->Desc.SpirvStatus != 0; }
|
||||
Bool PointSizeDemoted() const { return m_record->Desc.PointSizeDemoted != 0; }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
return m_record->Desc.EnableSpirvValidation != 0;
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
return m_record->Archive->Spirv.generatedSpirv;
|
||||
}
|
||||
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();
|
||||
}
|
||||
// The view has no lifetime of its own, so the memo lives in the server record it views
|
||||
// (MGPipeShaderCsoRecord::BackendHashMemo), never in the client's program object: an entry
|
||||
// answers only while the record's Serial and BindingsSerial are the ones it was taken at.
|
||||
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);
|
||||
for (const auto& entry : m_record->BackendHashMemo) {
|
||||
if (entry.Valid && entry.Key == flags && entry.Serial == m_record->Serial &&
|
||||
entry.BindingsSerial == m_record->BindingsSerial) {
|
||||
hash = entry.Hash;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
auto& memo = m_record->BackendHashMemo;
|
||||
auto& entry = memo[m_record->BackendHashMemoNext++ % memo.size()];
|
||||
entry.Serial = m_record->Serial;
|
||||
entry.BindingsSerial = m_record->BindingsSerial;
|
||||
entry.Hash = hash;
|
||||
entry.Key = flags;
|
||||
entry.Valid = true;
|
||||
}
|
||||
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(); }
|
||||
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
|
||||
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetImageUnitVersion() const { return m_record->BindingsSerial; }
|
||||
Uint64 GetLinkVersion() const { return m_record->Serial; }
|
||||
|
||||
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(); }
|
||||
const void* GetUBOData() const { return m_record->GlobalConstants.data(); }
|
||||
Uint GetUBOSize() const { return static_cast<Uint>(m_record->GlobalConstants.size()); }
|
||||
Uint32 GetUBOContentVersion() const { return m_record->GlobalConstantsVersion; }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
return static_cast<Int>(Link().glBlockIndexToTProgram.size());
|
||||
}
|
||||
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);
|
||||
return Program::IsValidUniformLocation(Link(), 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);
|
||||
@@ -145,17 +142,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Link().xfbVaryings.size(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
return Link().xfbVaryings;
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
SizeT GetTransformFeedbackBufferCount() const { return Link().xfbStrides.size(); }
|
||||
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();
|
||||
return Link().xfbBufferMode;
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
@@ -167,7 +163,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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
|
||||
@@ -188,7 +183,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: 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];
|
||||
@@ -210,7 +204,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
@@ -218,7 +211,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -194,69 +194,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
|
||||
XXH64_state_t* const hashState = m_hashState;
|
||||
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
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,
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.wireRenderPassCompatibilityId,
|
||||
sizeof(payload.wireRenderPassCompatibilityId)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(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)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
XXH64_update(hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXH64_update(hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXH64_update(hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOp, sizeof(payload.logicOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXH64_update(hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXH64_update(hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXH64_update(hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXH64_update(hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
XXH64_update(hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||
if (payload.colorAttachmentCount > 0) {
|
||||
XXHASH_VERIFY(XXH64_update(
|
||||
m_hashState,
|
||||
hashState,
|
||||
payload.colorBlendAttachments.data(),
|
||||
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(hashState);
|
||||
}
|
||||
|
||||
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
|
||||
@@ -286,12 +286,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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_frameCounter});
|
||||
return pipeline;
|
||||
@@ -341,34 +339,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
||||
if (renderPasses.empty() || m_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
||||
// per dying pass.
|
||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
||||
}
|
||||
it = m_cache.erase(it);
|
||||
++evicted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
||||
evicted, sortedPasses.size());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
||||
Uint32 evicted = 0;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
|
||||
@@ -35,11 +35,9 @@ 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
|
||||
@@ -142,15 +140,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
||||
Uint32 OnFrameBoundary();
|
||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
||||
// Destroys every cached pipeline built from the program with content hash
|
||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
||||
|
||||
@@ -2235,28 +2235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) {
|
||||
switch (imageTraits.dim) {
|
||||
case SpvDim1D:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D;
|
||||
case SpvDim2D:
|
||||
if (imageTraits.ms != 0) {
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray
|
||||
: TextureTarget::Texture2DMultisample;
|
||||
}
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D;
|
||||
case SpvDim3D:
|
||||
return TextureTarget::Texture3D;
|
||||
case SpvDimCube:
|
||||
return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap;
|
||||
case SpvDimBuffer:
|
||||
return TextureTarget::TextureBuffer;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim);
|
||||
return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
|
||||
switch (uniformType) {
|
||||
case GL_IMAGE_1D:
|
||||
@@ -2597,26 +2575,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program,
|
||||
CompileOptionFlags flags) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
|
||||
XXH64_state_t* const hashState = m_hashState;
|
||||
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
|
||||
// We expect shader stages in program object are sorted
|
||||
const auto& spirvs = program.GetGeneratedSpirv();
|
||||
for (const auto& spv : spirvs) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &flags, sizeof(CompileOptionFlags)));
|
||||
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
|
||||
// re-key every program in the cache on a resize for no reason.
|
||||
if (flags & CompileOptionBit::FragCoordYFlip) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &m_defaultFramebufferHeight,
|
||||
sizeof(m_defaultFramebufferHeight)));
|
||||
}
|
||||
|
||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &blockCount, sizeof(blockCount)));
|
||||
for (Uint32 i = 0; i < blockCount; ++i) {
|
||||
const Uint32 binding = program.GetUniformBlockBinding(i);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &binding, sizeof(binding)));
|
||||
}
|
||||
|
||||
// The transform feedback capture layout is baked into the modules by
|
||||
@@ -2627,18 +2607,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// hashed for a capturing compile, so nothing else changes key.
|
||||
if (flags & CompileOptionBit::XfbCapture) {
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
}
|
||||
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &stride, sizeof(stride)));
|
||||
}
|
||||
}
|
||||
|
||||
HashType hash = XXH64_digest(m_hashState);
|
||||
HashType hash = XXH64_digest(hashState);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -3199,11 +3179,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
if (descriptorKind == DescriptorBindingKind::StorageBuffer) {
|
||||
const GLuint blockIndex =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
program.GetShaderStorageBlockIndex(uniformName);
|
||||
#else
|
||||
GetShaderStorageBlockIndex(program, uniformName);
|
||||
#endif
|
||||
if (blockIndex == GL_INVALID_INDEX) {
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u",
|
||||
uniformName.c_str(), binding);
|
||||
@@ -3495,13 +3471,11 @@ 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
|
||||
entry.dynamicBindings.empty() &&
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
#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>)
|
||||
@@ -175,7 +173,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
|
||||
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
Uint32 extraImageCount, VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
|
||||
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
|
||||
"SwapchainObject::Create failed: incomplete swapchain capabilities");
|
||||
@@ -200,11 +198,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_supportedPresentModes = swapchainCapabilities.presentModes;
|
||||
|
||||
const auto& swapchainCaps = swapchainCapabilities.capabilities;
|
||||
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
|
||||
// The extra images count from the surface's own minimum: that is how many the platform
|
||||
// keeps for itself and the application, so only images beyond it delay a buffer's reuse.
|
||||
Uint32 targetImageCount =
|
||||
std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount + extraImageCount);
|
||||
if (swapchainCaps.maxImageCount != 0) {
|
||||
targetImageCount = std::min(targetImageCount, swapchainCaps.maxImageCount);
|
||||
}
|
||||
MGLOG_I("Set minImageCount = %u", targetImageCount);
|
||||
MGLOG_I("Set minImageCount = %u (surface min %u, max %u, extra %u)", targetImageCount,
|
||||
swapchainCaps.minImageCount, swapchainCaps.maxImageCount, extraImageCount);
|
||||
MGLOG_I("Swapchain currentTransform = %s",
|
||||
string_VkSurfaceTransformFlagBitsKHR(swapchainCaps.currentTransform));
|
||||
|
||||
@@ -349,7 +351,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
@@ -367,7 +368,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
|
||||
@@ -36,8 +36,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
|
||||
|
||||
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
|
||||
Optional<Int> swapInterval);
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, Uint32 extraImageCount,
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval);
|
||||
void Shutdown(VkDevice device);
|
||||
// Tells the frontend (or, under a transport, the client) the default framebuffer's size
|
||||
// and formats. Create does this; a renderer switching between surfaces repeats it.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
#include "RenderPassGuard.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "VkSamplerManager.h"
|
||||
@@ -27,16 +28,6 @@ namespace MobileGL::MG_State::GLState {
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class UniformManager {
|
||||
public:
|
||||
struct SamplerBindingOverride {
|
||||
Uint32 binding = 0;
|
||||
Uint32 element = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Bool forceNearestFiltering = false;
|
||||
};
|
||||
|
||||
struct SamplerImageFeedbackBinding {
|
||||
Uint32 samplerBinding = 0;
|
||||
Uint32 samplerElement = 0;
|
||||
@@ -56,7 +47,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.
|
||||
@@ -74,10 +64,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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; }
|
||||
// The image-write epoch the last full memory barrier made visible (see m_wireImageBarrierEpoch),
|
||||
// and the note a caller that just recorded that same barrier makes.
|
||||
Uint64 WireImageBarrierEpoch() const { return m_wireImageBarrierEpoch; }
|
||||
void NoteWireImageBarrier() const { m_wireImageBarrierEpoch = WireImageWriteEpoch(); }
|
||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
||||
@@ -102,48 +95,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||
// walk and reports whether every visited binding still resolves to the recorded
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MagmaProgramSource& 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
|
||||
// sum, sampling-resolution generation (sampler params + texture shape), image
|
||||
// epochs AND per-resource layout values - so the per-binding cached
|
||||
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||
Bool samplerDescriptorsUnchangedHint = false,
|
||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
@@ -151,16 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||
VkFormat resourceFormat, Bool useBindingFormat);
|
||||
|
||||
// True when the program reads at least one sampler and every one of them is bound to a
|
||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
// ---- per-frame descriptor pools ----------------------------------------------------
|
||||
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
|
||||
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
|
||||
@@ -215,7 +168,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
|
||||
@@ -273,7 +225,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
@@ -283,12 +234,10 @@ 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,36 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 quietEpochs = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||
// texture after the fallback substitution (may still be null when no fallback
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||
Bool ResolveSampledBinding(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MagmaProgramSource& 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.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
@@ -344,32 +263,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
//
|
||||
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||
// (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,
|
||||
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;
|
||||
Uint32 element, Bool storage, VkDescriptorImageInfo& out,
|
||||
VkTextureManager::TextureResource** outResource = nullptr) const;
|
||||
// P14: THE WIRE DESCRIPTOR REBIND. A draw whose image descriptors cannot have changed since
|
||||
// the last full resolve of the same program in the same frame - same applier context, same
|
||||
// unit / sampler / image / buffer-binding serials, same texture shutter, no texture image
|
||||
// re-created or erased, no sampler destroyed, every sampled image still in GENERAL and no
|
||||
// image write awaiting its barrier - re-resolves only its buffer descriptors (the UBO
|
||||
// dynamic offsets move every draw) and, when those name the same buffers and ranges,
|
||||
// rebinds the set it already has. Anything else, or any program with a texel buffer, storage
|
||||
// image or placeholder descriptor, takes the full resolve.
|
||||
struct WireDescriptorRebind {
|
||||
Bool valid = false;
|
||||
Uint32 frameIndex = 0;
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
Uint64 programHash = 0;
|
||||
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
MG_Pipe::MGPipeHandle programHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 programSerial = 0, programBindingsSerial = 0;
|
||||
Uint64 contextSerial = 0, samplerViewsSerial = 0, samplerStatesSerial = 0, shaderImagesSerial = 0;
|
||||
Uint64 shaderBuffersSerial = 0, textureShutterSerial = 0;
|
||||
Uint64 storeDestroyEpoch = 0, textureImageEpoch = 0, resourceEraseEpoch = 0;
|
||||
Uint64 samplerDestroyEpoch = 0, samplerFrameBoundary = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
Vector<VkTextureManager::TextureResource*> sampledResources;
|
||||
// The buffer descriptors in walk order: dynamic UBOs with offset 0 (their offset is the
|
||||
// dynamic one), storage buffers whole.
|
||||
Vector<VkDescriptorBufferInfo> bufferInfos;
|
||||
};
|
||||
WireDescriptorRebind m_wireDescriptorRebind;
|
||||
Vector<VkDescriptorBufferInfo> m_wireRebindBufferScratch;
|
||||
Vector<VkTextureManager::TextureResource*> m_wireRebindResourcesScratch;
|
||||
Bool TryRebindWireDescriptorSet(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint);
|
||||
Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, Bool storage, VkBufferView& out);
|
||||
@@ -379,9 +309,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
@@ -415,10 +342,8 @@ 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,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, UboBindResult& out) const;
|
||||
@@ -487,19 +412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_descriptorSlotTrims = 0;
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// See AcquireUnboundTexelBufferView. Lazily populated, never evicted (a program's declared
|
||||
// formats are a fixed, tiny set) and torn down with the manager. The texel views are keyed
|
||||
// by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
@@ -526,49 +446,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||
Uint32 m_descriptorReuseMemoNext = 0;
|
||||
|
||||
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
|
||||
// descriptor set selected by the last cacheable full walk of a program
|
||||
// whose active bindings are exactly one dynamic UBO (single descriptor)
|
||||
// plus combined-image samplers. When the next call proves every sampler
|
||||
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
|
||||
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
|
||||
// moved, the per-draw glUniform case - the walk collapses to: resolve one
|
||||
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
|
||||
// rebinding the same set with different dynamic offsets).
|
||||
// Invalidation inventory: BeginFrame clears it (the frame's sets are
|
||||
// recycled) and the frameIndex field guards cross-frame confusion on top;
|
||||
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
|
||||
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
|
||||
// program relink bumps the backend state version and thus programObj.hash
|
||||
// so the key misses; the program lifetime id is never reused, so a
|
||||
// deleted-and-recreated program misses; a texture/sampler/binding change
|
||||
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||
// advances, so the recorded set is never re-written within its frame.
|
||||
// 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;
|
||||
Uint64 programLifetimeId = 0;
|
||||
ProgramFactory::HashType programHash = 0;
|
||||
Uint32 uboBinding = 0;
|
||||
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize uboRange = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
};
|
||||
FastRebindMemo m_fastRebindMemo;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||
@@ -660,7 +537,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_samplerResolveMemoHighWater = binding + 1;
|
||||
}
|
||||
}
|
||||
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
|
||||
// WireImageWriteEpoch() when ResolveWireImageDescriptor, or a new draw pass's attachment
|
||||
// barrier (VulkanRenderer::TransitionWireImage), last made image writes visible.
|
||||
mutable Uint64 m_wireImageBarrierEpoch = 0;
|
||||
// P14: the wire arm's sampler memo, one entry per texture unit. The sampler a unit resolves
|
||||
// to is a function of the sampler CSO it names (immutable for a {slot, gen, Serial} within
|
||||
// one applier - ContextSerial tells appliers apart), the texture's format, the numeric
|
||||
// domain and the view's level count. A hit skips the parameter hash and the cache map; it
|
||||
// is taken only while the sampler manager has destroyed nothing (DestroyEpoch) and only
|
||||
// within the frame boundary of the last real lookup, so every used entry is still stamped
|
||||
// once per frame and the idle sweep can never retire a sampler this memo hands out.
|
||||
struct WireSamplerMemo {
|
||||
Bool valid = false;
|
||||
Uint32 slot = 0, gen = 0;
|
||||
Uint64 serial = 0, contextSerial = 0;
|
||||
Uint32 format = 0, viewLevelCount = 0;
|
||||
Bool forceNearest = false;
|
||||
Uint64 frameBoundary = 0, destroyEpoch = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr SizeT kWireSamplerMemoUnits = 192; // MG_Pipe::kMGPipeMaxTextureUnits
|
||||
mutable Array<WireSamplerMemo, kWireSamplerMemoUnits> m_wireSamplerMemo{};
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include <MG_Backend/Record/ApplyRoleBackend.h>
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
@@ -17,428 +18,68 @@
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
|
||||
namespace {
|
||||
struct WireVertexInputMemo {
|
||||
struct Key {
|
||||
Uint64 Offset;
|
||||
Uint32 Stride;
|
||||
Uint32 Divisor;
|
||||
};
|
||||
Bool Valid = false;
|
||||
Uint64 ContentSerial = 0;
|
||||
Uint32 ActiveMask = 0;
|
||||
Uint32 Start = 0;
|
||||
Uint32 Count = 0;
|
||||
Uint32 KeyCount = 0;
|
||||
Array<Key, MG_Pipe::kMGPipeMaxVertexAttribs> Keys{};
|
||||
VertexInputStateFactory::BackendVertexInputState State;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||
const auto& attr = vao.GetAttribute(i);
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||
|
||||
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||
// hash. An address is recycled by the allocator, so a deleted-and-recreated
|
||||
// buffer reproduces it; combined with a byte-identical attribute layout that
|
||||
// reproduces the WHOLE content hash, and the hash is what
|
||||
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
|
||||
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
|
||||
// slice was bound for its successor's draw, which is how a transform-feedback
|
||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
//
|
||||
// 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
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
const VertexInputStateFactory::BackendVertexInputState* VertexInputStateFactory::ResolveWireVertexInput(
|
||||
const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state,
|
||||
Uint32 activeMask) const {
|
||||
auto* memo = static_cast<WireVertexInputMemo*>(elements.BackendMemo.get());
|
||||
if (memo == nullptr) {
|
||||
auto created = std::make_shared<WireVertexInputMemo>();
|
||||
memo = created.get();
|
||||
elements.BackendMemo = Move(created);
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
// The window first: a location outside it is BuildWireVertexInput's refusal, and the
|
||||
// keys below index the window.
|
||||
Bool hit = memo->Valid && memo->ContentSerial == elements.ContentSerial && memo->ActiveMask == activeMask &&
|
||||
memo->Start == state.VertexBufferStart && memo->Count == state.VertexBufferCount;
|
||||
if (hit) {
|
||||
Uint32 n = 0;
|
||||
for (Uint32 location = 0; location < elements.AttributeCount && hit; ++location) {
|
||||
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
|
||||
const auto& buffer = state.VertexBuffers[location];
|
||||
const auto& key = memo->Keys[n++];
|
||||
hit = key.Offset == buffer.Offset && key.Stride == buffer.Stride && key.Divisor == buffer.Divisor;
|
||||
}
|
||||
if (hit) return &memo->State;
|
||||
}
|
||||
memo->Valid = false;
|
||||
if (!BuildWireVertexInput(elements, state, activeMask, memo->State)) return nullptr;
|
||||
Uint32 n = 0;
|
||||
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
|
||||
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
|
||||
const auto& buffer = state.VertexBuffers[location];
|
||||
memo->Keys[n++] = {buffer.Offset, buffer.Stride, buffer.Divisor};
|
||||
}
|
||||
memo->KeyCount = n;
|
||||
memo->ContentSerial = elements.ContentSerial;
|
||||
memo->ActiveMask = activeMask;
|
||||
memo->Start = state.VertexBufferStart;
|
||||
memo->Count = state.VertexBufferCount;
|
||||
memo->Valid = true;
|
||||
return &memo->State;
|
||||
}
|
||||
|
||||
#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.
|
||||
const void* memoState = nullptr;
|
||||
Uint64 memoEpoch = 0;
|
||||
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
|
||||
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
|
||||
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *entry;
|
||||
}
|
||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
|
||||
// memo (pure VALUES derived from the VAO configuration, so config-version
|
||||
// guarding alone is sound). The draw fast path reads them from the VAO object it
|
||||
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
|
||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||
return entry;
|
||||
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||
auto it = m_cache.find(hash);
|
||||
if (it != m_cache.end()) {
|
||||
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
VertexInputStateBuilder builder;
|
||||
Vector<SizeT> bindingBufferKeys;
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
const auto& attr = vao.GetAttribute(location);
|
||||
if (!attr.Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||
// 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) {
|
||||
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
||||
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
||||
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
||||
// than dropped. Mirrors what DirectGLES does for the same state.
|
||||
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
||||
sourceVkFormat = narrowedFormat;
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
||||
"float32 precision through format=%d (size=%d long=%s)",
|
||||
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
||||
}
|
||||
}
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat vkFormat = sourceVkFormat;
|
||||
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
|
||||
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
|
||||
// so a zero here is the binding model's stride 0 - every vertex reads the same
|
||||
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
|
||||
// means. Substituting the element size fetched a fresh element per vertex and ran
|
||||
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
|
||||
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
|
||||
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
// For a client-memory array attr.Offset holds the raw client pointer, and the
|
||||
// draw path re-uploads the data to a 16-aligned transient slice with attribute
|
||||
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
|
||||
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
|
||||
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
|
||||
((sourceStride % requiredAlignment) != 0 ||
|
||||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
|
||||
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
// A converted stream is tightly packed, so its stride is the converted element
|
||||
// size - unless the source stride is zero, which does not describe a packing at
|
||||
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||
// converts exactly one element and every vertex reads it.
|
||||
if (sourceStride != 0) {
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
||||
conversion == VertexStreamConversion::Float64ToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
|
||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||
const Uint32 binding = static_cast<Uint32>(bindingBufferKeys.size());
|
||||
bindingBufferKeys.push_back(bufferKey);
|
||||
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
|
||||
bindingAttributeLocations.push_back(location);
|
||||
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
|
||||
// else needs the extension to say it.
|
||||
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
|
||||
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
|
||||
}
|
||||
}
|
||||
|
||||
const auto& state = builder.Build();
|
||||
|
||||
auto& slot = m_cache[hash];
|
||||
if (!slot) {
|
||||
slot = MakeUnique<BackendVertexInputState>();
|
||||
}
|
||||
BackendVertexInputState& entry = *slot;
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindingDivisors = Move(bindingDivisors);
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
// buffer identities, so identical layouts across VAOs/buffers agree.
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
|
||||
for (const auto& binding : entry.bindings) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
|
||||
}
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
for (const auto& divisor : entry.bindingDivisors) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
for (const auto& attribute : entry.attributes) {
|
||||
if (attribute.location < 32u) {
|
||||
entry.attributeLocationMask |= (1u << attribute.location);
|
||||
}
|
||||
}
|
||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
||||
entry.bindingConversions = std::move(bindingConversions);
|
||||
entry.unsupportedAttribMask = unsupportedAttribMask;
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
||||
if (!entry.bindingDivisors.empty()) {
|
||||
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
|
||||
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
|
||||
entry.state.pNext = &entry.divisorState;
|
||||
} else {
|
||||
entry.state.pNext = nullptr;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
|
||||
out = BackendVertexInputState{};
|
||||
// The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing.
|
||||
out.ResetKeepingCapacity();
|
||||
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
|
||||
const auto& attr = elements.Attributes[location];
|
||||
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
|
||||
@@ -506,7 +147,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attr.IsBgra, attr.IsLong);
|
||||
auto conversion = VertexStreamConversion::None;
|
||||
if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) {
|
||||
const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
const auto* backend = MG_Backend::ApplyRoleBackend();
|
||||
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
|
||||
@@ -573,42 +214,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
||||
// reference into the current entry across its setup, and unordered_map
|
||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
||||
// from the VAO state (same hash, same content).
|
||||
constexpr Uint64 kSweepInterval = 256;
|
||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. 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
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
++m_evictionEpoch;
|
||||
#endif
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||
Bool isBgra, Bool isLong) {
|
||||
@@ -847,19 +452,12 @@ 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
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
// 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"
|
||||
@@ -76,66 +74,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
|
||||
// Back to a default-constructed state while keeping the vectors' storage, so a
|
||||
// per-draw scratch rebuilds without touching the heap. Every member is reset by the
|
||||
// default assignment; only the vectors' buffers are carried across it.
|
||||
void ResetKeepingCapacity() {
|
||||
const auto keep = [](auto& vector) {
|
||||
auto storage = Move(vector);
|
||||
storage.clear();
|
||||
return storage;
|
||||
};
|
||||
auto keptBindings = keep(bindings);
|
||||
auto keptAttributes = keep(attributes);
|
||||
auto keptBufferKeys = keep(bindingBufferKeys);
|
||||
auto keptBaseOffsets = keep(bindingBaseOffsets);
|
||||
auto keptLocations = keep(bindingAttributeLocations);
|
||||
auto keptClientMemory = keep(bindingUsesClientMemory);
|
||||
auto keptConversions = keep(bindingConversions);
|
||||
auto keptDivisors = keep(bindingDivisors);
|
||||
*this = BackendVertexInputState{};
|
||||
bindings = Move(keptBindings);
|
||||
attributes = Move(keptAttributes);
|
||||
bindingBufferKeys = Move(keptBufferKeys);
|
||||
bindingBaseOffsets = Move(keptBaseOffsets);
|
||||
bindingAttributeLocations = Move(keptLocations);
|
||||
bindingUsesClientMemory = Move(keptClientMemory);
|
||||
bindingConversions = Move(keptConversions);
|
||||
bindingDivisors = Move(keptDivisors);
|
||||
}
|
||||
};
|
||||
|
||||
#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;
|
||||
|
||||
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
||||
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
||||
// Readers that find the aux memo valid can use these without resolving the entry.
|
||||
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
||||
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
||||
}
|
||||
|
||||
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||
// is unchanged. Use this on per-draw paths.
|
||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#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);
|
||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
||||
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
||||
// not by luck: a recreated buffer can no longer land back on its dead
|
||||
// predecessor's key. Without eviction the map grows for the whole session.
|
||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||
// and the draw path's entry reference never spans a frame boundary, so
|
||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||
// compare except on sweep boundaries.
|
||||
void OnFrameBoundary();
|
||||
static SizeT GetComponentSize(DataType type);
|
||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
// an unknown/unsupported type.
|
||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||
#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
|
||||
// The same layout, kept on the record (MGPipeVertexElementsRecord::BackendMemo) and rebuilt
|
||||
// only when the record's configuration, the program's active mask, the buffer window or an
|
||||
// active attribute's buffer offset/stride/divisor moved - everything the build reads that
|
||||
// can change. Null where BuildWireVertexInput would return false.
|
||||
const BackendVertexInputState* ResolveWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state,
|
||||
Uint32 activeMask) const;
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||
@@ -144,85 +134,11 @@ 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
|
||||
// other than the erased one. The draw path (and the VAOs' state-pointer
|
||||
// memos) hold entry pointers across both; only the unique_ptr cell moves,
|
||||
// never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
||||
// their heap-allocated entry (stable across map insert/rehash by
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
//
|
||||
// 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
|
||||
// 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 std::atomic<Uint64> s_evictionEpochSource{0};
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
static inline thread_local 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,8 @@
|
||||
#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 {
|
||||
@@ -40,56 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool transformFeedbackUsageEnabled = false;
|
||||
};
|
||||
|
||||
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
|
||||
// Owned (refcounted) by the frontend BufferObject; the manager holds only weak
|
||||
// references (for shutdown) plus strong references on deferred-release lists.
|
||||
class VkBufferResource : public MG_State::GLState::BackendBufferResource {
|
||||
public:
|
||||
~VkBufferResource() override = default;
|
||||
|
||||
// Resident storage (may be invalid for streaming-only buffers).
|
||||
VkBufferObject buffer;
|
||||
VkDeviceSize storageSize = 0;
|
||||
VkBufferUsageFlags usageFlags = 0;
|
||||
// Frame serial of the last GPU reference; drives busy tracking.
|
||||
Uint64 lastUseSerial = 0;
|
||||
// Set when an immediate op could not be applied; forces a full re-upload
|
||||
// on the next AcquireResidentSlice.
|
||||
Bool pendingFullUpload = false;
|
||||
// Backs a zero-copy coherent persistent map (PipeResource GPU residency): the
|
||||
// buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is
|
||||
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
||||
Bool persistentMapped = false;
|
||||
|
||||
// Bumped from a manager-wide counter every time anything that decides which
|
||||
// BufferSlice an Acquire*Slice call hands back changes: storage created or
|
||||
// released, a full re-upload becoming due, a promotion/demotion between
|
||||
// resident and streamed storage, or a new per-frame arena slice. Callers that
|
||||
// memoise a resolved slice compare this to prove the memo still describes the
|
||||
// buffer. The counter is manager-wide (never per-resource) so a freshly
|
||||
// created resource - including one that replaces a destroyed resource at the
|
||||
// same address - can never reproduce a value some memo already holds. 0 means
|
||||
// "no slice has ever been handed out", which no memo can match.
|
||||
Uint64 sliceEpoch = 0;
|
||||
|
||||
// Cached transient (streaming) slice for the current frame.
|
||||
BufferSlice transientSlice{};
|
||||
Uint64 transientFrameSerial = 0;
|
||||
Uint64 transientChangeSerial = 0;
|
||||
VkDeviceSize transientSize = 0;
|
||||
|
||||
// Streaming re-copies the whole store into the per-frame arena on every
|
||||
// frame, which is right for genuinely per-frame data but pure waste for a
|
||||
// Dynamic-hinted buffer the app stopped touching. After the content
|
||||
// survives kStreamedPromotionStreak frame boundaries unchanged it is
|
||||
// promoted to resident storage (one final upload, then zero per-frame
|
||||
// cost); the first content change demotes it back to streaming, and the
|
||||
// streaming path's existing downgrade releases the resident store.
|
||||
Uint32 unchangedStreak = 0;
|
||||
Bool promotedResident = false;
|
||||
Uint64 promotedChangeSerial = 0;
|
||||
};
|
||||
|
||||
// Supplies a command buffer that is recording and outside any render pass,
|
||||
// for staged buffer-range copies. Implemented by VulkanRenderer.
|
||||
class IBufferCopyCommandProvider {
|
||||
@@ -103,7 +51,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
|
||||
@@ -137,6 +84,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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; }
|
||||
// P15 S0 (M4): busy writes that renamed their store instead of taking a staged copy.
|
||||
Uint64 GetWireRenameCount() const { return m_wireRenames; }
|
||||
|
||||
// Resource-op entry points. All run on the server apply owner.
|
||||
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
|
||||
@@ -153,9 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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);
|
||||
// P13 W4: monolith's record arm donates the wire store itself as the client's persistent
|
||||
// map (see the definition). Null when the store cannot be handed over.
|
||||
void* DonateWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes);
|
||||
// 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);
|
||||
@@ -174,6 +125,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// frameSerial-minus-frameCount inference.
|
||||
void NotifyFrameSerialComplete(Uint64 serial);
|
||||
void SetCopyCommandProvider(IBufferCopyCommandProvider* provider);
|
||||
// The owning renderer's submission counter: AcquireWireSlice stamps the next submission
|
||||
// from it directly rather than finding the renderer through the session-local slot on
|
||||
// every bind (a thread-local probe per vertex, index and uniform buffer of every draw).
|
||||
void SetSubmitCounterSource(const Uint64* submitCounter) { m_submitCounter = submitCounter; }
|
||||
|
||||
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
|
||||
VkDeviceSize alignment, BufferSlice& outSlice);
|
||||
@@ -199,59 +154,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
BufferSlice& outSlice);
|
||||
// Draw-time acquire for streamed buffers: uploads the whole shadow into
|
||||
// the per-frame arena (cached by change serial), releasing any resident
|
||||
// storage the buffer may still own.
|
||||
Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
BufferSlice& outSlice);
|
||||
|
||||
// Zero-copy persistent map (PipeResource GPU residency): create (once) a
|
||||
// HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage,
|
||||
// seed it from the shadow, and return its mapped base for the app to write into
|
||||
// directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow).
|
||||
void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject);
|
||||
|
||||
// Immediate ops, dispatched from the frontend BufferBackendOps table.
|
||||
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
|
||||
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
|
||||
void OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, DataPtr data);
|
||||
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> appAccess);
|
||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||
|
||||
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
||||
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
|
||||
// memo was taken means no buffer this manager owns changed which slice it hands
|
||||
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||
#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
|
||||
// P15 S0: the store's content state, as a process-wide monotonic serial: it moves on every
|
||||
// host write, respecify and GPU-write mark, and on every bind a shader may write through
|
||||
// (storage / texture-buffer slices). False for a store whose bytes the client writes
|
||||
// directly (an imported or donated store), which no cache may key on.
|
||||
Bool GetWireBufferContentSerial(MG_Pipe::MGPipeHandle res, Uint64& serial);
|
||||
// P15 S0: CONVERTED WIRE VERTEX STREAMS, KEPT ACROSS FRAMES (the frontend arm's
|
||||
// ConvertedVertexStream cache, lost with it, and longer-lived than it: that one was per
|
||||
// frame). Key: the source store's content serial - process-wide monotonic, so no store or
|
||||
// handle identity can alias - and the layout. Each entry owns a vertex buffer holding the
|
||||
// converted elements; a newer conversion of the same store and layout replaces it, and the
|
||||
// whole set is bounded by bytes (least recently used first). A replaced or evicted buffer
|
||||
// is released when the frame slot that last used it comes round again.
|
||||
struct ConvertedStreamKey {
|
||||
Uint64 contentSerial = 0;
|
||||
Uint64 baseOffset = 0;
|
||||
Uint32 stride = 0, type = 0, size = 0, conversion = 0;
|
||||
Bool normalized = false, isInteger = false, isBgra = false;
|
||||
Bool operator==(const ConvertedStreamKey&) const = default;
|
||||
};
|
||||
Bool LookupConvertedStream(const ConvertedStreamKey& key, SizeT elementCount, BufferSlice& outSlice);
|
||||
Bool StoreConvertedStream(const ConvertedStreamKey& key, MG_Pipe::MGPipeHandle source, SizeT elementCount,
|
||||
const void* bytes, SizeT size, BufferSlice& outSlice);
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
Uint64 GetCompletedSerial() const;
|
||||
// Busy = potentially referenced by GPU work that has not been fenced yet
|
||||
// (including commands recorded for the current, unsubmitted frame).
|
||||
Bool IsResourceBusy(const VkBufferResource& resource) const;
|
||||
// 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;
|
||||
@@ -262,6 +197,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
// P15 S0: see GetWireBufferContentSerial.
|
||||
Uint64 contentSerial = 0;
|
||||
// P15 S0 (M4): the store's bytes on the host, for a small store whose content only
|
||||
// host writes have made since it was specified - what a busy write renames from (see
|
||||
// WriteWireBuffer). Empty: no shadow (a large store, the budget was spent, or a GPU
|
||||
// write, shader-writable bind or client mapping made the host copy unknowable).
|
||||
Vector<Uint8> shadow;
|
||||
// P15: buffers this store was renamed away from, kept for the next rename instead of
|
||||
// allocating one (a UI store is renamed several times a frame). A spare is reused only
|
||||
// once the submission that can name it has completed - DeferWireRelease's own rule -
|
||||
// and its handle never died, so no handle-keyed memo can mistake it for another
|
||||
// buffer. Bounded; the rest go through DeferWireRelease.
|
||||
struct RenameSpare {
|
||||
VkBufferObject buffer;
|
||||
Uint64 submitIndex = 0;
|
||||
Uint64 lastUseSerial = 0;
|
||||
};
|
||||
Vector<RenameSpare> renameSpares;
|
||||
Bool gpuWritesPending = false;
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
@@ -275,6 +228,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
// P8-D: see GetWireHostWaitCount.
|
||||
Uint64 m_wireHostWaits = 0;
|
||||
Uint64 m_wireRenames = 0;
|
||||
SizeT m_wireShadowBytes = 0;
|
||||
void DropWireShadow(WireBufferResource& resource);
|
||||
void ReleaseRenameSpares(WireBufferResource& resource);
|
||||
Bool CreateWireStoreBuffer(WireBufferResource& resource);
|
||||
Bool RenameBusyWireStore(WireBufferResource& resource);
|
||||
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
|
||||
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
|
||||
}
|
||||
@@ -332,8 +291,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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.
|
||||
// the destroy cannot match after it. Deliberately not bumped by WriteWireBuffer, which
|
||||
// would defeat the memo on every glBufferSubData.
|
||||
struct DeferredWireRelease {
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseSerial = 0;
|
||||
@@ -371,29 +330,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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);
|
||||
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
|
||||
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
|
||||
VkMemoryPropertyFlags requiredFlags = 0);
|
||||
// Swap storage (conditional orphan) and refill it from the shadow copy.
|
||||
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
|
||||
// Record a staging-slice copy into the resident storage, ordered against
|
||||
// in-flight and already-recorded GPU work.
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, const void* data,
|
||||
SizeT offset, SizeT size);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size);
|
||||
#endif
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyAllDeferredReleases();
|
||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||
void ReleaseAllLiveResources();
|
||||
// See VkBufferResource::sliceEpoch.
|
||||
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
|
||||
|
||||
VkBufferManagerInitInfo m_initInfo{};
|
||||
BufferArena m_transientUploadArena;
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
@@ -402,11 +342,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
const Uint64* m_submitCounter = nullptr;
|
||||
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;
|
||||
// P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and
|
||||
// uniform buffer). An entry is believed only for the generation it was taken at ({slot,
|
||||
// gen} is never reused), and the erase of that node clears it: a node never moves until
|
||||
// erased. Slot-indexed rather than direct-mapped because a Minecraft frame draws from a
|
||||
// few thousand live buffers, which a small hashed table mostly missed.
|
||||
struct WireBufferLookup {
|
||||
Uint32 gen = 0;
|
||||
WireBufferResource* resource = nullptr;
|
||||
};
|
||||
static constexpr SizeT kWireBufferLookupMaxSlots = SizeT{1} << 20;
|
||||
Vector<WireBufferLookup> m_wireBufferLookup;
|
||||
void ForgetWireBufferLookup(Uint64 key) {
|
||||
const SizeT slot = static_cast<SizeT>(key & 0xffffffffu);
|
||||
if (slot < m_wireBufferLookup.size()) m_wireBufferLookup[slot] = {};
|
||||
}
|
||||
// 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;
|
||||
@@ -421,18 +374,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
// so that 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;
|
||||
Uint64 m_wireContentSerial = 0;
|
||||
struct ConvertedStream {
|
||||
ConvertedStreamKey key;
|
||||
MG_Pipe::MGPipeHandle source = MG_Pipe::kMGPipeNullHandle;
|
||||
SizeT elementCount = 0;
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseFrameSerial = 0;
|
||||
};
|
||||
Vector<ConvertedStream> m_convertedStreams;
|
||||
SizeT m_convertedStreamBytes = 0;
|
||||
void RetireConvertedStream(SizeT index);
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
Uint64 m_frameSerial = 1;
|
||||
Uint64 m_completedSerialFloor = 0;
|
||||
// Never reset (not even by Shutdown): a value handed to a resource must stay
|
||||
// unique for the process, or a memo taken before a re-initialize could match
|
||||
// a different resource's state after it.
|
||||
Uint64 m_sliceEpochCounter = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#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;
|
||||
@@ -36,7 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_externalCoherent = coherent;
|
||||
m_externalAhb = ahb;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
|
||||
m_allocator = other.m_allocator;
|
||||
@@ -44,9 +42,7 @@ 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;
|
||||
@@ -67,9 +63,7 @@ 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;
|
||||
@@ -170,10 +164,8 @@ 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,7 +192,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};
|
||||
@@ -209,7 +200,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,7 +224,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};
|
||||
@@ -243,7 +232,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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) {
|
||||
|
||||
@@ -65,13 +65,10 @@ 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
|
||||
@@ -82,7 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
|
||||
private:
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
@@ -90,12 +86,10 @@ 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,7 @@
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
@@ -55,7 +55,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_Pipe::gPipeInputs->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
|
||||
@@ -68,43 +68,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
toLinear(payload.color.z()), payload.color.w());
|
||||
}
|
||||
|
||||
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
|
||||
switch (payload.colorEncoding) {
|
||||
case ClearColorEncoding::Int:
|
||||
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
|
||||
break;
|
||||
case ClearColorEncoding::Uint:
|
||||
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
|
||||
break;
|
||||
case ClearColorEncoding::Float:
|
||||
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
|
||||
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) {
|
||||
if (!IsCubeMapFaceUploadTarget(target)) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return 0;
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
}
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
@@ -113,425 +76,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
if (attachmentType == FramebufferAttachmentType::None) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& attachment = drawFbo.GetAttachment(attachmentType);
|
||||
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
.mipLevel = mipLevel,
|
||||
.baseArrayLayer = baseArrayLayer,
|
||||
.layerCount = layerCount,
|
||||
};
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
MOBILEGL_ASSERT(attachment.IsTexture() && !attachment.IsRenderbuffer(),
|
||||
"MakePendingClearKey requires a texture framebuffer attachment");
|
||||
auto* texture = attachment.GetTexture().get();
|
||||
MOBILEGL_ASSERT(texture != nullptr, "MakePendingClearKey: texture attachment resolved to null");
|
||||
const Uint32 mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
|
||||
const Uint32 layerCount = ResolveAttachmentLayerCount(attachment);
|
||||
return MakePendingClearKey(texture, mipLevel, baseArrayLayer, layerCount);
|
||||
}
|
||||
|
||||
Bool VkClearManager::Initialize() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void VkClearManager::Shutdown() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
};
|
||||
}
|
||||
|
||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||
dst.mask |= src.mask;
|
||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
// The whole colour story travels together (same rule as
|
||||
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||
// payload carries its value in colorInt/colorUint and its branch selector
|
||||
// in colorEncoding - dropping them here would leave the pending clear
|
||||
// reading as an all-zero float one.
|
||||
dst.color = src.color;
|
||||
dst.colorEncoding = src.colorEncoding;
|
||||
dst.colorInt = src.colorInt;
|
||||
dst.colorUint = src.colorUint;
|
||||
}
|
||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
dst.depth = src.depth;
|
||||
}
|
||||
if ((src.mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
dst.stencil = src.stencil;
|
||||
}
|
||||
}
|
||||
|
||||
void VkClearManager::ErasePendingClearsForTextureLocked(const TextureIdentity& identity) {
|
||||
Vector<PendingClearKey> keysToErase;
|
||||
keysToErase.reserve(m_pendingClears.size());
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
if (PendingClearMatchesTextureIdentity(it->first, identity)) {
|
||||
keysToErase.emplace_back(it->first);
|
||||
}
|
||||
}
|
||||
for (const auto& key : keysToErase) {
|
||||
m_pendingClears.erase(key);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
outTexture.reset();
|
||||
if (identity.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto aliveIt = m_aliveObjects.find(identity);
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
ErasePendingClearsForTextureLocked(identity);
|
||||
return false;
|
||||
}
|
||||
|
||||
outTexture = aliveIt->second.lock();
|
||||
if (!outTexture || outTexture.get() != identity.texture || outTexture->GetLifetimeId() != identity.lifetimeId) {
|
||||
ErasePendingClearsForTextureLocked(identity);
|
||||
outTexture.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkClearManager::LockTextureLocked(const PendingClearKey& key,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
return LockTextureIdentityLocked(TextureIdentity{
|
||||
.texture = key.texture,
|
||||
.lifetimeId = key.textureLifetimeId,
|
||||
}, outTexture);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo) {
|
||||
if (mask & GL_COLOR_BUFFER_BIT) {
|
||||
auto& drawbufs = drawFbo.GetDrawBuffers();
|
||||
// This should automatically work on default & offscreen FBO
|
||||
for (auto drawbuf: drawbufs) {
|
||||
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
|
||||
if (!attachment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QueueClear({
|
||||
.mask = GL_COLOR_BUFFER_BIT,
|
||||
.color = clearPayload.color
|
||||
}, *attachment);
|
||||
|
||||
MGLOG_D("%s: %s (texture %d) - color = (%.2f, %.2f, %.2f, %.2f)", __func__,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(drawbuf).c_str(),
|
||||
attachment->GetTexture()->GetExternalIndex(),
|
||||
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
|
||||
}
|
||||
}
|
||||
|
||||
if (mask & GL_DEPTH_BUFFER_BIT) {
|
||||
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Depth);
|
||||
if (attachment) {
|
||||
QueueClear({
|
||||
.mask = GL_DEPTH_BUFFER_BIT,
|
||||
.depth = clearPayload.depth,
|
||||
}, *attachment);
|
||||
|
||||
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
|
||||
attachment->GetTexture()->GetExternalIndex(), clearPayload.depth);
|
||||
}
|
||||
}
|
||||
|
||||
if (mask & GL_STENCIL_BUFFER_BIT) {
|
||||
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Stencil);
|
||||
if (attachment) {
|
||||
QueueClear({
|
||||
.mask = GL_STENCIL_BUFFER_BIT,
|
||||
.stencil = clearPayload.stencil,
|
||||
}, *attachment);
|
||||
|
||||
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
|
||||
attachment->GetTexture()->GetExternalIndex(), clearPayload.stencil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
|
||||
if (clearPayload.mask == 0 || !texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (clearPayload.mask == 0 || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
|
||||
return;
|
||||
}
|
||||
const auto texture = attachment.GetTexture();
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
return LockTextureLocked(it->first, liveTexture);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(const PendingClearKey& key) {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_pendingClears.find(key) == m_pendingClears.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
return LockTextureLocked(key, liveTexture);
|
||||
}
|
||||
|
||||
Bool VkClearManager::HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
|
||||
return false;
|
||||
}
|
||||
return HasPendingClear(MakePendingClearKey(attachment));
|
||||
}
|
||||
|
||||
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload) {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
return GetPendingClear(key, outPayload, liveTexture);
|
||||
}
|
||||
|
||||
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
if (key.texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!LockTextureLocked(key, outTexture)) {
|
||||
return false;
|
||||
}
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it == m_pendingClears.end()) {
|
||||
outTexture.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
outPayload = it->second;
|
||||
MGLOG_D("%s: Got pending clear for texture@%p lifetime=%llu, mip=%u layer=%u count=%u mask=0x%x clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
|
||||
static_cast<void*>(key.texture),
|
||||
static_cast<unsigned long long>(key.textureLifetimeId),
|
||||
key.mipLevel, key.baseArrayLayer, key.layerCount,
|
||||
static_cast<Uint32>(outPayload.mask),
|
||||
outPayload.color[0], outPayload.color[1], outPayload.color[2], outPayload.color[3],
|
||||
outPayload.depth,
|
||||
outPayload.stencil);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkClearManager::GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
ClearAttachmentPayload& outPayload) {
|
||||
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
|
||||
MGLOG_D("%s: Failed getting pending clear for non-texture framebuffer attachment", __func__);
|
||||
return false;
|
||||
}
|
||||
return GetPendingClear(MakePendingClearKey(attachment), outPayload);
|
||||
}
|
||||
|
||||
Bool VkClearManager::GetPendingClears(MG_State::GLState::ITextureObject* texture,
|
||||
Vector<PendingClearEntry>& outEntries) {
|
||||
outEntries.clear();
|
||||
if (texture == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
if (!LockTextureIdentityLocked(MakeTextureIdentity(texture), liveTexture)) {
|
||||
return false;
|
||||
}
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
|
||||
outEntries.emplace_back(PendingClearEntry{.key = it->first, .payload = it->second});
|
||||
}
|
||||
}
|
||||
return !outEntries.empty();
|
||||
}
|
||||
|
||||
void VkClearManager::PopPendingClear(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
|
||||
return; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
const TextureIdentity identity = MakeTextureIdentity(texture);
|
||||
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
ErasePendingClearsForTextureLocked(identity);
|
||||
}
|
||||
|
||||
void VkClearManager::PopPendingClear(const PendingClearKey& key) {
|
||||
if (key.texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_pendingClears.find(key);
|
||||
if (it != m_pendingClears.end()) {
|
||||
m_pendingClears.erase(it);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
MGLOG_D("%s: Pop pending clear for texture@%p lifetime=%llu mip=%u layer=%u count=%u", __func__,
|
||||
static_cast<void*>(key.texture), static_cast<unsigned long long>(key.textureLifetimeId),
|
||||
key.mipLevel, key.baseArrayLayer, key.layerCount);
|
||||
}
|
||||
|
||||
void VkClearManager::PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
|
||||
return;
|
||||
}
|
||||
PopPendingClear(MakePendingClearKey(attachment));
|
||||
}
|
||||
|
||||
SizeT VkClearManager::CollectGarbage() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_gcCounter++;
|
||||
if (m_gcCounter != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Vector<TextureIdentity> expiredTextures;
|
||||
expiredTextures.reserve(m_aliveObjects.size());
|
||||
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
|
||||
if (it->second.expired()) {
|
||||
expiredTextures.emplace_back(it->first);
|
||||
}
|
||||
}
|
||||
if (expiredTextures.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const auto& identity : expiredTextures) {
|
||||
ErasePendingClearsForTextureLocked(identity);
|
||||
}
|
||||
return expiredTextures.size();
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -46,10 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// expressed in whichever type matches (GL 4.6 core 15.2.3).
|
||||
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
|
||||
|
||||
// Applies that same rule in place, for the paths that have to bake it into the payload before
|
||||
// the destination is known.
|
||||
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
|
||||
|
||||
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
|
||||
// function to whatever value it is handed. Every other write path in this backend goes through
|
||||
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
|
||||
@@ -59,111 +55,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// encoding itself.
|
||||
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
|
||||
|
||||
struct PendingClearKey {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint32 mipLevel = 0;
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 layerCount = 1;
|
||||
|
||||
Bool operator==(const PendingClearKey& other) const {
|
||||
return texture == other.texture && textureLifetimeId == other.textureLifetimeId &&
|
||||
mipLevel == other.mipLevel &&
|
||||
baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
|
||||
Bool operator==(const TextureIdentity& other) const {
|
||||
return texture == other.texture && lifetimeId == other.lifetimeId;
|
||||
}
|
||||
};
|
||||
|
||||
struct PendingClearEntry {
|
||||
PendingClearKey key{};
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
struct PendingClearKeyHash {
|
||||
SizeT operator()(const PendingClearKey& key) const {
|
||||
const SizeT textureHash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
const SizeT textureLifetimeHash = std::hash<Uint64>{}(key.textureLifetimeId);
|
||||
const SizeT mipHash = std::hash<Uint32>{}(key.mipLevel);
|
||||
const SizeT layerHash = std::hash<Uint32>{}(key.baseArrayLayer);
|
||||
const SizeT layerCountHash = std::hash<Uint32>{}(key.layerCount);
|
||||
SizeT hash = textureHash;
|
||||
hash ^= textureLifetimeHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= mipHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= layerHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= layerCountHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextureIdentityHash {
|
||||
SizeT operator()(const TextureIdentity& key) const {
|
||||
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
|
||||
void QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueClear(
|
||||
const ClearAttachmentPayload& clearPayload,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture);
|
||||
void QueueClear(const ClearAttachmentPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
Bool HasPendingClear(MG_State::GLState::ITextureObject* texture);
|
||||
Bool HasPendingClear(const PendingClearKey& key);
|
||||
Bool HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload);
|
||||
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
Bool GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
ClearAttachmentPayload& outPayload);
|
||||
Bool GetPendingClears(MG_State::GLState::ITextureObject* texture, Vector<PendingClearEntry>& outEntries);
|
||||
void PopPendingClear(MG_State::GLState::ITextureObject* texture);
|
||||
void PopPendingClear(const PendingClearKey& key);
|
||||
void PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
SizeT CollectGarbage();
|
||||
private:
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
static void MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src);
|
||||
void ErasePendingClearsForTextureLocked(const TextureIdentity& identity);
|
||||
Bool LockTextureIdentityLocked(const TextureIdentity& identity,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
Bool LockTextureLocked(const PendingClearKey& key,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
public:
|
||||
// Lock-free probe for the consecutive-draw fast path: any pending clear
|
||||
// forces the full SetupDraw path (which materializes/consumes it).
|
||||
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
|
||||
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
|
||||
// read it before taking the lock: during draw batches the pending set
|
||||
// is almost always empty, so this turns several locked map probes per
|
||||
// draw into one relaxed load.
|
||||
std::atomic<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,40 +20,6 @@
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class TrackedAttachmentTarget : Uint8 {
|
||||
Texture,
|
||||
Renderbuffer,
|
||||
SwapchainColor,
|
||||
SwapchainDepthStencil
|
||||
};
|
||||
|
||||
struct PendingClearAttachmentInfo {
|
||||
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
|
||||
Uint32 attachmentIndex = 0;
|
||||
// Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
|
||||
// draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
|
||||
// Only meaningful for color clears.
|
||||
Uint32 colorAttachmentSlot = 0;
|
||||
PendingClearKey key{};
|
||||
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
|
||||
Bool hasInlinePayload = false;
|
||||
ClearAttachmentPayload inlinePayload{};
|
||||
};
|
||||
|
||||
struct TrackedAttachmentLayoutInfo {
|
||||
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
|
||||
WeakPtr<MG_State::GLState::ITextureObject> texture;
|
||||
// Identity-compare shortcut for the per-draw "does the active pass use
|
||||
// this sampled texture" probe: comparing this against a LIVE texture's
|
||||
// address needs no weak_ptr::lock (two refcount atomics per probe).
|
||||
// May dangle once the texture dies - compare only, never dereference.
|
||||
MG_State::GLState::ITextureObject* textureRaw = nullptr;
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
Uint32 textureMipLevel = 0;
|
||||
Uint32 swapchainImageIndex = 0;
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
|
||||
struct DepthStencilAttachmentLoadInfo {
|
||||
VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
@@ -68,13 +34,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct RenderPassEntry {
|
||||
// Per Magma session (MagmaSession.h): each session's renderer is its own device.
|
||||
static inline SessionLocal<VkDevice> s_device;
|
||||
static inline thread_local Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
|
||||
Uint64 hash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
Uint64 compatibilityHash = 0;
|
||||
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
|
||||
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
|
||||
Uint32 attachmentCount = 0;
|
||||
Uint32 colorAttachmentCount = 0;
|
||||
Bool hasDepthStencilAttachment = false;
|
||||
@@ -84,6 +47,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 layers = 1;
|
||||
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
|
||||
Uint64 lastUsedFrame = 0;
|
||||
// P15 S0 (wire passes only): what the render pass was created from, and the render
|
||||
// passes that differ from it only in loading some attachments with LOAD_OP_CLEAR. Load ops
|
||||
// are not part of render-pass compatibility, so a variant begins with this entry's own
|
||||
// framebuffer and runs every pipeline created against `renderPass`.
|
||||
Vector<VkAttachmentDescription> wireAttachments;
|
||||
Vector<VkAttachmentReference> wireColors;
|
||||
VkAttachmentReference wireDepth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
|
||||
Vector<std::pair<Uint64, VkRenderPass>> clearVariants;
|
||||
|
||||
RenderPassEntry() = default;
|
||||
RenderPassEntry(const RenderPassEntry&) = delete;
|
||||
@@ -92,8 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(renderPass, that.renderPass);
|
||||
std::swap(framebuffer, that.framebuffer);
|
||||
std::swap(compatibilityHash, that.compatibilityHash);
|
||||
std::swap(pendingClearAttachments, that.pendingClearAttachments);
|
||||
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
|
||||
std::swap(attachmentCount, that.attachmentCount);
|
||||
std::swap(colorAttachmentCount, that.colorAttachmentCount);
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
@@ -101,6 +70,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
std::swap(wireAttachments, that.wireAttachments);
|
||||
std::swap(wireColors, that.wireColors);
|
||||
std::swap(wireDepth, that.wireDepth);
|
||||
std::swap(clearVariants, that.clearVariants);
|
||||
}
|
||||
// Move ASSIGNMENT, not just construction. The move constructor above and the
|
||||
// destructor below each independently suppress the implicit one, which left the
|
||||
@@ -126,8 +99,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(renderPass, that.renderPass);
|
||||
std::swap(framebuffer, that.framebuffer);
|
||||
std::swap(compatibilityHash, that.compatibilityHash);
|
||||
std::swap(pendingClearAttachments, that.pendingClearAttachments);
|
||||
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
|
||||
std::swap(attachmentCount, that.attachmentCount);
|
||||
std::swap(colorAttachmentCount, that.colorAttachmentCount);
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
@@ -135,36 +106,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
std::swap(wireAttachments, that.wireAttachments);
|
||||
std::swap(wireColors, that.wireColors);
|
||||
std::swap(wireDepth, that.wireDepth);
|
||||
std::swap(clearVariants, that.clearVariants);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
RenderPassEntry(
|
||||
Uint64 hash,
|
||||
VkRenderPass renderpass,
|
||||
VkFramebuffer framebuffer,
|
||||
Uint64 compatibilityHash,
|
||||
const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
|
||||
const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
|
||||
Uint32 attachmentCount,
|
||||
Uint32 colorAttachmentCount,
|
||||
Bool hasDepthStencilAttachment,
|
||||
VkSampleCountFlagBits sampleCount,
|
||||
IntVec2 extent, Uint32 layers):
|
||||
hash(hash),
|
||||
renderPass(renderpass),
|
||||
framebuffer(framebuffer),
|
||||
compatibilityHash(compatibilityHash),
|
||||
pendingClearAttachments(Move(pendingClearAttachments)),
|
||||
trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
|
||||
attachmentCount(attachmentCount),
|
||||
colorAttachmentCount(colorAttachmentCount),
|
||||
hasDepthStencilAttachment(hasDepthStencilAttachment),
|
||||
sampleCount(sampleCount),
|
||||
extent(extent),
|
||||
layers(layers)
|
||||
{}
|
||||
|
||||
~RenderPassEntry() {
|
||||
for (const auto& variant : clearVariants)
|
||||
if (variant.second != VK_NULL_HANDLE) vkDestroyRenderPass(s_device.Get(), variant.second, nullptr);
|
||||
if (renderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
|
||||
}
|
||||
@@ -186,7 +137,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 hash = 0;
|
||||
Uint64 compatibilityHash = 0;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
|
||||
IntVec2 extent = {0, 0};
|
||||
|
||||
Bool CompatibleWith(const RenderPassEntry& that) const {
|
||||
@@ -202,85 +152,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
||||
// everything keyed on them before any new pass can be created (the sweep and
|
||||
// the notification run back-to-back with no creation in between; observers
|
||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
||||
// scan, not one per dying pass. The wholesale paths
|
||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
||||
// every pipeline outright.
|
||||
class IEvictionObserver {
|
||||
public:
|
||||
virtual ~IEvictionObserver() = default;
|
||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
||||
};
|
||||
|
||||
VkRenderPassManager(VkDevice device,
|
||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||
VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||
~VkRenderPassManager();
|
||||
|
||||
// Observer may be null (no notifications). Not owned.
|
||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
// The surface target the default framebuffer currently names (VulkanRenderer's surface
|
||||
// targets): a default-framebuffer pass of one surface is not another's.
|
||||
void SetDefaultFramebufferTarget(Uint64 serial) {
|
||||
m_defaultFramebufferTarget = serial;
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
// Destroys every cached pass and framebuffer (not the renderbuffers). The device must be
|
||||
// idle and no pass may be open: a surface target's image views are going away.
|
||||
void PurgeRenderPasses();
|
||||
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool includePendingClear = true,
|
||||
Bool includeDefaultFboDepthStencil = true);
|
||||
// drawUsesDepthStencil: whether the operation about to run inside the pass
|
||||
// reads or writes the depth/stencil buffer (depth test or stencil test
|
||||
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
|
||||
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
|
||||
// default-FBO pass whose draws provably never touch depth/stencil is
|
||||
// created WITHOUT the depth attachment - on a tiler that skips the whole
|
||||
// depth tile load AND store. The flavor only escalates: once a pass with
|
||||
// depth is active, later depth-less draws keep using it, and a depth-using
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
|
||||
// Frame boundary hook: ages the render-pass cache and evicts long-unused
|
||||
// entries (their command buffers retired many frames ago).
|
||||
void OnPresent();
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
|
||||
// P15 S0: begins `renderPassEntry` through `renderPass` (one of its clear-load variants)
|
||||
// with these clear values; it is active as the entry itself, so a draw continues it.
|
||||
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry,
|
||||
VkRenderPass renderPass, const VkClearValue* clearValues, Uint32 clearValueCount);
|
||||
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
|
||||
static ActiveRenderPassInfo* GetActiveRenderPass();
|
||||
private:
|
||||
@@ -288,13 +176,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkClearManager& m_clearManager;
|
||||
VkTextureManager& m_textureManager;
|
||||
SwapchainObject& m_swapchainObject;
|
||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
|
||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||
@@ -308,127 +191,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
|
||||
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
|
||||
// framebuffer state is provably unchanged since the last resolution, the active render pass
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
// version change, swapchain rotation, any attachment image recreation (the two epochs),
|
||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||
Uint64 m_defaultFramebufferTarget = 0;
|
||||
Bool m_rpFastValid = false;
|
||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||
// the same number of version bumps would otherwise compare equal (both count
|
||||
// from 0), serving the dead framebuffer's pass to the new object.
|
||||
Uint64 m_rpFastFboLifetimeId = 0;
|
||||
Uint16 m_rpFastFboVersion = 0;
|
||||
Uint32 m_rpFastSwapchainIndex = 0;
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
Uint64 m_rpFastRbEpoch = 0;
|
||||
Uint64 m_rpFastRenderPassHash = 0;
|
||||
// Whether the memoized entry carries a depth/stencil attachment; a
|
||||
// default-FBO resolution whose effective depth request differs must
|
||||
// miss the memo (the depth-less/depth-full flavors hash differently).
|
||||
Bool m_rpFastHadDepthStencil = false;
|
||||
|
||||
public:
|
||||
struct RenderbufferResource {
|
||||
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
||||
// expired. Dead resources age past every in-flight frame before Destroy
|
||||
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
||||
// for frames-in-flight frames after the GL object dies.
|
||||
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
||||
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
// UNORM reinterpretation of an sRGB image, used as the attachment view while
|
||||
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
VkExtent2D extent = {0, 0};
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
Int samples = 0;
|
||||
// m_frameCounter value at which the weak reference was first seen expired.
|
||||
Uint64 deadSinceFrame = kNeverObservedDead;
|
||||
|
||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||
};
|
||||
|
||||
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
|
||||
// attachments the same way texture attachments go through the texture manager.
|
||||
RenderbufferResource* GetOrCreateRenderbufferResource(
|
||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
|
||||
ClearAttachmentPayload& outPayload) const;
|
||||
|
||||
private:
|
||||
struct PendingRenderbufferClear {
|
||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||
ClearAttachmentPayload payload{};
|
||||
};
|
||||
|
||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
||||
// until enough frame boundaries have passed that no in-flight command buffer
|
||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
||||
struct DeferredRenderbufferRelease {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView unormTwinView = VK_NULL_HANDLE;
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
|
||||
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||
// materializes the source's pending clear, which looks that same resource up again. Growing
|
||||
// an open-addressed table relocates every element, so the cached pointer went on to name
|
||||
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
|
||||
// out at "source image layout is undefined", silently dropping the blit -
|
||||
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
|
||||
// iterations that grew the table.
|
||||
//
|
||||
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||
// ordering rule. m_textureResources is node-based for the same reason.
|
||||
//
|
||||
// The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
|
||||
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
|
||||
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
|
||||
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
|
||||
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
|
||||
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
|
||||
// That narrows the insert hazard; it does not touch the erase one.)
|
||||
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
|
||||
|
||||
Bool HasPendingRenderbufferClear(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||
void CollectRenderbufferGarbage();
|
||||
// Frame-boundary margin after which a resource last referenced by a retired
|
||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
||||
Uint64 RetireAgeFrames() const;
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
// Per Magma session (MagmaSession.h): one session's open render pass and managers are not
|
||||
// another's.
|
||||
static inline SessionLocal<ActiveRenderPassInfo> s_activeRenderPass;
|
||||
static inline SessionLocal<Bool> s_hasActiveRenderPass;
|
||||
static inline SessionLocal<VkClearManager*> s_clearManager;
|
||||
static inline SessionLocal<VkTextureManager*> s_textureManager;
|
||||
static inline SessionLocal<SwapchainObject*> s_swapchainObject;
|
||||
static inline SessionLocal<VkRenderPassManager*> s_renderPassManager;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
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.
|
||||
@@ -43,9 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
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 +217,16 @@ 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 +237,8 @@ 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 +259,8 @@ 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;
|
||||
@@ -294,6 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkSamplerManager::Shutdown() {
|
||||
++m_destroyEpoch;
|
||||
for (auto& [_, sampler] : m_samplers) {
|
||||
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, sampler.handle, nullptr);
|
||||
@@ -329,6 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
++m_destroyEpoch;
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
@@ -339,65 +321,58 @@ 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");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
|
||||
XXH64_state_t* const hashState = m_hashState;
|
||||
XXHASH_VERIFY(XXH64_reset(hashState, m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &singleLevelView, sizeof(singleLevelView)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &minFilter, sizeof(minFilter)));
|
||||
const auto magFilter = sampler.GetMagFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &magFilter, sizeof(magFilter)));
|
||||
const auto mipmapMode = sampler.GetMipmapMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &mipmapMode, sizeof(mipmapMode)));
|
||||
const auto wrapS = sampler.GetWrapS();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &wrapS, sizeof(wrapS)));
|
||||
const auto wrapT = sampler.GetWrapT();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &wrapT, sizeof(wrapT)));
|
||||
const auto wrapR = sampler.GetWrapR();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &wrapR, sizeof(wrapR)));
|
||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &minLod, sizeof(minLod)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &maxLod, sizeof(maxLod)));
|
||||
const auto lodBias = sampler.GetLodBias();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &lodBias, sizeof(lodBias)));
|
||||
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
|
||||
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
|
||||
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &compareFunc, sizeof(compareFunc)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
XXHASH_VERIFY(XXH64_update(hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
return XXH64_digest(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 +384,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,19 +444,11 @@ 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 +505,9 @@ 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 +515,6 @@ 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;
|
||||
|
||||
@@ -40,18 +40,10 @@ public:
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
// viewLevelCount is the mip-level count of the image view this sampler will be paired
|
||||
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false,
|
||||
Uint32 viewLevelCount = 0);
|
||||
#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
|
||||
@@ -64,6 +56,12 @@ public:
|
||||
// fence wait. Self-gated: one counter bump and compare except on sweep
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
// For a caller that memoises a returned VkSampler (UniformManager's wire sampler memo): the
|
||||
// frame-boundary count a GetOrCreate call stamps, and a counter that moves whenever any cached
|
||||
// sampler is destroyed. A memo may reuse a handle only while the destroy epoch is unchanged,
|
||||
// and must still call GetOrCreate once per frame boundary so the entry stays stamped as used.
|
||||
Uint64 FrameBoundaryCount() const { return m_frameBoundaryCounter; }
|
||||
Uint64 DestroyEpoch() const { return m_destroyEpoch; }
|
||||
|
||||
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
|
||||
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
|
||||
@@ -92,37 +90,24 @@ 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;
|
||||
@@ -138,6 +123,8 @@ private:
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
// Moves on every vkDestroySampler of a cached entry (sweep and Shutdown).
|
||||
Uint64 m_destroyEpoch = 0;
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,20 +19,16 @@
|
||||
#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;
|
||||
@@ -112,54 +108,36 @@ inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAt
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
|
||||
// storage texture's (VkTextureManager::StorageTextureOf).
|
||||
//
|
||||
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
|
||||
// GetOrCreate*View entry point below expects values that have already been through here, and so
|
||||
// does everything that reads or copies an attachment directly. Both are identity on a plain
|
||||
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
|
||||
// and there is no second, view-only code path to keep in step.
|
||||
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
|
||||
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
|
||||
}
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||
// manager keys its per-draw fast path on this so an attachment's image recreation
|
||||
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
|
||||
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
|
||||
// P15 (wire deferred clears): while one of these lives, SyncTextureResourceByHandle leaves an
|
||||
// image's pending clears pending - its caller is resolving the image as an ATTACHMENT, and
|
||||
// either folds the clears into the pass it begins (LOAD_OP_CLEAR) or replaces them with a
|
||||
// newer clear. Every other caller (sampling, blits, copies, readbacks, uploads, mip
|
||||
// generation) materializes them first, so a use that forgets the hold is merely slower.
|
||||
class PendingClearHold {
|
||||
public:
|
||||
explicit PendingClearHold(VkTextureManager& manager) : m_manager(manager) { ++m_manager.m_pendingClearHold; }
|
||||
~PendingClearHold() { --m_manager.m_pendingClearHold; }
|
||||
PendingClearHold(const PendingClearHold&) = delete;
|
||||
PendingClearHold& operator=(const PendingClearHold&) = delete;
|
||||
private:
|
||||
VkTextureManager& m_manager;
|
||||
};
|
||||
// A whole-subresource clear that was recorded but has not run yet (VulkanRenderer::DeferWireClear).
|
||||
struct WirePendingClear {
|
||||
Uint32 level = 0, layer = 0, layers = 1;
|
||||
VkImageAspectFlags aspects = 0;
|
||||
VkClearValue value{};
|
||||
};
|
||||
// Bumped whenever any tracked texture resource is erased; cached
|
||||
// TextureResource pointers are valid only while this is unchanged.
|
||||
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
|
||||
|
||||
struct TextureIdentity {
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
|
||||
Bool operator==(const TextureIdentity& other) const {
|
||||
return texture == other.texture && lifetimeId == other.lifetimeId;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextureIdentityHash {
|
||||
SizeT operator()(const TextureIdentity& key) const {
|
||||
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
struct InitInfo {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
||||
@@ -355,7 +333,6 @@ public:
|
||||
// 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;
|
||||
// SHARED IMAGES: level 0 is a server shared image (an imported AHardwareBuffer), not a
|
||||
// VMA allocation. The resource owns the image and its dedicated imported memory, and
|
||||
@@ -381,7 +358,9 @@ public:
|
||||
SharedPtr<const void> yuvOwner;
|
||||
SharedPtr<void> yuvSource;
|
||||
Uint64 yuvConvertedFrame = 0;
|
||||
#endif
|
||||
// P15: clears of this image still to run (WirePendingClear). Part of the image, so a
|
||||
// respecify (which moves a new image in, below) drops them with the old one.
|
||||
Vector<WirePendingClear> pendingClears;
|
||||
|
||||
TextureResource() = default;
|
||||
TextureResource(const TextureResource&) = delete;
|
||||
@@ -415,7 +394,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);
|
||||
std::swap(this->importedMemory, that.importedMemory);
|
||||
std::swap(this->sharedImageId, that.sharedImageId);
|
||||
@@ -428,7 +406,7 @@ public:
|
||||
std::swap(this->yuvOwner, that.yuvOwner);
|
||||
std::swap(this->yuvSource, that.yuvSource);
|
||||
std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame);
|
||||
#endif
|
||||
std::swap(this->pendingClears, that.pendingClears);
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
@@ -466,7 +444,6 @@ public:
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(s_allocator.Get(), image, allocation);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (importedMemory != VK_NULL_HANDLE) {
|
||||
if (image != VK_NULL_HANDLE) vkDestroyImage(s_device.Get(), image, nullptr);
|
||||
vkFreeMemory(s_device.Get(), importedMemory, nullptr);
|
||||
@@ -482,7 +459,6 @@ public:
|
||||
yuvOwner.reset();
|
||||
yuvSource.reset();
|
||||
yuvConvertedFrame = 0;
|
||||
#endif
|
||||
fullView = VK_NULL_HANDLE;
|
||||
sampledView = VK_NULL_HANDLE;
|
||||
perMipViews.clear();
|
||||
@@ -510,9 +486,7 @@ public:
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
syncedShapeVersion = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
syncedWireSerial = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
~TextureResource() {
|
||||
@@ -539,28 +513,15 @@ 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.
|
||||
void CollectAllDeferredReleases();
|
||||
|
||||
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
|
||||
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
|
||||
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
|
||||
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
|
||||
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
|
||||
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
|
||||
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
|
||||
// layout field, so a second resource aliasing the same image would desynchronise the moment
|
||||
// either of them transitioned it.
|
||||
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// The window a GL texture object opens onto its storage image. For a plain texture this is
|
||||
// the resource's own full extent; for a view it is the sub-range, format and aspect
|
||||
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
|
||||
@@ -579,32 +540,6 @@ public:
|
||||
Bool isTextureView = false;
|
||||
};
|
||||
|
||||
// The four component swizzles packed into one value, for the sampled-view cache key.
|
||||
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
|
||||
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
|
||||
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
|
||||
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
|
||||
}
|
||||
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
|
||||
const TextureResource& resource) const;
|
||||
// Records what a GL texture view needs of the image it views, so the next sync of the
|
||||
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
|
||||
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
|
||||
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
|
||||
MG_State::GLState::ITextureObject& storageTexture);
|
||||
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
|
||||
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with.
|
||||
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
|
||||
Vector<VkFormat>& outFormats) const;
|
||||
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
|
||||
// image. Shared back end of every GL-texture-view sampled path.
|
||||
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
|
||||
TextureResource& resource, const TextureViewWindow& window);
|
||||
|
||||
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
|
||||
@@ -615,8 +550,8 @@ public:
|
||||
// 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
|
||||
// keyed by the handle's {slot, gen}, so a recycled slot 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);
|
||||
@@ -679,30 +614,6 @@ public:
|
||||
const auto it = m_wireTextureResources.find(key);
|
||||
return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr;
|
||||
}
|
||||
#endif
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||
VkImageViewType viewType);
|
||||
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
|
||||
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
VkFormat format, Bool layered, Int32 layer);
|
||||
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
|
||||
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject* texture,
|
||||
Uint32 writtenMipLevel,
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
||||
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
||||
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
||||
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
||||
SamplerNumericDomain numericDomain,
|
||||
VkPipelineStageFlags consumerShaderStageMask,
|
||||
SampledTextureSnapshot& outSnapshot);
|
||||
|
||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||
// generation advances every time the frame command buffer (re)begins
|
||||
// recording; a resource whose stamp does not match was not referenced by
|
||||
@@ -712,8 +623,6 @@ public:
|
||||
void StampResourceRecordingUse(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
}
|
||||
// Map-lookup variant for callers that only hold the GL texture object.
|
||||
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
@@ -722,42 +631,6 @@ public:
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
resource.lastRecordingWriteGeneration = m_recordingGeneration;
|
||||
}
|
||||
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
|
||||
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
|
||||
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
|
||||
//
|
||||
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
|
||||
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
|
||||
// before that write, which then overwrites the newer texels. (An image the recording only
|
||||
// read keeps the old trade: its earlier reads see the newer texels.)
|
||||
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The next sync would record client texels into the image that is live now: same shape (a
|
||||
// shape change mints a new image instead) and content moved since the last sync.
|
||||
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The live resource, or null.
|
||||
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
||||
// would cost far more than the compression it wins back.
|
||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||
// The same ordering question for the other recreate-and-preserve trigger: true when this
|
||||
// texture's live image carries a shorter mip chain than a full one, so defining the missing
|
||||
// levels recreates it and copies the old contents forward.
|
||||
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
|
||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
|
||||
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
|
||||
|
||||
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
|
||||
// anything for an image that carries both aspects. Defaulted so the call sites that have no
|
||||
// texture in hand keep the depth-aspect answer they have always given.
|
||||
@@ -765,8 +638,6 @@ public:
|
||||
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
|
||||
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
@@ -788,63 +659,16 @@ public:
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
|
||||
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
|
||||
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
|
||||
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
|
||||
// SyncTexture, so after the first successful sync of a texture in a draw the
|
||||
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
|
||||
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
|
||||
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
|
||||
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
|
||||
void BeginDrawSyncScope();
|
||||
void EndDrawSyncScope();
|
||||
|
||||
// RAII guard that opens/closes a per-draw sync memo window (see above).
|
||||
class DrawSyncScope {
|
||||
public:
|
||||
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
|
||||
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
|
||||
DrawSyncScope(const DrawSyncScope&) = delete;
|
||||
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
|
||||
private:
|
||||
VkTextureManager& m_manager;
|
||||
};
|
||||
|
||||
private:
|
||||
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
|
||||
Uint64 m_textureImageEpoch = 1;
|
||||
Uint32 m_pendingClearHold = 0; // PendingClearHold's depth
|
||||
// See AdvanceRecordingGeneration. Starts above every resource's default
|
||||
// stamp of 0 so a fresh resource counts as untouched.
|
||||
Uint64 m_recordingGeneration = 1;
|
||||
|
||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
TextureResource &outResource);
|
||||
Bool SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
|
||||
TextureUploadTarget uploadTarget,
|
||||
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
|
||||
TextureResource &resource);
|
||||
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
|
||||
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
|
||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 baseArrayLayer,
|
||||
Uint32 layerCount,
|
||||
const VkComponentMapping* components = nullptr,
|
||||
VkImageUsageFlags viewUsage = 0) const;
|
||||
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
||||
TextureUploadTarget uploadTarget,
|
||||
TextureResource &outResource);
|
||||
static Bool CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
|
||||
TextureUploadTarget& outTarget,
|
||||
IntVec3& outTexelSize,
|
||||
SizeT& outByteSize,
|
||||
Uint32& outMipLevelCount);
|
||||
static Uint32 GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget target);
|
||||
static void ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
|
||||
Uint32& outBaseMipLevel, Uint32& outLevelCount);
|
||||
static VkImageAspectFlags GetAspectMaskForFormat(VkFormat format);
|
||||
void DeferResourceRelease(TextureResource&& resource);
|
||||
void DeferViewRelease(VkImageView view);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyDeferredReleases();
|
||||
// Frees the fence/command buffer/staging buffer of every in-flight texture
|
||||
@@ -852,11 +676,7 @@ private:
|
||||
// the single queue, so the scan stops at the first still-pending entry).
|
||||
// waitAll blocks on every entry - Shutdown's drain.
|
||||
void ReclaimCompletedUploads(Bool waitAll = false);
|
||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||
SizeT PruneDeadTextures();
|
||||
#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
|
||||
@@ -879,7 +699,6 @@ private:
|
||||
Vector<Uint64> m_sharedImageUses;
|
||||
// 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;
|
||||
@@ -896,37 +715,20 @@ private:
|
||||
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
||||
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
||||
Uint32 m_gcFrameCounter = 0;
|
||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||
// textures already fully synced in the current draw (small N -> flat scan).
|
||||
Bool m_drawSyncScopeActive = false;
|
||||
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
|
||||
Bool m_syncingYuvTextureStorage = false;
|
||||
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
|
||||
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
|
||||
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
|
||||
// returns the resource without re-hashing the identity into m_textureResources.
|
||||
struct DrawSyncedTexture {
|
||||
TextureIdentity identity;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
|
||||
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
|
||||
// are resolved on every draw, so cache their resource pointers and skip the
|
||||
// alive/resource map lookups. Node-based std::unordered_map keeps the
|
||||
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
|
||||
// every memo entry must match. SyncTexture still runs on memo hits, so
|
||||
// content/param freshness is unaffected. A dead-then-reused texture address
|
||||
// cannot false-hit: the new object carries a new lifetime id.
|
||||
struct SyncedTextureMemoEntry {
|
||||
const MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
Uint64 lifetimeId = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
// P14: a direct-mapped front for the two wire resource maps, keyed on the handle key
|
||||
// ({slot, gen}, never reused) and believed only at the erase epoch it was taken at - an
|
||||
// unordered_map node never moves until it is erased, and every erase moves the epoch.
|
||||
struct WireResourceLookup {
|
||||
Uint64 key = 0;
|
||||
Uint64 eraseEpoch = 0;
|
||||
TextureResource* resource = nullptr;
|
||||
};
|
||||
static constexpr Uint32 kSyncedTextureMemoSize = 8;
|
||||
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
|
||||
Uint32 m_syncedTextureMemoNext = 0;
|
||||
Uint64 m_resourceEraseEpoch = 1;
|
||||
static constexpr SizeT kWireResourceLookupEntries = 256;
|
||||
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireTextureLookup{};
|
||||
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireRenderbufferLookup{};
|
||||
// Formats whose mutable-image probe failed on this device; their images are created
|
||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
@@ -934,23 +736,6 @@ private:
|
||||
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
||||
// remembered rather than probed once globally.
|
||||
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
|
||||
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
|
||||
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
|
||||
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
|
||||
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
|
||||
// feature almost none of them use. A SAME-format view - which is the common case, and the
|
||||
// Better Clouds case - needs no flag at all and therefore costs nothing.
|
||||
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
|
||||
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
|
||||
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
|
||||
// that NO other format will ever be viewed, and building a view outside it is
|
||||
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
|
||||
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
// 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 {
|
||||
@@ -15,17 +14,80 @@
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
|
||||
Vector<Target> targets;
|
||||
// P15 S0: one descriptor pool per frame slot for the blit's set, reset when the slot's
|
||||
// previous recording is known complete (ClearWireDrawPassCache). A pool created and
|
||||
// destroyed per blit was ~0.16 ms a blit on Adreno 750. A full pool falls back to that.
|
||||
static constexpr Uint32 kFramePoolSets = 64;
|
||||
Vector<VkDescriptorPool> framePools;
|
||||
// P15: the blit's two views and its framebuffer, kept across frames. MC blits its main
|
||||
// target to the window every frame, and creating and retiring two VkImageViews and a
|
||||
// VkFramebuffer per blit was ~0.03 ms/frame on the apply thread. The key carries the
|
||||
// image epochs (any texture image recreated or resource erased invalidates every entry,
|
||||
// the same rule as the draw-pass cache) and the swapchain image for the default
|
||||
// framebuffer; ClearAllWireDrawPassCaches drops the lot at the idle points.
|
||||
struct CachedTarget {
|
||||
VkImage srcImage = VK_NULL_HANDLE, dstImage = VK_NULL_HANDLE;
|
||||
Uint32 srcLevel = 0, srcLayer = 0, dstLevel = 0, dstLayer = 0;
|
||||
VkFormat srcFormat = VK_FORMAT_UNDEFINED, dstFormat = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D dstExtent{};
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0;
|
||||
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
||||
Bool dstIsDefault = false;
|
||||
VkImageView srcView = VK_NULL_HANDLE, dstView = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr SizeT kCachedTargets = 8;
|
||||
Vector<CachedTarget> cachedTargets;
|
||||
};
|
||||
|
||||
// Retires cached blit targets through the wire's deferred destruction (they may still be
|
||||
// named by an in-flight submission). `all` false keeps the entries whose epochs still hold.
|
||||
void VulkanRenderer::RetireWireColorBlitTargets(Bool all) {
|
||||
if (!m_wireColorBlitResources) return;
|
||||
auto& cached = m_wireColorBlitResources->cachedTargets;
|
||||
const Uint64 imageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0;
|
||||
const Uint64 eraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0;
|
||||
WireRetiredObjects retired{};
|
||||
SizeT kept = 0;
|
||||
for (SizeT i = 0; i < cached.size(); ++i) {
|
||||
auto& entry = cached[i];
|
||||
if (!all && entry.textureImageEpoch == imageEpoch && entry.resourceEraseEpoch == eraseEpoch) {
|
||||
cached[kept++] = entry;
|
||||
continue;
|
||||
}
|
||||
retired.framebuffers.push_back(entry.framebuffer);
|
||||
retired.imageViews.push_back(entry.srcView);
|
||||
retired.imageViews.push_back(entry.dstView);
|
||||
}
|
||||
cached.resize(kept);
|
||||
if (!retired.framebuffers.empty()) RetireWireObjects(Move(retired));
|
||||
}
|
||||
|
||||
void VulkanRenderer::ResetWireColorBlitFramePool(Uint32 frameIndex) {
|
||||
auto& pools = m_wireColorBlitResources->framePools;
|
||||
if (frameIndex < pools.size() && pools[frameIndex] != VK_NULL_HANDLE)
|
||||
vkResetDescriptorPool(m_device, pools[frameIndex], 0);
|
||||
}
|
||||
|
||||
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;
|
||||
// Device idle here: cached blit targets no submission names any more.
|
||||
for (const auto& entry : cache.cachedTargets) {
|
||||
vkDestroyFramebuffer(m_device, entry.framebuffer, nullptr);
|
||||
vkDestroyImageView(m_device, entry.srcView, nullptr);
|
||||
vkDestroyImageView(m_device, entry.dstView, nullptr);
|
||||
}
|
||||
cache.cachedTargets.clear();
|
||||
for (const auto& target : cache.targets) {
|
||||
vkDestroyPipeline(m_device, target.pipeline, nullptr);
|
||||
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
|
||||
}
|
||||
for (const auto pool : cache.framePools)
|
||||
if (pool != VK_NULL_HANDLE) vkDestroyDescriptorPool(m_device, pool, nullptr);
|
||||
vkDestroySampler(m_device, cache.nearest, nullptr);
|
||||
vkDestroySampler(m_device, cache.linear, nullptr);
|
||||
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
|
||||
@@ -176,7 +238,7 @@
|
||||
// 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));
|
||||
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
VkFormatProperties destinationProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, &destinationProperties);
|
||||
if (!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) return false;
|
||||
@@ -315,31 +377,96 @@
|
||||
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));
|
||||
// P15: the views and framebuffer come from the cross-frame cache (see CachedTarget).
|
||||
RetireWireColorBlitTargets(/*all=*/false);
|
||||
WireColorBlitResources::CachedTarget wanted{};
|
||||
wanted.srcImage = source.image;
|
||||
wanted.srcLevel = source.level;
|
||||
wanted.srcLayer = source.layer;
|
||||
wanted.srcFormat = source.format;
|
||||
wanted.dstImage = destination.image;
|
||||
wanted.dstLevel = destination.level;
|
||||
wanted.dstLayer = destination.layer;
|
||||
wanted.dstFormat = destination.format;
|
||||
wanted.dstExtent = destination.extent;
|
||||
wanted.renderPass = target->renderPass;
|
||||
wanted.textureImageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0;
|
||||
wanted.resourceEraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0;
|
||||
wanted.dstIsDefault = destination.isDefault;
|
||||
wanted.swapchain = destination.isDefault ? m_swapchainObject.GetHandle() : VK_NULL_HANDLE;
|
||||
const auto sameTarget = [&](const WireColorBlitResources::CachedTarget& entry) {
|
||||
return entry.srcImage == wanted.srcImage && entry.srcLevel == wanted.srcLevel &&
|
||||
entry.srcLayer == wanted.srcLayer && entry.srcFormat == wanted.srcFormat &&
|
||||
entry.dstImage == wanted.dstImage && entry.dstLevel == wanted.dstLevel &&
|
||||
entry.dstLayer == wanted.dstLayer && entry.dstFormat == wanted.dstFormat &&
|
||||
entry.dstExtent.width == wanted.dstExtent.width &&
|
||||
entry.dstExtent.height == wanted.dstExtent.height && entry.renderPass == wanted.renderPass &&
|
||||
entry.textureImageEpoch == wanted.textureImageEpoch &&
|
||||
entry.resourceEraseEpoch == wanted.resourceEraseEpoch && entry.swapchain == wanted.swapchain &&
|
||||
entry.dstIsDefault == wanted.dstIsDefault;
|
||||
};
|
||||
auto& cachedTargets = cache.cachedTargets;
|
||||
WireColorBlitResources::CachedTarget* hit = nullptr;
|
||||
for (auto& entry : cachedTargets)
|
||||
if (sameTarget(entry)) { hit = &entry; break; }
|
||||
if (hit == nullptr) {
|
||||
if (cachedTargets.size() >= WireColorBlitResources::kCachedTargets) {
|
||||
WireRetiredObjects evicted{};
|
||||
evicted.framebuffers.push_back(cachedTargets.front().framebuffer);
|
||||
evicted.imageViews.push_back(cachedTargets.front().srcView);
|
||||
evicted.imageViews.push_back(cachedTargets.front().dstView);
|
||||
RetireWireObjects(Move(evicted));
|
||||
cachedTargets.erase(cachedTargets.begin());
|
||||
}
|
||||
wanted.srcView = makeView(source);
|
||||
wanted.dstView = makeView(destination);
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = target->renderPass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &wanted.dstView;
|
||||
framebufferInfo.width = destination.extent.width;
|
||||
framebufferInfo.height = destination.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &wanted.framebuffer));
|
||||
cachedTargets.push_back(wanted);
|
||||
hit = &cachedTargets.back();
|
||||
}
|
||||
const VkImageView sourceView = hit->srcView;
|
||||
const VkFramebuffer framebuffer = hit->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));
|
||||
{
|
||||
// The frame slot's pool (P15 S0): its sets live until the slot's next frame begins,
|
||||
// which is after this recording completed.
|
||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
||||
if (cache.framePools.size() <= frameIndex) cache.framePools.resize(frameIndex + 1, VK_NULL_HANDLE);
|
||||
if (cache.framePools[frameIndex] == VK_NULL_HANDLE) {
|
||||
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
WireColorBlitResources::kFramePoolSets};
|
||||
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
pool.maxSets = WireColorBlitResources::kFramePoolSets;
|
||||
pool.poolSizeCount = 1;
|
||||
pool.pPoolSizes = &poolSize;
|
||||
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.framePools[frameIndex]));
|
||||
}
|
||||
alloc.descriptorPool = cache.framePools[frameIndex];
|
||||
if (vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet) != VK_SUCCESS) descriptorSet = VK_NULL_HANDLE;
|
||||
}
|
||||
if (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));
|
||||
alloc.descriptorPool = descriptorPool;
|
||||
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
|
||||
@@ -397,12 +524,11 @@
|
||||
// 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 (descriptorPool != VK_NULL_HANDLE) {
|
||||
WireRetiredObjects retired{};
|
||||
retired.descriptorPools.push_back(descriptorPool);
|
||||
RetireWireObjects(Move(retired));
|
||||
}
|
||||
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
// 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 {
|
||||
@@ -63,4 +62,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return VK_FILTER_LINEAR;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
@@ -169,4 +168,3 @@ private:
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// 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 {
|
||||
@@ -326,4 +325,3 @@
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#include "WireMultisampleResolveSpirv.h"
|
||||
|
||||
@@ -993,4 +992,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return measurement;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
//
|
||||
// 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);
|
||||
@@ -118,4 +117,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
|
||||
Bool& renderPassArmAvailable, Bool& shaderStencilExport);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here
|
||||
// belong to the applier; no frontend object, lifetime-id lookup or allocator is used.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of
|
||||
// the fifteen P7-marked refusals die (the other four are WireDescriptorFatal's), and until now
|
||||
// every one of them raised its own abort: no SessionFault frame for the peer, no
|
||||
@@ -134,6 +133,9 @@
|
||||
out.swapchainImageIndex = isWriteTarget ? DefaultFramebufferWriteIndex(true)
|
||||
: DefaultFramebufferReadIndex(true);
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
// Every use of the acquired image is reached through here, ahead of its commands:
|
||||
// the work recorded before it goes out without the acquire wait.
|
||||
if (out.swapchainImageIndex == m_imageIndexAcquired) SubmitAheadOfAcquiredImage();
|
||||
out.image = m_swapchainObject.GetImage(out.swapchainImageIndex);
|
||||
out.format = m_swapchainObject.GetSurfaceFormat().format;
|
||||
out.layout = m_swapchainObject.GetImageLayout(out.swapchainImageIndex);
|
||||
@@ -173,6 +175,7 @@
|
||||
std::max(resource->extent.height >> level, 1u)};
|
||||
out.layout = resource->layout;
|
||||
out.trackedLayout = &resource->layout;
|
||||
out.resource = resource;
|
||||
out.samples = resource->sampleCount;
|
||||
// P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question
|
||||
// about the image and not about the format - VkTextureManager asks for SAMPLED on every
|
||||
@@ -194,13 +197,19 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) {
|
||||
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier) {
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags access = 0;
|
||||
GetImageTransitionSourceState(image.layout, stage, access);
|
||||
// A swapchain image leaves PRESENT_SRC_KHR (or UNDEFINED, new) only after it was acquired:
|
||||
// the transition is ordered after the presentation engine's release through the stages
|
||||
// the acquire wait blocks, which an empty (TOP_OF_PIPE) source scope would not chain to.
|
||||
if (image.isDefault && image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
||||
(image.layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR || image.layout == VK_IMAGE_LAYOUT_UNDEFINED))
|
||||
stage |= FrameContext::kAcquireWaitStages;
|
||||
// One tracked layout per image: move the complete mip chain, never just the surface.
|
||||
VkImageAspectFlags barrierAspect = image.aspect;
|
||||
// Every packed format, D16_UNORM_S8_UINT included (g5-msrbo review round): the layout is
|
||||
@@ -208,15 +217,31 @@
|
||||
// tracked layout - and CopyWireDepthStencilBlit moves the two aspects in two legs.
|
||||
if (WireFormatIsPackedDepthStencil(image.format))
|
||||
barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
// P15: A DRAW PASS'S ATTACHMENTS SHARE ONE FULL BARRIER, and the sampled-image
|
||||
// barrier shares it too. The memory barrier below is global (ALL_COMMANDS, every write
|
||||
// made visible), the same one ResolveWireImageDescriptor records, so a second one with no
|
||||
// image written since the last (WireImageWriteEpoch unchanged) orders nothing. Only the
|
||||
// new-pass caller (PrepareWireDrawPass) opts in: its attachments are written inside the
|
||||
// pass it is about to begin, and that pass's end moves the epoch. A transfer caller
|
||||
// writes right after this call with the epoch already moved, so it must neither skip nor
|
||||
// note - noting there would let a later barrier be skipped past its write.
|
||||
// FCL Magma: 27.5 barriers and 12.6 render passes per frame against dev's 3.4 and 4.1,
|
||||
// most of them a second barrier per attachment and a pass closed and reopened at the
|
||||
// first sampled texture after every pass begin.
|
||||
if (image.layout == layout) {
|
||||
// GENERAL->GENERAL is not a layout transition, but the next load,
|
||||
// sample or blit still depends on earlier attachment/shader writes.
|
||||
// The old per-verb queue idle must not be its only ordering edge.
|
||||
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(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
const Bool covered = coalesceWithLastFullBarrier && m_uniformManager &&
|
||||
WireImageWriteEpoch() == m_uniformManager->WireImageBarrierEpoch();
|
||||
if (!covered) {
|
||||
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(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
if (coalesceWithLastFullBarrier && m_uniformManager) m_uniformManager->NoteWireImageBarrier();
|
||||
}
|
||||
}
|
||||
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout,
|
||||
stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access,
|
||||
@@ -230,8 +255,341 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- P15: WIRE DEFERRED CLEARS -------------------------------------------------------------
|
||||
//
|
||||
// Minecraft (1.21.5) clears its targets through framebuffers of their own - a depth-only one
|
||||
// for the shared depth texture, a colour+depth one per target - and then draws through the
|
||||
// main framebuffer. A clear that begins a LOAD_OP_CLEAR pass of its own made every such clear a
|
||||
// pass that did nothing but clear, closed by the very next draw: 11.5 passes a frame against
|
||||
// dev's 4.1 (whose clear manager folded the same clears into the next pass's load op), each
|
||||
// one an extra full load/store of the attachments on a tiler.
|
||||
//
|
||||
// So a clear of an image the open pass holds is recorded inside that pass, and a whole-image
|
||||
// clear of any other image WAITS ON THE IMAGE (WirePendingClear, kept on the image's own
|
||||
// resource with its level and layers - never keyed by a GL name). The next draw pass that has
|
||||
// it as an attachment begins with LOAD_OP_CLEAR for it (PrepareWireDrawPass /
|
||||
// BeginWireDrawPassFolding); any other use reaches the image through
|
||||
// SyncTextureResourceByHandle, which runs it first (MaterializeWirePendingClears). The default
|
||||
// framebuffer never defers: its in-pass clear is a known Adreno problem (see
|
||||
// ClearWireFramebufferInPass), and it is presented, not sampled.
|
||||
|
||||
// Whether `image` can carry a deferred clear: a plain single-sample 2D(-array) image of this
|
||||
// session with a float colour or a depth/stencil format, clearable by a transfer command.
|
||||
Bool VulkanRenderer::WireImageCanDeferClear(const WireImage& image) {
|
||||
const auto* resource = image.resource;
|
||||
if (resource == nullptr || image.isDefault || image.is3D || image.samples != VK_SAMPLE_COUNT_1_BIT) return false;
|
||||
if (resource->sharedImageId != 0 || resource->yuvImageId != 0) return false;
|
||||
if ((resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0) return false;
|
||||
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
||||
(vkuFormatIsSRGB(image.format) || vkuFormatIsSINT(image.format) || vkuFormatIsUINT(image.format) ||
|
||||
vkuFormatIsCompressed(image.format)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::MaterializeWirePendingClears(VkTextureManager::TextureResource& resource) {
|
||||
if (resource.pendingClears.empty()) return;
|
||||
if (resource.image == VK_NULL_HANDLE) {
|
||||
resource.pendingClears.clear();
|
||||
return;
|
||||
}
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
||||
EndActiveRenderPassOn(frame.commandBuffer);
|
||||
// After every earlier access to the image (the clears were issued after them).
|
||||
if (resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, resource.image, resource.layout,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
resource.layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_WRITE_BIT,
|
||||
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource.aspect, 0, resource.mipLevels))
|
||||
MagmaWireFatal("pending-clear-transition");
|
||||
} else {
|
||||
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(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
for (const auto& pending : resource.pendingClears) {
|
||||
const VkImageSubresourceRange range{pending.aspects, pending.level, 1, pending.layer, pending.layers};
|
||||
if (pending.aspects & VK_IMAGE_ASPECT_COLOR_BIT)
|
||||
vkCmdClearColorImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
&pending.value.color, 1, &range);
|
||||
else
|
||||
vkCmdClearDepthStencilImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
&pending.value.depthStencil, 1, &range);
|
||||
}
|
||||
resource.pendingClears.clear();
|
||||
// Recorded writes: the next sampled-image or attachment barrier must cover them.
|
||||
BumpWireImageWriteEpoch();
|
||||
}
|
||||
|
||||
// Records `aspects` of `image` cleared to `value` as pending, replacing whatever an earlier
|
||||
// pending clear of the same subresource said for those aspects.
|
||||
void VulkanRenderer::NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects,
|
||||
const VkClearValue& value) {
|
||||
auto& pending = image.resource->pendingClears;
|
||||
for (SizeT i = 0; i < pending.size();) {
|
||||
auto& entry = pending[i];
|
||||
if (entry.level == image.level && entry.layer == image.layer && entry.layers == image.layers) {
|
||||
entry.aspects &= ~aspects;
|
||||
if (entry.aspects == 0) {
|
||||
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
VkTextureManager::WirePendingClear entry{};
|
||||
entry.level = image.level;
|
||||
entry.layer = image.layer;
|
||||
entry.layers = image.layers;
|
||||
entry.aspects = aspects;
|
||||
entry.value = value;
|
||||
pending.push_back(entry);
|
||||
}
|
||||
|
||||
// The open wire draw pass's attachment that IS `image` (same image, level and layers), or
|
||||
// VK_ATTACHMENT_UNUSED.
|
||||
Uint32 VulkanRenderer::OpenPassAttachmentOf(const WireImage& image,
|
||||
const Vector<WireDrawAttachmentKey>& attachments) {
|
||||
for (Uint32 a = 0; a < attachments.size(); ++a) {
|
||||
const auto& key = attachments[a];
|
||||
if (key.image == image.image && key.level == image.level && key.layer == image.layer &&
|
||||
key.layers == image.layers)
|
||||
return a;
|
||||
}
|
||||
return VK_ATTACHMENT_UNUSED;
|
||||
}
|
||||
|
||||
// A clear whose images the OPEN draw pass holds - through another framebuffer than the
|
||||
// pass's own (the depth-only clear framebuffer) - is recorded inside that pass. Returns the
|
||||
// buffers it did not take.
|
||||
GLbitfield VulkanRenderer::ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
||||
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
if (!remaining || fbo.IsDefault || m_wirePreparationDepth != 0 || !m_wireDrawPass) return remaining;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash)
|
||||
return remaining;
|
||||
const auto& key = m_wireDrawPassKey;
|
||||
const RenderPassEntry* pass = m_wireDrawPass.get();
|
||||
const VkCommandBuffer passBuffer = active->commandBuffer;
|
||||
const Uint64 passHash = active->hash;
|
||||
VkRect2D area{{0, 0}, {key.width, key.height}};
|
||||
if (GLScissorTestClips()) {
|
||||
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
|
||||
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
|
||||
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(key.width));
|
||||
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(key.height));
|
||||
if (x0 >= x1 || y0 >= y1) return remaining; // the standalone path answers an empty box
|
||||
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
|
||||
}
|
||||
// The image must be exactly the pass's extent, or the pass's render area is not its area.
|
||||
const auto fits = [&](const WireImage& image) {
|
||||
return image.extent.width == key.width && image.extent.height == key.height;
|
||||
};
|
||||
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
|
||||
Uint32 count = 0;
|
||||
GLbitfield handled = 0;
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
if (remaining & GL_COLOR_BUFFER_BIT) {
|
||||
Bool all = true;
|
||||
Uint32 colorCount = 0;
|
||||
VkClearAttachment colorClears[MG_Pipe::kMGPipeMaxColorAttachments]{};
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
|
||||
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
|
||||
const Int32 slot = fbo.DrawBuffers[i];
|
||||
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
||||
if (fbo.Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
|
||||
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
|
||||
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
|
||||
if (!mask.r() || !mask.g() || !mask.b() || !mask.a() || payload.colorEncoding != ClearColorEncoding::Float) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
const WireImage image = ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT, true);
|
||||
const Uint32 a = image.image ? OpenPassAttachmentOf(image, key.attachments) : VK_ATTACHMENT_UNUSED;
|
||||
Uint32 reference = VK_ATTACHMENT_UNUSED;
|
||||
for (Uint32 c = 0; a != VK_ATTACHMENT_UNUSED && c < key.colorReferences.size(); ++c)
|
||||
if (key.colorReferences[c] == a) reference = c;
|
||||
if (reference == VK_ATTACHMENT_UNUSED || !fits(image) || vkuFormatIsSRGB(image.format) ||
|
||||
vkuFormatIsSRGB(key.attachments[a].format) || vkuFormatIsSINT(image.format) ||
|
||||
vkuFormatIsUINT(image.format)) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
auto& clear = colorClears[colorCount++];
|
||||
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
clear.colorAttachment = reference;
|
||||
clear.clearValue.color = MakeVkClearColorValue(payload, false);
|
||||
}
|
||||
if (all) {
|
||||
for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c];
|
||||
handled |= GL_COLOR_BUFFER_BIT;
|
||||
}
|
||||
}
|
||||
VkImageAspectFlags depthStencilAspects = 0;
|
||||
const auto depthStencilHeld = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) {
|
||||
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
||||
return image.image && fits(image) && key.depthReference != VK_ATTACHMENT_UNUSED &&
|
||||
OpenPassAttachmentOf(image, key.attachments) == key.depthReference &&
|
||||
(key.attachments[key.depthReference].aspect & aspect) != 0;
|
||||
};
|
||||
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
||||
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
} else if (depthStencilHeld(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT)) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
}
|
||||
}
|
||||
if (remaining & GL_STENCIL_BUFFER_BIT) {
|
||||
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
||||
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
} else if (mask == 255u && depthStencilHeld(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
||||
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
}
|
||||
}
|
||||
if (depthStencilAspects) {
|
||||
auto& clear = clears[count++];
|
||||
clear.aspectMask = depthStencilAspects;
|
||||
clear.clearValue.depthStencil = {payload.depth, payload.stencil};
|
||||
}
|
||||
// A resolve above may have submitted (a lazy upload) or run another image's deferred clear,
|
||||
// closing the pass: then nothing is recorded and nothing is taken.
|
||||
active = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (active == nullptr || active->commandBuffer != passBuffer || active->hash != passHash ||
|
||||
m_wireDrawPass.get() != pass)
|
||||
return remaining;
|
||||
if (count > 0) {
|
||||
const VkClearRect rect{area, 0, key.layers};
|
||||
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
|
||||
}
|
||||
return remaining & ~handled;
|
||||
}
|
||||
|
||||
// A whole-image clear of images the open pass does not hold waits on the images (see the
|
||||
// section comment). Returns the buffers it did not take.
|
||||
GLbitfield VulkanRenderer::DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
||||
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
if (!remaining || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining;
|
||||
const Bool scissor = GLScissorTestClips();
|
||||
const auto wholeImage = [&](const WireImage& image) {
|
||||
if (!scissor) return true;
|
||||
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
|
||||
return box.x() <= 0 && box.y() <= 0 && box.x() + box.z() >= static_cast<Int32>(image.extent.width) &&
|
||||
box.y() + box.w() >= static_cast<Int32>(image.extent.height);
|
||||
};
|
||||
const Bool passOpen = VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass;
|
||||
const auto heldByOpenPass = [&](const WireImage& image) {
|
||||
return passOpen && OpenPassAttachmentOf(image, m_wireDrawPassKey.attachments) != VK_ATTACHMENT_UNUSED;
|
||||
};
|
||||
struct Target {
|
||||
WireImage image;
|
||||
VkImageAspectFlags aspects;
|
||||
VkClearValue value;
|
||||
Bool texture;
|
||||
};
|
||||
Target targets[MG_Pipe::kMGPipeMaxColorAttachments + 2]{};
|
||||
Uint32 targetCount = 0;
|
||||
GLbitfield handled = 0;
|
||||
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
||||
if ((remaining & GL_COLOR_BUFFER_BIT) && payload.colorEncoding == ClearColorEncoding::Float) {
|
||||
Bool all = true;
|
||||
const Uint32 first = targetCount;
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
|
||||
const Int32 slot = fbo.DrawBuffers[i];
|
||||
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
||||
const auto& surface = fbo.Color[slot];
|
||||
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
|
||||
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
|
||||
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
|
||||
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
const WireImage image = ResolveWireImage(fbo, surface, VK_IMAGE_ASPECT_COLOR_BIT, true);
|
||||
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
auto& target = targets[targetCount++];
|
||||
target.image = image;
|
||||
target.aspects = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
target.value.color = MakeVkClearColorValue(payload, false);
|
||||
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
||||
}
|
||||
if (all) handled |= GL_COLOR_BUFFER_BIT;
|
||||
else targetCount = first;
|
||||
}
|
||||
const auto depthStencil = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Bool {
|
||||
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
||||
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) return false;
|
||||
for (Uint32 t = 0; t < targetCount; ++t) {
|
||||
auto& target = targets[t];
|
||||
if (target.image.image == image.image && target.image.level == image.level &&
|
||||
target.image.layer == image.layer && target.image.layers == image.layers &&
|
||||
!(target.aspects & VK_IMAGE_ASPECT_COLOR_BIT)) {
|
||||
target.aspects |= aspect; // a combined depth/stencil image: one entry, both aspects
|
||||
return true;
|
||||
}
|
||||
}
|
||||
auto& target = targets[targetCount++];
|
||||
target.image = image;
|
||||
target.aspects = aspect;
|
||||
target.value.depthStencil = {payload.depth, payload.stencil};
|
||||
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
||||
return true;
|
||||
};
|
||||
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
||||
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
else if (depthStencil(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT))
|
||||
handled |= GL_DEPTH_BUFFER_BIT;
|
||||
}
|
||||
if (remaining & GL_STENCIL_BUFFER_BIT) {
|
||||
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
||||
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
else if (mask == 255u && depthStencil(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT))
|
||||
handled |= GL_STENCIL_BUFFER_BIT;
|
||||
}
|
||||
m_textureManager->FlushPendingUploads();
|
||||
for (Uint32 t = 0; t < targetCount; ++t) {
|
||||
const auto& target = targets[t];
|
||||
NoteWirePendingClear(target.image, target.aspects, target.value);
|
||||
// A GPU write as far as the CPU shadow is concerned, as a recorded clear is.
|
||||
if (target.texture)
|
||||
m_textureManager->MarkWireTextureGpuWritten(target.image.storage, target.image.level,
|
||||
target.image.layer, target.image.layers);
|
||||
}
|
||||
return remaining & ~handled;
|
||||
}
|
||||
|
||||
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
||||
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
||||
const ClearAttachmentPayload& clearRequest, GLint drawbuffer) {
|
||||
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
|
||||
// A clear of the window: the frame's work so far goes out ahead of the acquire wait, before
|
||||
// any of the clear's paths (in-pass, deferred, standalone) records or resolves anything.
|
||||
if (fbo.IsDefault) SubmitAheadOfAcquiredImage();
|
||||
// P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass,
|
||||
// WireDraw.inc); only the buffers it hands back take the standalone pass below.
|
||||
// P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds
|
||||
// its images (another framebuffer's), deferred on the images when it covers them whole,
|
||||
// and only then a pass of its own (the in-pass clear again, which begins one) or the
|
||||
// standalone path.
|
||||
ClearAttachmentPayload payload = clearRequest;
|
||||
if (m_wirePreparationDepth == 0 && WirePassMemoHolds(fbo, m_frameContext.GetCurrent()))
|
||||
payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = ClearWireAttachmentsOfOpenPass(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = DeferWireClear(fbo, payload, drawbuffer);
|
||||
if (payload.mask) payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
||||
if (!payload.mask) return;
|
||||
RetireWireDrawPass();
|
||||
// A stream that neither presents nor finishes (an off-screen loop) never submits, so its
|
||||
// recording and the per-clear objects retired below would grow until host memory runs
|
||||
@@ -240,12 +598,12 @@
|
||||
if (m_wireRetiredObjects.size() >= kRetiredObjectsBeforeSubmit && m_wirePreparationDepth == 0 &&
|
||||
HasPendingRecordedWork())
|
||||
(void)FlushPendingCommands();
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
|
||||
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
|
||||
const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
|
||||
Uint32 colorIndex) {
|
||||
if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return;
|
||||
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
const auto mask = MGB_CTX->GetColorMaskIndexed(colorIndex);
|
||||
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(colorIndex);
|
||||
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return;
|
||||
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
|
||||
MGLOG_W_ONCE("DirectVulkan: glClear with a partial color mask is not supported");
|
||||
@@ -253,10 +611,10 @@
|
||||
}
|
||||
}
|
||||
if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
||||
if (!MGB_CTX->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
if (!MG_Pipe::gPipeInputs->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
||||
const Uint32 mask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
||||
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
||||
if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
else if (mask != 255u) {
|
||||
MGLOG_W_ONCE("DirectVulkan: glClear with a partial stencil write mask is not supported");
|
||||
@@ -268,8 +626,8 @@
|
||||
if (!image.image) return;
|
||||
m_textureManager->FlushPendingUploads();
|
||||
VkRect2D area{{0, 0}, image.extent};
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
const auto& box = MGB_CTX->GetScissorBox();
|
||||
if (GLScissorTestClips()) {
|
||||
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
|
||||
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
|
||||
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(image.extent.width));
|
||||
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(image.extent.height));
|
||||
@@ -917,8 +1275,8 @@
|
||||
RetireWireDrawPass();
|
||||
// The scissor test clips blit writes: intersect the destination rectangle with
|
||||
// the scissor box and shrink the source proportionally.
|
||||
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
||||
const IntVec4& scissor = MGB_CTX->GetScissorBox();
|
||||
if (GLScissorTestClips()) {
|
||||
const IntVec4& scissor = MG_Pipe::gPipeInputs->GetScissorBox();
|
||||
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
|
||||
const Bool dstFlipped = d1 < d0;
|
||||
GLint lo = dstFlipped ? d1 : d0;
|
||||
@@ -1700,8 +2058,8 @@
|
||||
auto& record = state.TextureResources[handle.Slot];
|
||||
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
|
||||
resource->syncedWireSerial = ++record.Serial;
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle);
|
||||
auto& store = MG_Record::ServerStagedTexture();
|
||||
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(handle);
|
||||
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
|
||||
// The resource descriptor determines the upload-target set; no texture unit or
|
||||
// client upload-target vector participates in defining the generated shadow.
|
||||
@@ -1719,7 +2077,7 @@
|
||||
default: break;
|
||||
}
|
||||
for (Uint32 level=base+1;level<base+count;++level) {
|
||||
const IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(
|
||||
const IntVec3 extent = MG_Record::StagedTextureMipExtent(
|
||||
record.Desc.Target, record.Desc.Width, record.Desc.Height, record.Desc.Depth, level);
|
||||
// PH-4: the record's own target and format, not NoteLevelDefined's Tex2D/RGBA8
|
||||
// defaults - a 2D-array or 3D level has depth > 1, which the Tex2D device-limit rule
|
||||
@@ -1730,4 +2088,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// 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.
|
||||
@@ -164,4 +163,3 @@
|
||||
RetireWireObjects(Move(retired));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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);
|
||||
@@ -245,4 +244,3 @@
|
||||
out = {image.sampler, image.view, VK_IMAGE_LAYOUT_GENERAL};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// 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 {
|
||||
@@ -59,4 +58,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return key;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#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,
|
||||
@@ -46,4 +45,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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) {
|
||||
@@ -211,4 +210,3 @@
|
||||
ownedBytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
#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) {
|
||||
@@ -47,7 +45,9 @@ namespace MobileGL::MG_Pipe {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The stamp is the poison's (m_filled, the generations): a build without MOBILEGL_PIPE_POISON
|
||||
// compiles no read check that could ask any of this. Every transport build has the poison.
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// ================================================================================
|
||||
// P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict
|
||||
// ================================================================================
|
||||
@@ -228,9 +228,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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 MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MGPipeBlocksAreDistinct()) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_ROLE_SPLIT_STATE=1");
|
||||
}
|
||||
#endif
|
||||
if (!MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
|
||||
}
|
||||
@@ -238,6 +240,9 @@ namespace MobileGL::MG_Pipe {
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The strict knob is the transport's (MOBILEGL_IPC_STRICT_ERRORS); without one an
|
||||
// admitted pull is counted and the unbarriered one above is still Fatal.
|
||||
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
|
||||
@@ -255,6 +260,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
AdmittedBarrierPullOnce(field, verb, !statically);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
|
||||
@@ -348,12 +354,15 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P13 W5: the role split, the two blocks and the context-live flag are the transport's; the
|
||||
// stamp and the read verdicts around them are the record arm's (PipeInputs.h's dual block
|
||||
// gives a build without a transport the one-block answers inline).
|
||||
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
|
||||
@@ -389,7 +398,6 @@ namespace MobileGL::MG_Pipe {
|
||||
return MGPipeRoleSplitRehearsalActive() ||
|
||||
MG_Config::Transport == MG_Config::TransportMode::Spawn;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
PipeInputs& MGPipeClientInputs() {
|
||||
return MGPipeRoleSplitRehearsalActive() ? gPipeInputsClientBlock : gPipeInputs;
|
||||
@@ -407,7 +415,6 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
Bool MGPipeServerContextIsLive() { return MGPipeStampAccess::ServerContextLive(gPipeInputs); }
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace { const void* g_serverOwnedWindow = nullptr; }
|
||||
|
||||
void MGPipeServerSetOwnedWindow(const void* window) { g_serverOwnedWindow = window; }
|
||||
@@ -430,13 +437,12 @@ namespace MobileGL::MG_Pipe {
|
||||
if (height != nullptr) *height = g_serverOwnedWindowHeight;
|
||||
return true;
|
||||
}
|
||||
#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.
|
||||
// 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
|
||||
@@ -455,6 +461,7 @@ namespace MobileGL::MG_Pipe {
|
||||
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(
|
||||
(serial << 1) | Uint64{1})));
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
@@ -509,7 +516,9 @@ namespace MobileGL::MG_Pipe {
|
||||
// "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.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGPipeServerBlockNoteIdentity();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
|
||||
@@ -587,14 +596,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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;
|
||||
if (!gPipeInputs->ServerStampedVerb()) return;
|
||||
const auto ownership = MGPipeFieldOwnershipOf(field);
|
||||
if (ownership == MGPipeFieldOwnership::kFatal)
|
||||
MGPipeInputPoisonFatalForVerb(field, gPipeInputs.CurrentVerb());
|
||||
MGPipeInputPoisonFatalForVerb(field, gPipeInputs->CurrentVerb());
|
||||
if (ownership == MGPipeFieldOwnership::kBarrierPulled)
|
||||
CountBarrierPull(field, gPipeInputs.CurrentVerb());
|
||||
CountBarrierPull(field, gPipeInputs->CurrentVerb());
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
#endif // MOBILEGL_BUILD_RECORD_ARM && MOBILEGL_PIPE_POISON
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
namespace {
|
||||
|
||||
@@ -13,22 +13,19 @@
|
||||
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answer lives in the
|
||||
// applier - MGPipeApplierTextureShutterSerial() / MGPipeApplierContextSerial(), declared here
|
||||
// so the accessors below can answer with them under a server-stamped verb. MG_Pipe is below
|
||||
// MG_Backend, so this direction is the layering's, and PipeApply.h forward-declares
|
||||
// PipeInputs rather than including this header, so there is no cycle.
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
|
||||
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
|
||||
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
|
||||
// there without dragging MG_Remote in.
|
||||
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
|
||||
MOBILEGL_PIPE_VERIFY)
|
||||
#if (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || MOBILEGL_PIPE_VERIFY)
|
||||
#define MOBILEGL_PIPE_POISON 1
|
||||
#else
|
||||
#define MOBILEGL_PIPE_POISON 0
|
||||
@@ -38,9 +35,9 @@ namespace MobileGL::MG_Pipe {
|
||||
// TABLE 2 (CONTRACT-P5.md section 3, R-7): the four ownership classes, one per field, plus
|
||||
// the seven sticky forwards' own rows. Included HERE rather than from MG_Pipe/MGPipe.h with
|
||||
// gen_pipe.py's seven outputs, deliberately: MGPipe.h is in the PULL build's include closure
|
||||
// and G1 admits no symbol motion there, while this header is reached only through
|
||||
// PipeInputsSwitch.h's MOBILEGL_PIPE_PUSH arm. It is also exactly the header the poison check
|
||||
// below and the server's verb stamp both already see.
|
||||
// and G1 admitted no symbol motion there (until P13 retired the pull build), while this header is
|
||||
// what the backends include directly. It is also exactly the header the poison check below and the
|
||||
// server's verb stamp both already see.
|
||||
#include <MG_Pipe/generated/PipeFieldOwnership.inc>
|
||||
|
||||
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
|
||||
@@ -54,7 +51,6 @@ namespace MobileGL::MG_Pipe {
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- P5: the split arm of the read check (R-7.2, R-7.3) ------------------------------
|
||||
//
|
||||
// A stale read stops being one answer and becomes FOUR, keyed on the field's table-2 class
|
||||
@@ -88,13 +84,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// from either counting the same read twice or - worse, because the field's own class would
|
||||
// not be BARRIER-PULLED - aborting a read the argument row had just declared legal.
|
||||
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped);
|
||||
#endif
|
||||
|
||||
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
|
||||
// a read of a field whose stamp is older than the current verb serial is
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#define MGP_INPUT_CHECK(Field) \
|
||||
do { \
|
||||
if ((m_serverStampedVerb && ::MobileGL::MG_Pipe::MGPipeFieldOwnershipOf(Field) == \
|
||||
@@ -111,15 +105,6 @@ namespace MobileGL::MG_Pipe {
|
||||
} \
|
||||
} while (0)
|
||||
#else
|
||||
#define MGP_INPUT_CHECK(Field) \
|
||||
do { \
|
||||
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
|
||||
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
|
||||
} \
|
||||
} while (0)
|
||||
#define MGP_INPUT_CHECK_ARG(Field, Arg0) MGP_INPUT_CHECK(Field)
|
||||
#endif
|
||||
#else
|
||||
#define MGP_INPUT_CHECK(Field) ((void)0)
|
||||
#define MGP_INPUT_CHECK_ARG(Field, Arg0) ((void)0)
|
||||
#endif
|
||||
@@ -256,18 +241,17 @@ namespace MobileGL::MG_Pipe {
|
||||
};
|
||||
|
||||
// ---- identity / liveness (not fields) ----
|
||||
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
|
||||
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push gPipeInputs
|
||||
// must be true as soon as a context exists, fill or no fill, which is what today's
|
||||
// null-context guards test.
|
||||
Bool IsLive() const;
|
||||
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
|
||||
// The live GLContext's address at the last fill; serves gPipeInputs.
|
||||
const void* ContextIdentity() const { return m_contextIdentity; }
|
||||
// The verb of the last fill, kVerbCount before the first one.
|
||||
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
const MGPipeFilledState& FilledState() const { return m_filled; }
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// TRUE between the server's verb-boundary stamp and whoever clears it. It is the
|
||||
// arming condition of the whole split read path: only inside a server-stamped verb is
|
||||
// a BARRIER-PULLED read counted rather than Fatal, and only there is a sticky forward
|
||||
@@ -286,7 +270,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// case the sticky exemption was written for, would be counted and, under strict, would
|
||||
// abort. So: PipeApplier clears on leaving the applier. Not optional.
|
||||
Bool ServerStampedVerb() const { return m_serverStampedVerb; }
|
||||
#endif
|
||||
|
||||
// ---- V: values ----
|
||||
Int GetActiveTextureUnit() const {
|
||||
@@ -443,7 +426,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// BufferTarget, and Coverage.def:62-69 already rules that such a field stays ONE row.
|
||||
// Only [0] (pack) has a carrier - set_pixel_pack_state, which the applier writes
|
||||
// (PipeApply.cpp:1373) - so the field is APPLIER-DERIVED and the UNPACK half is FATAL:
|
||||
// every MGB_CTX->GetPixelStoreParameters site in the tree passes false
|
||||
// every gPipeInputs->GetPixelStoreParameters site in the tree passes false
|
||||
// (DirectGLES.cpp:7924, :9399, :10893, :11272, Utils.cpp:2302,
|
||||
// VulkanRenderer.cpp:10980), and PipeFill.cpp's EmitPixelPackState says the same from
|
||||
// the other side: "nothing on the far side of the boundary reads unpack state".
|
||||
@@ -492,9 +475,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 GetSamplingResolutionGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_serverStampedVerb) return MGPipeApplierTextureShutterSerial();
|
||||
#endif
|
||||
return m_samplingResolutionGeneration;
|
||||
}
|
||||
const IntVec4& GetScissorBox() const {
|
||||
@@ -510,21 +491,17 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 GetTextureBindGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// See GetSamplingResolutionGeneration: the server answers from its own Serial.
|
||||
if (m_serverStampedVerb) return MGPipeApplierTextureShutterSerial();
|
||||
#endif
|
||||
return m_textureBindGeneration;
|
||||
}
|
||||
Uint64 GetTextureContextId() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A context IDENTITY rather than a generation: stable within the served context,
|
||||
// moved by every MGPipeApplierReset - which is all the backends' per-context memo
|
||||
// keys ask of it.
|
||||
if (m_serverStampedVerb) return MGPipeApplierContextSerial();
|
||||
#endif
|
||||
return m_textureContextId;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const {
|
||||
@@ -758,9 +735,7 @@ namespace MobileGL::MG_Pipe {
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState m_filled{};
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool m_serverStampedVerb = false;
|
||||
#endif
|
||||
|
||||
// ---- V ----
|
||||
Int m_activeTextureUnit = 0;
|
||||
@@ -823,7 +798,7 @@ namespace MobileGL::MG_Pipe {
|
||||
TextureUnit* m_textureUnitBase = nullptr;
|
||||
};
|
||||
|
||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||
// The single global the backends read through gPipeInputs (ARCHITECTURE.md 9.2). An inline
|
||||
// variable: no .cpp is needed for the definition.
|
||||
//
|
||||
// P14 S2: IT IS THE PROCESS-WIDE BLOCK, and `gPipeInputs` below is the name every reader
|
||||
@@ -851,18 +826,24 @@ namespace MobileGL::MG_Pipe {
|
||||
// ============================================================================
|
||||
//
|
||||
// ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it
|
||||
// through MGB_CTX, both on the SAME apply thread - the block IS "the state the record in hand
|
||||
// through gPipeInputs, both on the SAME apply thread - the block IS "the state the record in hand
|
||||
// carries". Two sessions applying concurrently on two apply threads through one block would
|
||||
// read each other's render state, which is the cross-talk a multi-session server must not
|
||||
// have. Nothing about the block's contents changed; what changed is that the name resolves to
|
||||
// the SESSION'S block on a thread that belongs to one.
|
||||
//
|
||||
// WHY A THREAD-LOCAL REFERENCE, AND NOT SOMETHING CLEVERER. MGB_CTX is
|
||||
// `(&::MobileGL::MG_Pipe::gPipeInputs)` (MG_Pipe/PipeInputsSwitch.h) and is spelled at ~380
|
||||
// sites in MG_Backend/MG_Impl; a reference whose TARGET varies with the calling thread is the
|
||||
// only shape that keeps all of them compiling and all of them correct. The cost is one TLS
|
||||
// address computation per access site - not one per field, and with no guard: the reference is
|
||||
// bound once per thread on its first use.
|
||||
// WHY A THREAD-LOCAL SLOT, AND NOT SOMETHING CLEVERER. gPipeInputs is spelled at ~380
|
||||
// sites in MG_Backend/MG_Impl (they read `MG_Pipe::gPipeInputs->` directly since P13 retired the
|
||||
// pull/push switch macro); a name whose TARGET varies with the calling thread is the only
|
||||
// shape that keeps all of them compiling and all of them correct.
|
||||
//
|
||||
// P15: A POINTER SLOT, NOT A REFERENCE. It used to be a `thread_local PipeInputs&` bound by a
|
||||
// dynamic initializer, and a thread_local with a dynamic initializer is reached through its
|
||||
// TLS init function, which tests two more thread_local guards on every read: three TLS
|
||||
// address calls per access in a dlopen'ed library, at every API level. The slot below is
|
||||
// constant-initialized and trivially destructible, so an access is one TLS address
|
||||
// computation, a load and a null test; the binding itself is unchanged - once per thread, at
|
||||
// its first use, through the same resolver.
|
||||
//
|
||||
// THE RESOLVER IS A HOOK because this header may not know about sessions (MG_Remote/Server
|
||||
// owns them and includes THIS header, not the other way round). It is installed once by the
|
||||
@@ -885,7 +866,27 @@ namespace MobileGL::MG_Pipe {
|
||||
return gPipeInputsDefault;
|
||||
}
|
||||
|
||||
inline thread_local PipeInputs& gPipeInputs = MGPipeServerInputsForCallingThread();
|
||||
class PipeInputsThreadSlot {
|
||||
public:
|
||||
constexpr PipeInputsThreadSlot() = default;
|
||||
PipeInputsThreadSlot(const PipeInputsThreadSlot&) = delete;
|
||||
PipeInputsThreadSlot& operator=(const PipeInputsThreadSlot&) = delete;
|
||||
// The block's address is `&*gPipeInputs`; the slot's own address names nothing.
|
||||
void operator&() const = delete;
|
||||
|
||||
PipeInputs& Get() {
|
||||
PipeInputs* bound = m_bound;
|
||||
if (bound == nullptr) [[unlikely]] bound = m_bound = &MGPipeServerInputsForCallingThread();
|
||||
return *bound;
|
||||
}
|
||||
PipeInputs* operator->() { return &Get(); }
|
||||
PipeInputs& operator*() { return Get(); }
|
||||
operator PipeInputs&() { return Get(); }
|
||||
|
||||
private:
|
||||
PipeInputs* m_bound = nullptr;
|
||||
};
|
||||
inline constinit thread_local PipeInputsThreadSlot gPipeInputs;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ============================================================================
|
||||
@@ -903,7 +904,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// dual-block arm, PipeInputs.cpp) instead of a silent cross-role answer.
|
||||
//
|
||||
// THE READ SIDE'S SPELLING DOES NOT MOVE. gPipeInputs remains the server-role block, so the
|
||||
// 379 MGB_CTX sites and PipeApply.cpp's applier writes are untouched; the only new spelling
|
||||
// 379 gPipeInputs sites and PipeApply.cpp's applier writes are untouched; the only new spelling
|
||||
// is on the fill side (MG_Impl/Pipe/PipeFill.cpp), which asks MGPipeClientInputs(). Under
|
||||
// monolith transport - every unit and integration-gpu lane of a split build - the selection
|
||||
// folds back to the single shared block and behaviour is byte-for-byte the old one.
|
||||
@@ -1007,6 +1008,9 @@ namespace MobileGL::MG_Pipe {
|
||||
// inline that no caller in such a build ever has a reason to call twice - the disaggregated
|
||||
// arm above is the real one.
|
||||
inline PipeInputs& MGPipeClientInputs() { return gPipeInputs; }
|
||||
// P13 W5: one block, no client-role stamp to withdraw - and no server role to be on.
|
||||
inline void MGPipeClientClearVerbBoundary() {}
|
||||
inline constexpr Bool MGPipeServerArm() { return false; }
|
||||
#endif
|
||||
|
||||
// Every field has storage or is forwarded, and nothing else.
|
||||
@@ -1017,7 +1021,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// The docs budget ~20 KB; the block is a few KB.
|
||||
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ============================================================================
|
||||
// P5: the server-side verb stamp, and the counter that sizes what it leaves behind
|
||||
// ============================================================================
|
||||
@@ -1092,7 +1095,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// object or write into the frontend. This is what puts them in `rsp` and, under
|
||||
// MOBILEGL_IPC_STRICT_ERRORS=1, makes them Fatal like any other BARRIER-PULLED row.
|
||||
void MGPipeStickyForwardPull(MGPipeInputField field);
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Record/ApplyRoleBackend.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
|
||||
|
||||
// P13 W5: WHOSE BACKEND ANSWERS A CAPABILITY QUESTION ON THE APPLY SIDE. Under a transport the
|
||||
// server's own backend (ServerLoop's) is the authority for the records it applies; the record arm
|
||||
// asked MG_Remote for it by name, which a library without MG_Remote cannot. The answer now comes
|
||||
// through this hook, which ServerLoop.cpp installs at static init. Without it - a library without
|
||||
// a transport - the answer is null, which is exactly what a monolith process got before (a
|
||||
// monolith has no server loop, so its server backend was always null).
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
class BackendObject;
|
||||
|
||||
using ApplyRoleBackendHook = BackendObject* (*)();
|
||||
inline constinit ApplyRoleBackendHook gApplyRoleBackendHook = nullptr;
|
||||
|
||||
inline BackendObject* ApplyRoleBackend() {
|
||||
return gApplyRoleBackendHook != nullptr ? gApplyRoleBackendHook() : nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Record/RecordVerbSink.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
|
||||
|
||||
// P13 W5: THE RECORD VERBS' CONSUMER. The verbs no MGPipeApply* entry point takes (clear, blit, the
|
||||
// readbacks, the draws, dispatch, copies, image binds, XFB, mipmaps) end in the backend's own call
|
||||
// through the private GlobalBackendFunctionsTable this sink holds. Under a transport the server's
|
||||
// ServerVerbSink (MG_Remote/Server/PipeApplier.h) is this plus the session's verbs; the record
|
||||
// arm's in-process port drives this class directly, which is what lets a library without a
|
||||
// transport run the record arm.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeVerbSink.h>
|
||||
|
||||
namespace MobileGL::MG_Record {
|
||||
|
||||
class RecordVerbSink : public MG_Pipe::MGPipeVerbSink {
|
||||
public:
|
||||
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
|
||||
// verb that arrives before then declines by name rather than dereferencing.
|
||||
void SetBackend(MG_Backend::BackendObject* backend);
|
||||
MG_Backend::BackendObject* Backend() const { return m_backend; }
|
||||
void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; }
|
||||
|
||||
Bool OnClear(const MG_Pipe::MGPClear& clear) override;
|
||||
Bool OnBlit(const MG_Pipe::MGPBlit& blit) override;
|
||||
Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||
MG_Pipe::MGPipeReplySink* replies) override;
|
||||
Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||
MG_Pipe::MGPipeReplySink* replies) override;
|
||||
// P9 (CONTRACT-P9.md §1): the pack-buffer halves. Nothing is answered; the tight pixels
|
||||
// land in record.Dst through the applier's buffer-write gate.
|
||||
Bool OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override;
|
||||
Bool OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override;
|
||||
Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
|
||||
const MG_Pipe::MGHostSpan* userIndices,
|
||||
const MG_Pipe::MGPDrawIndirect* indirect) override;
|
||||
|
||||
// ---- P5b (MG_Remote/CONTRACT-P5B.md): one override per row a migration package owns.
|
||||
// At the contract commit EVERY BODY BELOW IS A STUB that dies
|
||||
// Fatal{UnmigratedVerb, "<GL slot>"} by the slot's own name - the same line the client's
|
||||
// class-C table raises and the census greps - so a client flipped ahead of its server
|
||||
// half aborts by name rather than rendering nothing, and the census on this head is
|
||||
// unchanged (the client refuses first). The owning package replaces the body.
|
||||
//
|
||||
// i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion
|
||||
// ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"),
|
||||
// OnSetStorageBlockBinding ("ShaderStorageBlockBinding")
|
||||
// t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput /
|
||||
// OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies
|
||||
// now: the backend call the contract names, the null-slot DECLINE, a tally.
|
||||
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
|
||||
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
|
||||
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the
|
||||
// instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots
|
||||
// produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names)
|
||||
Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override;
|
||||
Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override;
|
||||
Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override;
|
||||
Bool OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) override;
|
||||
Bool OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
|
||||
const char* name) override;
|
||||
Bool OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) override;
|
||||
Bool OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) override;
|
||||
Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
|
||||
Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
|
||||
Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override;
|
||||
Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override;
|
||||
Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override;
|
||||
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
|
||||
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
|
||||
|
||||
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
|
||||
// split build is also what a scenario that ran entirely on the monolith path looks
|
||||
// like (R-16: a probe may not arm against a stub).
|
||||
Uint64 Clears() const { return m_clears; }
|
||||
Uint64 Draws() const { return m_draws; }
|
||||
Uint64 Readbacks() const { return m_readbacks; }
|
||||
Uint64 Blits() const { return m_blits; }
|
||||
Uint64 ReadbackBytes() const { return m_readbackBytes; }
|
||||
// P9: pack-buffer readbacks that landed in their buffer (both ops), and the buffer-write
|
||||
// records they took. Readbacks() above counts only the reply-slot form.
|
||||
Uint64 BufferReadbacks() const { return m_bufferReadbacks; }
|
||||
Uint64 BufferReadbackWrites() const { return m_bufferReadbackWrites; }
|
||||
// ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a
|
||||
// split build "the scenario passed" is also what a scenario that never left the
|
||||
// monolith path looks like - so the lane asserts the number that only this sink can
|
||||
// move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry
|
||||
// points and memory_barrier both barrier ones, and the sink is where they separate.
|
||||
Uint64 ImageBinds() const { return m_imageBinds; }
|
||||
Uint64 Dispatches() const { return m_dispatches; }
|
||||
Uint64 MemoryBarriers() const { return m_memoryBarriers; }
|
||||
Uint64 ImageCopies() const { return m_imageCopies; }
|
||||
Uint64 StorageBlockBindings() const { return m_storageBlockBindings; }
|
||||
// ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal
|
||||
// the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from
|
||||
// DstSize is exactly the heap overflow codex 1 found, one field over.
|
||||
Uint64 ReadbackScratchBytes() const { return static_cast<Uint64>(m_readbackScratch.size()); }
|
||||
|
||||
// ---- P5b d1: what the LAST draw_vbo record carried, as the sink saw it ---------------
|
||||
//
|
||||
// Recorded BEFORE the backend is consulted, so a process with no backend object (every
|
||||
// unit case) can still assert the wire's fields rather than only that a draw "was
|
||||
// declined": the record's head, its first range, its indirect block, and whether a
|
||||
// user-index span rode with it and how many bytes it named. Nothing here outlives the
|
||||
// call except these copies (rule C: the pointers the sink was handed are not kept).
|
||||
struct LastDrawRecord {
|
||||
MG_Pipe::MGPDrawInfo Info{};
|
||||
MG_Pipe::MGPDrawRange FirstRange{};
|
||||
MG_Pipe::MGPDrawIndirect Indirect{};
|
||||
Uint64 UserIndexBytes = 0;
|
||||
Bool HadUserIndices = false;
|
||||
Bool HadIndirect = false;
|
||||
};
|
||||
const LastDrawRecord& LastDraw() const { return m_lastDraw; }
|
||||
// draw_vbo records seen, applied or declined; Draws() above counts only the applied.
|
||||
Uint64 DrawRecords() const { return m_drawRecords; }
|
||||
// P5b t2's tallies, for the same reason the five above exist (R-16): under split "the
|
||||
// scenario passed" is also what a scenario that ran entirely on the monolith path looks
|
||||
// like, so a lane that wants to say the XFB spans CROSSED has to read a counter the
|
||||
// server moved. Spans counts Begin and End together - they are one span and a lane that
|
||||
// saw only one of them has a bug the two-counter version would have hidden behind a
|
||||
// sum; controls counts Pause and Resume; binds and patch parameters count their own.
|
||||
Uint64 StreamOutputSpans() const { return m_streamOutputSpans; }
|
||||
Uint64 StreamOutputControls() const { return m_streamOutputControls; }
|
||||
Uint64 StreamOutputBinds() const { return m_streamOutputBinds; }
|
||||
Uint64 PatchParameters() const { return m_patchParameters; }
|
||||
|
||||
protected:
|
||||
// A backend change: the subclass lets go of what it held on the old one.
|
||||
virtual void OnBackendChanged() {}
|
||||
// The wire's user-index span (a draw whose indices rode in a stage segment). The record
|
||||
// arm's port never hands one over - its indices are in the client's own buffer - so only
|
||||
// the wire's subclass, which can check a span against its segments, answers true.
|
||||
virtual Bool CheckUserIndices(const MG_Pipe::MGPDrawInfo&, const MG_Pipe::MGPDrawRange*,
|
||||
const MG_Pipe::MGHostSpan&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
|
||||
|
||||
// read_pixels' read, shared by the reply form and the pack-buffer form: the bound read
|
||||
// framebuffer, NEUTRAL pack state (ID-49), `tight` bytes into m_readbackScratch. False
|
||||
// when this server has no GL.ReadPixels - the caller owns what that means.
|
||||
Bool ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight);
|
||||
// get_texture_image's read, shared the same way: the whole level into `bytes`.
|
||||
Bool ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector<Uint8>& bytes);
|
||||
// P9: `tight` holds Src's pixels, tightly packed; land them in record.Dst at the
|
||||
// record's layout through MGPipeApplyResourceSubData, one write per contiguous run. The
|
||||
// layout was checked before the read (PipeApplier.cpp ReadbackToBufferLayoutFault).
|
||||
Bool LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight,
|
||||
Uint64 bytesPerPixel);
|
||||
|
||||
MG_Backend::BackendObject* m_backend = nullptr;
|
||||
Uint64 m_clears = 0;
|
||||
Uint64 m_draws = 0;
|
||||
Uint64 m_readbacks = 0;
|
||||
Uint64 m_blits = 0;
|
||||
Uint64 m_readbackBytes = 0;
|
||||
Uint64 m_bufferReadbacks = 0;
|
||||
Uint64 m_bufferReadbackWrites = 0;
|
||||
Uint64 m_imageBinds = 0;
|
||||
Uint64 m_dispatches = 0;
|
||||
Uint64 m_memoryBarriers = 0;
|
||||
Uint64 m_imageCopies = 0;
|
||||
Uint64 m_storageBlockBindings = 0;
|
||||
Uint64 m_streamOutputSpans = 0;
|
||||
Uint64 m_streamOutputControls = 0;
|
||||
Uint64 m_streamOutputBinds = 0;
|
||||
Uint64 m_patchParameters = 0;
|
||||
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||
// between them. Grown, never shrunk, and never handed out past the call.
|
||||
Vector<Uint8> m_readbackScratch;
|
||||
// Server-declared LinkTerms.maxReplyBytes, copied from the attached server link.
|
||||
Uint64 m_maxReplyBytes = 0;
|
||||
// P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges
|
||||
// per record (rule C: bounded by NumDraws, owned here, never handed out past the call),
|
||||
// and the last-record witness above.
|
||||
Vector<GLsizei> m_multiCounts;
|
||||
Vector<GLint> m_multiFirsts;
|
||||
Vector<const void*> m_multiOffsets;
|
||||
Vector<GLint> m_multiBaseVertices;
|
||||
LastDrawRecord m_lastDraw{};
|
||||
Uint64 m_drawRecords = 0;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Record
|
||||
+23
-7
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/StagedShadow.h
|
||||
// MobileGL - MobileGL/MG_Backend/Record/StagedShadow.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
|
||||
@@ -38,9 +38,9 @@
|
||||
#include <Includes.h>
|
||||
|
||||
// P7 wave 0 / Ph slice (2): RequireCovered's death goes through Session::Fail like every other
|
||||
// one. Not an out-of-line helper - this header is already inside MG_Remote, so it may name
|
||||
// MG_Remote's own funnel, and check_include_closure.py's four probes do not reach it.
|
||||
#include <MG_Remote/FatalFunnel.h>
|
||||
// one. P13 W5: the store left MG_Remote (the record arm runs without it), so it dies through
|
||||
// MG_Pipe's record fail seam, which FatalFunnel.cpp points at SessionFail wherever MG_Remote is linked.
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
namespace MobileGL::MG_Record {
|
||||
|
||||
// P14: WHOSE STAGED BYTES ARE THESE? The two server stores below are one per process, but the
|
||||
// in-process display server serves several sessions in that process at once, and a session's
|
||||
@@ -120,6 +120,22 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return shadow.Bytes.data();
|
||||
}
|
||||
|
||||
// P13 W4a: a COPY even in a non-copying store - the fallback for a GPU-written store on
|
||||
// monolith's record arm that never recorded the shadow base it aliases (Managers.cpp's
|
||||
// RefreshAliasedStoreFromGpu). Whole-store, covered, and dropped with the key.
|
||||
const Uint8* AdoptPrivateCopy(const void* key, SizeT width, const void* bytes, SizeT size) {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
Shadow& shadow = m_shadows[key];
|
||||
shadow.SessionKey = CurrentStagedBucket().SessionKey;
|
||||
if (shadow.Bytes.size() < std::max(width, size)) shadow.Bytes.resize(std::max(width, size), 0);
|
||||
if (size != 0 && bytes != nullptr) {
|
||||
std::memcpy(shadow.Bytes.data(), bytes, size);
|
||||
CoverageAdd(shadow.Covered, 0, size);
|
||||
}
|
||||
m_any.store(true, std::memory_order_release);
|
||||
return shadow.Bytes.data();
|
||||
}
|
||||
|
||||
void Drop(const void* key) {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -156,7 +172,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
if (it == m_shadows.end() || it->second.Bytes.data() != hostBase) return;
|
||||
if (CoverageHas(it->second.Covered, start, end)) return;
|
||||
// Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0).
|
||||
SessionFail(MGFatalFamily::StageSnapshotTooNarrow,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::StageSnapshotTooNarrow,
|
||||
"MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants "
|
||||
"[%zu, %zu) of a buffer whose staged coverage does not include it. Under "
|
||||
"split the authoritative shadow is SERVER-OWNED (rule C) and "
|
||||
@@ -246,4 +262,4 @@ namespace MobileGL::MG_Remote::Server {
|
||||
std::atomic<Bool> m_any{false};
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
} // namespace MobileGL::MG_Record
|
||||
+121
-47
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/StagedTextureStore.h
|
||||
// MobileGL - MobileGL/MG_Backend/Record/StagedTextureStore.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
|
||||
@@ -100,10 +100,10 @@
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
// StagedShadowStore::CoverageAdd / CoverageHas: ONE spelling of the covered set, so the texture
|
||||
// half's runs and the buffer half's ranges cannot drift apart on what "covered" means.
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#include <MG_Backend/Record/StagedShadow.h>
|
||||
// P7 wave 0 / Ph slice (2): RequireLevelBytes' death goes through Session::Fail. Named here
|
||||
// rather than left to StagedShadow.h's copy of the same include.
|
||||
#include <MG_Remote/FatalFunnel.h>
|
||||
#include <MG_Pipe/PipeSessionFail.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
@@ -114,11 +114,9 @@
|
||||
#include <mutex>
|
||||
#include <limits>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
namespace MobileGL::MG_Record {
|
||||
|
||||
// §1's server-side per-level extent (CONTRACT-P5C table 0): max(1, base_extent >> level)
|
||||
// per SHRINKING axis, with an array texture's layer count fixed - it is not a dimension of
|
||||
@@ -298,6 +296,20 @@ namespace MobileGL::MG_Remote::Server {
|
||||
explicit StagedTextureStore(Bool copies) : m_copies(copies) {}
|
||||
|
||||
Bool CopiesIntoServerStorage() const { return m_copies; }
|
||||
// P13 W4b: THE ALIASING MODE - monolith's record arm. The store keeps every piece of
|
||||
// bookkeeping it keeps under split (definitions, extents, the exact covered runs, the
|
||||
// driver-written and GPU-dirty marks), but a level's bytes are the FRONTEND's level shadow
|
||||
// in place: a run records the address it arrived at (minus its offset in the level) and
|
||||
// nothing is copied. The readers then ask exactly what they ask under split - is the
|
||||
// level covered, what are its bytes - and get the same answers from memory that is
|
||||
// already there. This store never writes frontend memory: a copy it follows
|
||||
// (FollowCopy) first takes the destination level into a server-owned copy, which is then
|
||||
// what AdoptClientBoxes lands the client's later boxes on - exactly the split store's
|
||||
// shape for that level - and a copy it cannot follow marks the destination
|
||||
// driver-written, which sends its readers to the driver.
|
||||
Bool Aliases() const { return !m_copies && MG_Config::RecordArmAliasesFrontend(); }
|
||||
// Does this store answer at all: a copying store, or an aliasing one.
|
||||
Bool Holds() const { return m_copies || Aliases(); }
|
||||
void SetDeviceLimits(Int maxTextureSize, Int max3DTextureSize, Int maxCubeMapTextureSize,
|
||||
Int maxArrayTextureLayers, Int maxTextureBufferSize) {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -330,14 +342,17 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// reallocates it, which is StagedShadowStore::Adopt's note one level down. Nothing needs
|
||||
// the base while a level is still being assembled - the readers run at the barrier, after
|
||||
// the last piece - so callers here ignore it.
|
||||
// `frontendShadow`: the bytes ARE the frontend's level shadow (a client record's run) - the
|
||||
// one source an aliasing store may keep by address. Every other adoption (the server's own
|
||||
// readbacks, regenerations and promotions) is server memory that dies with the call, and is
|
||||
// copied even by an aliasing store.
|
||||
const Uint8* AdoptRun(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
|
||||
if (!m_copies) return nullptr;
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize, Bool frontendShadow = false) {
|
||||
if (!Holds()) return nullptr;
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)];
|
||||
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A run can be the first piece of a redefined level. Old coverage
|
||||
// belongs to the previous coordinate system even when the new run
|
||||
// happens to touch its old range; retaining it invents bytes that
|
||||
@@ -348,9 +363,9 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// belt that keeps B4's rule if that refusal is ever relaxed.
|
||||
if (!shadow.Defined || shadow.Extent != extent) {
|
||||
shadow.Bytes.clear();
|
||||
shadow.Alias = nullptr;
|
||||
shadow.Covered.clear();
|
||||
}
|
||||
#endif
|
||||
if (shadow.HoldsFollowedCopy) {
|
||||
// P8-E: a run landed whole over a followed copy (AdoptClientBoxes is the spelling
|
||||
// that keeps it), so the bytes no longer hold what the driver does.
|
||||
@@ -358,7 +373,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
m_driverWritten[bucketed] = true;
|
||||
}
|
||||
shadow.GpuDirty = false;
|
||||
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize);
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize, frontendShadow);
|
||||
}
|
||||
|
||||
// THE WHOLE-LEVEL SPELLING: the run is the level's COMPLETE current content, beginning at
|
||||
@@ -368,18 +384,20 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// into; under monolith both do nothing and return nullptr, and the caller
|
||||
// (Ops_H_TextureSubData) has already returned before reaching either.
|
||||
const Uint8* Adopt(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
const void* bytes, SizeT byteSize) {
|
||||
if (!m_copies) return nullptr;
|
||||
const void* bytes, SizeT byteSize, Bool frontendShadow = false) {
|
||||
if (!Holds()) return nullptr;
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)];
|
||||
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
|
||||
shadow.GpuDirty = false;
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
// P8-E: the caller states these bytes ARE the level, which a followed copy is part of.
|
||||
shadow.HoldsFollowedCopy = false;
|
||||
shadow.Bytes.clear();
|
||||
shadow.Alias = nullptr;
|
||||
shadow.Covered.clear();
|
||||
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize);
|
||||
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize, frontendShadow);
|
||||
}
|
||||
|
||||
// A storage-defining respecify named this level: it EXISTS from here on, at the exact
|
||||
@@ -408,7 +426,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
Uint8 resourceTarget = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
|
||||
Uint32 internalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8)) {
|
||||
if (!m_copies) return;
|
||||
if (!Holds()) return;
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
Uint64 byteBound = 0;
|
||||
@@ -420,7 +438,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
if (!validExtent ||
|
||||
(supportsSubData && sizedFormat && !emptyExtent && !StagedTextureDeclaredLevelByteBound(
|
||||
resourceTarget, internalFormat, extent, m_deviceLimits, &byteBound))) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.NoteLevelDefined\"} - "
|
||||
"the accepted respecify has no valid server-bounded texture level "
|
||||
"(key=%llu, target=%u, level=%u, extent=%d,%d,%d, internalFormat=%u)",
|
||||
@@ -431,11 +449,14 @@ namespace MobileGL::MG_Remote::Server {
|
||||
if (!shadow.Defined || shadow.Extent != extent || shadow.InternalFormat != internalFormat ||
|
||||
shadow.ResourceTarget != resourceTarget || shadow.DeclaredByteBound != byteBound) {
|
||||
shadow.Bytes.clear();
|
||||
// An aliased level's base is the frontend allocation this redefinition replaced.
|
||||
shadow.Alias = nullptr;
|
||||
// The covered set is a statement about THIS coordinate system, so it goes with
|
||||
// the bytes it described.
|
||||
shadow.Covered.clear();
|
||||
shadow.GpuDirty = false;
|
||||
shadow.HoldsFollowedCopy = false;
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
shadow.Extent = extent;
|
||||
shadow.InternalFormat = internalFormat;
|
||||
@@ -453,6 +474,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.erase(bucketed);
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// T5's dirty mark: a GPU-side generation made this level's texels newer than any
|
||||
@@ -460,18 +482,26 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// never crossed - and the mark is what answers "dirty region, level has no pending
|
||||
// upload" without asking the client (§2.2's last row).
|
||||
void MarkLevelGpuDirty(Uint64 key, Uint16 uploadTarget, Uint16 level, Bool dirty) {
|
||||
if (!m_copies) return;
|
||||
if (!Holds()) return;
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty;
|
||||
if (!dirty) m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
m_any.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// P14: moves whenever a level's GPU-dirty mark may have gone false - a CPU run or adoption
|
||||
// over it, a redefinition, an explicit clear, or a level/shadow dropped. A caller that
|
||||
// re-marks the same levels every draw (Magma's attachments) may skip the repeat while it
|
||||
// has not moved: marking is idempotent until something clears it.
|
||||
Uint64 GpuDirtyClearGeneration() const { return m_gpuDirtyClears.load(std::memory_order_relaxed); }
|
||||
|
||||
void Drop(Uint64 key) {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const ShadowKey bucketed = Bucketed(key);
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.erase(bucketed);
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
m_driverWritten.erase(bucketed);
|
||||
}
|
||||
|
||||
@@ -485,10 +515,12 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (session == 0) {
|
||||
m_shadows.clear();
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
m_driverWritten.clear();
|
||||
return;
|
||||
}
|
||||
EraseSession(m_shadows, session);
|
||||
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
|
||||
EraseSession(m_driverWritten, session);
|
||||
}
|
||||
|
||||
@@ -510,7 +542,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// next run for it is the CLIENT's shadow, which never saw the copy: AdoptClientBoxes
|
||||
// lands only the texels that run says the client wrote.
|
||||
void MarkDriverWritten(Uint64 key) {
|
||||
if (!m_copies) return;
|
||||
if (!Holds()) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_driverWritten[Bucketed(key)] = true;
|
||||
m_any.store(true, std::memory_order_release);
|
||||
@@ -535,7 +567,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool FollowCopy(Uint64 srcKey, Uint16 srcTarget, Uint16 srcLevel, const IntVec3& srcOrigin,
|
||||
Uint64 dstKey, Uint16 dstTarget, Uint16 dstLevel, const IntVec3& dstOrigin,
|
||||
const IntVec3& size) {
|
||||
if (!m_copies) return false;
|
||||
if (!Holds()) return false;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_driverWritten.find(Bucketed(srcKey)) != m_driverWritten.end()) return false;
|
||||
const LevelShadow* src = FindLevel(srcKey, srcTarget, srcLevel);
|
||||
@@ -549,14 +581,22 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// Gathered first, so a copy within one level cannot read rows it has already written.
|
||||
const SizeT rowBytes = static_cast<SizeT>(size.x()) * texel;
|
||||
Vector<Uint8> rows(rowBytes * static_cast<SizeT>(size.y()) * static_cast<SizeT>(size.z()));
|
||||
const Uint8* srcBytes = LevelData(*src);
|
||||
for (Int z = 0; z < size.z(); ++z) {
|
||||
for (Int y = 0; y < size.y(); ++y) {
|
||||
std::memcpy(rows.data() + (static_cast<SizeT>(z) * size.y() + y) * rowBytes,
|
||||
src->Bytes.data() + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y,
|
||||
srcOrigin.z() + z, texel),
|
||||
srcBytes + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y,
|
||||
srcOrigin.z() + z, texel),
|
||||
rowBytes);
|
||||
}
|
||||
}
|
||||
if (dst->Alias != nullptr) {
|
||||
// P13 W4b, the aliasing mode: the destination's bytes are the frontend's level
|
||||
// shadow, which never sees the copy and which this store does not write. The level
|
||||
// becomes server-owned here, from the bytes it stood for, as under split.
|
||||
dst->Bytes.assign(dst->Alias, dst->Alias + static_cast<SizeT>(dst->DeclaredByteBound));
|
||||
dst->Alias = nullptr;
|
||||
}
|
||||
if (!LevelComplete(*dst)) {
|
||||
dst->Bytes.assign(static_cast<SizeT>(dst->DeclaredByteBound), Uint8{0});
|
||||
dst->Covered.clear();
|
||||
@@ -589,10 +629,13 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool AdoptClientBoxes(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize,
|
||||
const ClientTexelBox* boxes, SizeT boxCount) {
|
||||
if (!m_copies || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false;
|
||||
if (!Holds() || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
LevelShadow* shadow = FindLevelMutable(key, uploadTarget, level);
|
||||
if (shadow == nullptr || !shadow->HoldsFollowedCopy || !LevelComplete(*shadow) || shadow->Extent != extent) {
|
||||
// Alias is null on a level holding a followed copy (FollowCopy took it server-owned);
|
||||
// checked anyway, because the boxes below are written into Bytes.
|
||||
if (shadow == nullptr || !shadow->HoldsFollowedCopy || shadow->Alias != nullptr || !LevelComplete(*shadow) ||
|
||||
shadow->Extent != extent) {
|
||||
return false;
|
||||
}
|
||||
const SizeT texel = LevelTexelBytes(*shadow);
|
||||
@@ -645,17 +688,12 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// reads the level image WHOLE (LevelByteSize / the sync's texel base), so a level with
|
||||
// one piece missing is as unreadable as one with none.
|
||||
const Uint8* RequireLevelBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, const char* site) const {
|
||||
if (!m_copies) return nullptr;
|
||||
if (!Holds()) return nullptr;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
if (shadow != nullptr && shadow->Defined && shadow->DeclaredByteBound != 0 &&
|
||||
shadow->Bytes.size() == shadow->DeclaredByteBound &&
|
||||
StagedShadowStore::CoverageHas(shadow->Covered, 0,
|
||||
static_cast<SizeT>(shadow->DeclaredByteBound))) {
|
||||
return shadow->Bytes.data();
|
||||
}
|
||||
if (shadow != nullptr && LevelComplete(*shadow)) return LevelData(*shadow);
|
||||
// Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0).
|
||||
SessionFail(MGFatalFamily::StageSnapshotTooNarrow,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::StageSnapshotTooNarrow,
|
||||
"MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the "
|
||||
"bytes of (uploadTarget=%u, level=%u) and the server's staged shadow has no "
|
||||
"COMPLETE covered run for it. Under split the authoritative shadow is "
|
||||
@@ -675,7 +713,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// covered level answers, and nothing is fatal: an uncovered box is the caller's to skip.
|
||||
const Uint8* CoveredBoxBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, Int x, Int y, Uint32 w, Uint32 h,
|
||||
SizeT texel, Int* rowTexels) const {
|
||||
if (!m_copies) return nullptr;
|
||||
if (!Holds()) return nullptr;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
if (shadow == nullptr || !shadow->Defined || x < 0 || y < 0 || w == 0 || h == 0 ||
|
||||
@@ -684,11 +722,11 @@ namespace MobileGL::MG_Remote::Server {
|
||||
for (Uint32 row = 0; row < h; ++row) {
|
||||
const SizeT begin = TexelOffset(shadow->Extent, x, y + static_cast<Int>(row), 0, texel);
|
||||
const SizeT end = begin + static_cast<SizeT>(w) * texel;
|
||||
if (end > shadow->Bytes.size() || !StagedShadowStore::CoverageHas(shadow->Covered, begin, end))
|
||||
if (end > LevelBytesHeld(*shadow) || !StagedShadowStore::CoverageHas(shadow->Covered, begin, end))
|
||||
return nullptr;
|
||||
}
|
||||
*rowTexels = shadow->Extent.x();
|
||||
return shadow->Bytes.data();
|
||||
return LevelData(*shadow);
|
||||
}
|
||||
|
||||
// Diagnostics the sync path and the unit cases read, so that a check can assert WHAT
|
||||
@@ -697,7 +735,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
if (shadow == nullptr || !shadow->Defined || shadow->DeclaredByteBound == 0 ||
|
||||
shadow->Bytes.size() != shadow->DeclaredByteBound) return false;
|
||||
LevelBytesHeld(*shadow) != shadow->DeclaredByteBound) return false;
|
||||
// THE WHOLE LEVEL, not merely the currently assembled prefix: all runs must cover
|
||||
// the server-derived bound before a reader can treat the level as complete.
|
||||
return StagedShadowStore::CoverageHas(
|
||||
@@ -737,7 +775,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
if (shadow == nullptr || !shadow->Defined) return 0;
|
||||
return shadow->Bytes.size();
|
||||
return LevelBytesHeld(*shadow);
|
||||
}
|
||||
Uint64 LevelDeclaredByteBound(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
|
||||
if (!m_any.load(std::memory_order_acquire)) return 0;
|
||||
@@ -820,7 +858,20 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool GpuDirty = false;
|
||||
// P8-E: FollowCopy wrote server-side texels into Bytes that the client's shadow lacks.
|
||||
Bool HoldsFollowedCopy = false;
|
||||
// P13 W4b, the aliasing mode: the level's first byte in the FRONTEND's level shadow,
|
||||
// learned from the first run that arrived (its address minus its offset in the level).
|
||||
// Null until then, and nulled with the coordinate system it belongs to. Bytes stays
|
||||
// empty in this mode; LevelData / LevelBytesHeld are the two readers' spellings.
|
||||
const Uint8* Alias = nullptr;
|
||||
};
|
||||
static const Uint8* LevelData(const LevelShadow& shadow) {
|
||||
return shadow.Alias != nullptr ? shadow.Alias : shadow.Bytes.data();
|
||||
}
|
||||
// How many bytes of the level image this store can hand out: an aliased level is the
|
||||
// frontend's whole level allocation, which is exactly its declared bound.
|
||||
static SizeT LevelBytesHeld(const LevelShadow& shadow) {
|
||||
return shadow.Alias != nullptr ? static_cast<SizeT>(shadow.DeclaredByteBound) : shadow.Bytes.size();
|
||||
}
|
||||
struct TextureShadow {
|
||||
ska::flat_hash_map<Uint32, LevelShadow> Levels;
|
||||
};
|
||||
@@ -835,14 +886,14 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// `InternalFormat == 0`, but 0 is TextureInternalFormat::R8, so every R8 upload read
|
||||
// as "arrived before the level was declared".
|
||||
if (!shadow.Defined) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
|
||||
"resource_subdata arrived before the server declared texture level "
|
||||
"(key=%llu, uploadTarget=%u, level=%u)",
|
||||
static_cast<unsigned long long>(key), uploadTarget, level);
|
||||
}
|
||||
if (!StagedTextureTargetSupportsSubData(shadow.ResourceTarget)) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"} - "
|
||||
"resource_subdata is not legal for the server-declared texture target "
|
||||
"(key=%llu, uploadTarget=%u, level=%u, target=%u)",
|
||||
@@ -850,7 +901,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
static_cast<Uint32>(shadow.ResourceTarget));
|
||||
}
|
||||
if (shadow.DeclaredByteBound == 0) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelBound\"} - "
|
||||
"resource_subdata into a byteless declared level: its extent is empty or "
|
||||
"its format has no uncompressed size on this server "
|
||||
@@ -860,7 +911,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
shadow.InternalFormat);
|
||||
}
|
||||
if (shadow.Extent == extent) return;
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
|
||||
"resource_subdata extent disagrees with the server-declared texture level "
|
||||
"(key=%llu, uploadTarget=%u, level=%u, declared=%d,%d,%d, record=%d,%d,%d)",
|
||||
@@ -872,12 +923,12 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// Check the run's exclusive end against the extent recorded by the server's prior
|
||||
// resource_respecify before any cast, coverage edit, or vector growth.
|
||||
const Uint8* CopyRunInto(LevelShadow& shadow, Uint64 key, Uint16 uploadTarget, Uint16 level,
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize, Bool frontendShadow) {
|
||||
if (bytes != nullptr && byteSize != 0) {
|
||||
constexpr Uint64 max = std::numeric_limits<Uint64>::max();
|
||||
const Uint64 runBytes = static_cast<Uint64>(byteSize);
|
||||
if (imageOffset > max - runBytes) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
|
||||
"texture run end overflows (handle=%llu, uploadTarget=%u, level=%u, "
|
||||
"offset=%llu, size=%llu)",
|
||||
@@ -888,7 +939,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const Uint64 end = imageOffset + runBytes;
|
||||
if (end > shadow.DeclaredByteBound ||
|
||||
end > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption,
|
||||
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
|
||||
"texture run exceeds the server-declared level byte bound "
|
||||
"(handle=%llu, uploadTarget=%u, level=%u, offset=%llu, size=%llu, "
|
||||
@@ -901,11 +952,35 @@ namespace MobileGL::MG_Remote::Server {
|
||||
const auto* raw = static_cast<const Uint8*>(bytes);
|
||||
const SizeT begin = static_cast<SizeT>(imageOffset);
|
||||
const SizeT needed = static_cast<SizeT>(end);
|
||||
if (!m_copies && frontendShadow) {
|
||||
// The aliasing mode: the run IS the frontend level shadow at `begin`, so the
|
||||
// level's base is one subtraction away and nothing is copied. Every run of one
|
||||
// level comes from one allocation; a run from another is a level redefined
|
||||
// without a respecify (or one the server had been holding a copy of), which
|
||||
// would leave earlier coverage describing other memory - so it starts the
|
||||
// coverage over.
|
||||
const Uint8* base = raw - begin;
|
||||
if (shadow.Alias != base) {
|
||||
shadow.Covered.clear();
|
||||
shadow.Bytes.clear();
|
||||
shadow.Alias = base;
|
||||
}
|
||||
StagedShadowStore::CoverageAdd(shadow.Covered, begin, needed);
|
||||
m_any.store(true, std::memory_order_release);
|
||||
return shadow.Alias;
|
||||
}
|
||||
if (shadow.Alias != nullptr) {
|
||||
// A server copy landing on an aliased level: the level becomes server-owned, so
|
||||
// the bytes it already stands for are taken into it first.
|
||||
shadow.Bytes.assign(shadow.Alias, shadow.Alias + static_cast<SizeT>(shadow.DeclaredByteBound));
|
||||
shadow.Alias = nullptr;
|
||||
}
|
||||
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
|
||||
std::memcpy(shadow.Bytes.data() + begin, raw, byteSize);
|
||||
StagedShadowStore::CoverageAdd(shadow.Covered, begin, needed);
|
||||
}
|
||||
m_any.store(true, std::memory_order_release);
|
||||
if (shadow.Alias != nullptr) return shadow.Alias;
|
||||
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
|
||||
}
|
||||
|
||||
@@ -927,7 +1002,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// that does not divide evenly is a level this cannot address texel by texel.
|
||||
static Bool LevelComplete(const LevelShadow& shadow) {
|
||||
return shadow.Defined && shadow.DeclaredByteBound != 0 &&
|
||||
shadow.Bytes.size() == shadow.DeclaredByteBound &&
|
||||
LevelBytesHeld(shadow) == shadow.DeclaredByteBound &&
|
||||
StagedShadowStore::CoverageHas(shadow.Covered, 0, static_cast<SizeT>(shadow.DeclaredByteBound));
|
||||
}
|
||||
static SizeT LevelTexelBytes(const LevelShadow& shadow) {
|
||||
@@ -963,9 +1038,9 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one
|
||||
// acquire load of a never-written flag rather than a mutex and two hash lookups.
|
||||
std::atomic<Bool> m_any{false};
|
||||
std::atomic<Uint64> m_gpuDirtyClears{0};
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ONE PER PROCESS, and its copying arm is decided ONCE at first use - StagedShadow's
|
||||
// ServerStaged() ruling verbatim: the two arms hold the authoritative bytes in DIFFERENT
|
||||
// places, so an answer that changed mid-run would strand every level already staged.
|
||||
@@ -978,6 +1053,5 @@ namespace MobileGL::MG_Remote::Server {
|
||||
*new StagedTextureStore(MG_Config::Transport != MG_Config::TransportMode::Monolith);
|
||||
return store;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
} // namespace MobileGL::MG_Record
|
||||
@@ -46,12 +46,64 @@
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
#if defined(__unix__) || defined(__linux__) || defined(__ANDROID__)
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Impl::EGLImpl {
|
||||
namespace {
|
||||
// MOBILEGL_FPS_LOG=1 (on Android also the property debug.mobilegl.fps_log=1): one INFO line
|
||||
// about every two seconds with the frames this process presented, their mean frame time and
|
||||
// the presenting thread's CPU per frame. A frame rate for an app-hosted game that shows none
|
||||
// of its own (a launcher's Minecraft with the debug overlay closed, which is itself a cost).
|
||||
// Off, a swap pays one load and branch.
|
||||
Bool FrameRateLogEnabled() {
|
||||
static const Bool enabled = [] {
|
||||
const char* value = std::getenv("MOBILEGL_FPS_LOG");
|
||||
#if defined(__ANDROID__)
|
||||
char property[PROP_VALUE_MAX] = {};
|
||||
if (value == nullptr && __system_property_get("debug.mobilegl.fps_log", property) > 0) value = property;
|
||||
#endif
|
||||
return value != nullptr && value[0] == '1';
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void NoteSwapForFrameRateLog() {
|
||||
if (!FrameRateLogEnabled()) return;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static Clock::time_point windowStart = Clock::now();
|
||||
static Uint32 frames = 0;
|
||||
static Double cpuStartMs = -1.0;
|
||||
const auto threadCpuMs = []() -> Double {
|
||||
#if defined(__unix__) || defined(__linux__) || defined(__ANDROID__)
|
||||
struct timespec now {};
|
||||
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0)
|
||||
return static_cast<Double>(now.tv_sec) * 1000.0 + static_cast<Double>(now.tv_nsec) / 1e6;
|
||||
#endif
|
||||
return -1.0;
|
||||
};
|
||||
if (cpuStartMs < 0.0) cpuStartMs = threadCpuMs();
|
||||
++frames;
|
||||
const auto now = Clock::now();
|
||||
const Double elapsedMs = std::chrono::duration<Double, std::milli>(now - windowStart).count();
|
||||
if (elapsedMs < 2000.0) return;
|
||||
const Double cpuNow = threadCpuMs();
|
||||
MGLOG_I("MobileGL fps: %.1f over %u frames (%.2f ms/frame, presenting thread CPU %.2f ms/frame)",
|
||||
1000.0 * frames / elapsedMs, frames, elapsedMs / frames,
|
||||
cpuNow >= 0.0 && cpuStartMs >= 0.0 ? (cpuNow - cpuStartMs) / frames : -1.0);
|
||||
windowStart = now;
|
||||
frames = 0;
|
||||
cpuStartMs = cpuNow;
|
||||
}
|
||||
|
||||
// SetDispatcherCurrentApiHook.
|
||||
std::atomic<EGLenum (*)()> g_dispatcherCurrentApi{nullptr};
|
||||
using EGLStateContext = MG_State::EGLState::EGLContext;
|
||||
@@ -766,6 +818,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
// A resize made between frames takes effect for the next one.
|
||||
ApplyWaylandResize(state, dpy, draw);
|
||||
#endif
|
||||
NoteSwapForFrameRateLog();
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
@@ -1606,7 +1659,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A split client's backend is the remote one, which forwards the shared-image verbs to
|
||||
// the server; the server's own apply thread and a monolith have no allocator behind them.
|
||||
return StreamGateActive() && MG_Backend::pActiveBackendObject != nullptr;
|
||||
// Nor has inproc: shared images carry memory between processes, and an inproc server is
|
||||
// this process (its server refuses them too - ServerVerbSink::OnSharedImage).
|
||||
return StreamGateActive() && MG_Config::Transport != MG_Config::TransportMode::InProcess &&
|
||||
MG_Backend::pActiveBackendObject != nullptr;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
@@ -1497,7 +1497,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (sb, CONTRACT-P5E.md §5.6). THE BIND-POINT GENERATION, and it is bumped HERE -
|
||||
// before the writes below and after every refusal above - rather than inside
|
||||
// BindingSlotRange1D, for two reasons. The slot has no back-pointer to the state
|
||||
@@ -1510,7 +1509,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The dirty bits this feeds are 15/16/17 (Tracker.h); before P5e they shuttered on the
|
||||
// buffer CONTENT aggregate, which no bind has ever moved.
|
||||
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
|
||||
#endif
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
|
||||
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
|
||||
@@ -1634,13 +1632,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (sb): the other half of BindBufferBase_State's bump - same generation, same
|
||||
// placement, same reason. A glBindBufferRange moves the point's EXTENT as well as what
|
||||
// is bound to it, and the record carries the resolved Offset/Size, so a range change
|
||||
// with no object change still has to reach the emitter.
|
||||
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
|
||||
#endif
|
||||
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
|
||||
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
@@ -1332,7 +1330,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<Uint>(bufferIndex));
|
||||
const auto& buffer = bindingPoint.GetBoundObject();
|
||||
if (buffer == nullptr) continue;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// This fixup READS the captured bytes back through the shadow, so it is the one
|
||||
// consumer that cannot simply inherit the deferral EndTransformFeedback's dropped
|
||||
// fence introduces. Under split it pays the reconciliation itself, which is the
|
||||
@@ -1344,8 +1341,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// BeginXfbCaptureForDraw does mark the capture targets - where the fence at the
|
||||
// caller still runs, so the readback it emits is pure new work on the monolith
|
||||
// path and integration-gpu cannot see it.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) buffer->SyncGpuWrites();
|
||||
#endif
|
||||
buffer->SyncGpuWrites();
|
||||
const Range1D range = bindingPoint.GetRange();
|
||||
const Uint8* mapped = buffer->MappedData();
|
||||
if (mapped == nullptr) continue;
|
||||
@@ -1386,12 +1382,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGP_FILL(EndTransformFeedback);
|
||||
endXfb();
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 (b1), the second producer the client-side GPU-write set ADDS, and it has to be
|
||||
// taken HERE - before GLContext::EndTransformFeedback clears the live bindings, since
|
||||
// a mark taken after it marks nothing.
|
||||
MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets();
|
||||
#endif
|
||||
MG_Record::MarkEndTransformFeedbackCaptureTargets();
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||
// End; the capture targets are host-coherent GPU memory, so completing
|
||||
|
||||
@@ -1307,17 +1307,6 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmaxARB, GLenum target, GLboolean rese
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferSampleLocationsfvARB, GLenum target, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferSampleLocationsfvARB, target, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferSampleLocationsfvARB, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferSampleLocationsfvARB, framebuffer, start, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EvaluateDepthValuesARB, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EvaluateDepthValuesARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteObjectARB, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteObjectARB, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, GetHandleARB, GLenum pname) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, GetHandleARB, pname)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DetachObjectARB, GLhandleARB containerObj, GLhandleARB attachedObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DetachObjectARB, containerObj, attachedObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateShaderObjectARB, GLenum shaderType) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateShaderObjectARB, shaderType)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateProgramObjectARB, void) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateProgramObjectARB, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, AttachObjectARB, GLhandleARB containerObj, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, AttachObjectARB, containerObj, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramObjectARB, GLhandleARB programObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramObjectARB, programObj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterfvARB, GLhandleARB obj, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterfvARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterivARB, GLhandleARB obj, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterivARB, obj, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInfoLogARB, GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInfoLogARB, obj, maxLength, length, infoLog)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetAttachedObjectsARB, GLhandleARB containerObj, GLsizei maxCount, GLsizei* count, GLhandleARB* obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetAttachedObjectsARB, containerObj, maxCount, count, obj)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedStringARB, GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedStringARB, type, namelen, name, strinen, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteNamedStringARB, GLint namelen, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteNamedStringARB, namelen, name)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompileShaderIncludeARB, GLuint shader, GLsizei count, const GLchar* const* path, const GLint* length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompileShaderIncludeARB, shader, count, path, length)
|
||||
@@ -1346,35 +1335,6 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WeightuivARB, GLint size, const GLuint* weig
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WeightPointerARB, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WeightPointerARB, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendARB, GLint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexBlendARB, count)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBufferSubDataARB, GLenum target, GLintptrARB offset, GLsizeiptrARB size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetBufferSubDataARB, target, offset, size, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1dARB, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1dARB, index, x)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1dvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1sARB, GLuint index, GLshort x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1sARB, index, x)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1svARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2dARB, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2dARB, index, x, y)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2dvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2sARB, GLuint index, GLshort x, GLshort y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2sARB, index, x, y)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2svARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3dARB, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3dARB, index, x, y, z)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3dvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3sARB, GLuint index, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3sARB, index, x, y, z)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3svARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NbvARB, GLuint index, const GLbyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NbvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NivARB, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NivARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NsvARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NsvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NubARB, GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NubARB, index, x, y, z, w)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NubvARB, GLuint index, const GLubyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NubvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NuivARB, GLuint index, const GLuint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NuivARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NusvARB, GLuint index, const GLushort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NusvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4bvARB, GLuint index, const GLbyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4bvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4dARB, GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4dARB, index, x, y, z, w)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4dvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4ivARB, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4ivARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4sARB, GLuint index, GLshort x, GLshort y, GLshort z, GLshort w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4sARB, index, x, y, z, w)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4svARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4ubvARB, GLuint index, const GLubyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4ubvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4uivARB, GLuint index, const GLuint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4uivARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4usvARB, GLuint index, const GLushort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4usvARB, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribdvARB, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribdvARB, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArraydvNV, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArraydvNV, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexeddNV, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexeddNV, index, n, f)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos2dARB, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos2dARB, x, y)
|
||||
@@ -2996,6 +2956,221 @@ MOBILEGL_GL_API void glGetnUniformdvARB(GLuint program, GLint location, GLsizei
|
||||
glGetnUniformdv(program, location, bufSize, params);
|
||||
}
|
||||
|
||||
// ---- GL_ARB_shader_objects --------------------------------------------------------------------
|
||||
// A handle is a name of the core's one shared shader/program name space: CreateShaderObjectARB and
|
||||
// CreateProgramObjectARB hand out exactly what glCreateShader / glCreateProgram would, so an ARB
|
||||
// handle works with every core call and a core name with every ARB call. The calls whose meaning
|
||||
// is the core one under another name forward to the core entry point; the ones that ask which
|
||||
// kind of object a handle names (DeleteObjectARB, GetHandleARB, GetObjectParameter*ARB,
|
||||
// GetInfoLogARB) are GLImpl's (Program/GL_ShaderObjectsARB.cpp). GLhandleARB is GLuint everywhere
|
||||
// but Apple, where the header makes it a pointer - hence the two conversions.
|
||||
namespace {
|
||||
inline GLuint ArbName(GLhandleARB handle) {
|
||||
#if defined(__APPLE__)
|
||||
return static_cast<GLuint>(reinterpret_cast<uintptr_t>(handle));
|
||||
#else
|
||||
return handle;
|
||||
#endif
|
||||
}
|
||||
inline GLhandleARB ArbHandle(GLuint name) {
|
||||
#if defined(__APPLE__)
|
||||
return reinterpret_cast<GLhandleARB>(static_cast<uintptr_t>(name));
|
||||
#else
|
||||
return name;
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateShaderObjectARB(GLenum shaderType) { return ArbHandle(glCreateShader(shaderType)); }
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glCreateProgramObjectARB(void) { return ArbHandle(glCreateProgram()); }
|
||||
|
||||
MOBILEGL_GL_API void glDeleteObjectARB(GLhandleARB obj) {
|
||||
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
|
||||
MobileGL::MG_Impl::GLImpl::DeleteObjectARB(ArbName(obj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLhandleARB glGetHandleARB(GLenum pname) {
|
||||
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
|
||||
return ArbHandle(MobileGL::MG_Impl::GLImpl::GetHandleARB(pname));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glDetachObjectARB(GLhandleARB containerObj, GLhandleARB attachedObj) {
|
||||
glDetachShader(ArbName(containerObj), ArbName(attachedObj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glAttachObjectARB(GLhandleARB containerObj, GLhandleARB obj) {
|
||||
glAttachShader(ArbName(containerObj), ArbName(obj));
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glShaderSourceARB(GLhandleARB shaderObj, GLsizei count, const GLcharARB** string,
|
||||
const GLint* length) {
|
||||
glShaderSource(ArbName(shaderObj), count, string, length);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glCompileShaderARB(GLhandleARB shaderObj) { glCompileShader(ArbName(shaderObj)); }
|
||||
|
||||
MOBILEGL_GL_API void glLinkProgramARB(GLhandleARB programObj) { glLinkProgram(ArbName(programObj)); }
|
||||
|
||||
MOBILEGL_GL_API void glUseProgramObjectARB(GLhandleARB programObj) { glUseProgram(ArbName(programObj)); }
|
||||
|
||||
MOBILEGL_GL_API void glValidateProgramARB(GLhandleARB programObj) { glValidateProgram(ArbName(programObj)); }
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterfvARB(GLhandleARB obj, GLenum pname, GLfloat* params) {
|
||||
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
|
||||
MobileGL::MG_Impl::GLImpl::GetObjectParameterfvARB(ArbName(obj), pname, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetObjectParameterivARB(GLhandleARB obj, GLenum pname, GLint* params) {
|
||||
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
|
||||
MobileGL::MG_Impl::GLImpl::GetObjectParameterivARB(ArbName(obj), pname, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetInfoLogARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) {
|
||||
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
|
||||
MobileGL::MG_Impl::GLImpl::GetInfoLogARB(ArbName(obj), maxLength, length, infoLog);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetAttachedObjectsARB(GLhandleARB containerObj, GLsizei maxCount, GLsizei* count,
|
||||
GLhandleARB* obj) {
|
||||
#if defined(__APPLE__)
|
||||
Vector<GLuint> names(maxCount > 0 ? static_cast<SizeT>(maxCount) : 0);
|
||||
GLsizei written = 0;
|
||||
glGetAttachedShaders(ArbName(containerObj), maxCount, &written, names.empty() ? nullptr : names.data());
|
||||
if (obj != nullptr) {
|
||||
for (GLsizei i = 0; i < written; ++i) obj[i] = ArbHandle(names[static_cast<SizeT>(i)]);
|
||||
}
|
||||
if (count != nullptr) *count = written;
|
||||
#else
|
||||
glGetAttachedShaders(ArbName(containerObj), maxCount, count, obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API GLint glGetUniformLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetUniformLocation(ArbName(programObj), name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetActiveUniformARB(GLhandleARB programObj, GLuint index, GLsizei maxLength, GLsizei* length,
|
||||
GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveUniform(ArbName(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformfvARB(GLhandleARB programObj, GLint location, GLfloat* params) {
|
||||
glGetUniformfv(ArbName(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetUniformivARB(GLhandleARB programObj, GLint location, GLint* params) {
|
||||
glGetUniformiv(ArbName(programObj), location, params);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glGetShaderSourceARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* source) {
|
||||
glGetShaderSource(ArbName(obj), maxLength, length, source);
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glUniform1fARB(GLint location, GLfloat v0) { glUniform1f(location, v0); }
|
||||
MOBILEGL_GL_API void glUniform2fARB(GLint location, GLfloat v0, GLfloat v1) { glUniform2f(location, v0, v1); }
|
||||
MOBILEGL_GL_API void glUniform3fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) {
|
||||
glUniform3f(location, v0, v1, v2);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform4fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) {
|
||||
glUniform4f(location, v0, v1, v2, v3);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform1iARB(GLint location, GLint v0) { glUniform1i(location, v0); }
|
||||
MOBILEGL_GL_API void glUniform2iARB(GLint location, GLint v0, GLint v1) { glUniform2i(location, v0, v1); }
|
||||
MOBILEGL_GL_API void glUniform3iARB(GLint location, GLint v0, GLint v1, GLint v2) { glUniform3i(location, v0, v1, v2); }
|
||||
MOBILEGL_GL_API void glUniform4iARB(GLint location, GLint v0, GLint v1, GLint v2, GLint v3) {
|
||||
glUniform4i(location, v0, v1, v2, v3);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform1fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform1fv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform2fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform2fv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform3fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform3fv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform4fvARB(GLint location, GLsizei count, const GLfloat* value) {
|
||||
glUniform4fv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform1ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform1iv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform2ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform2iv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform3ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform3iv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniform4ivARB(GLint location, GLsizei count, const GLint* value) {
|
||||
glUniform4iv(location, count, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniformMatrix2fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix2fv(location, count, transpose, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniformMatrix3fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix3fv(location, count, transpose, value);
|
||||
}
|
||||
MOBILEGL_GL_API void glUniformMatrix4fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||
glUniformMatrix4fv(location, count, transpose, value);
|
||||
}
|
||||
|
||||
// ---- GL_ARB_vertex_shader -----------------------------------------------------------------------
|
||||
// Advertised beside ARB_shader_objects (see BackendObject_*'s extension lists); every entry point is
|
||||
// the core one under the ARB name, and the three that take a program take an ARB handle.
|
||||
MOBILEGL_GL_API void glVertexAttrib1sARB(GLuint index, GLshort x) { glVertexAttrib1s(index, x); }
|
||||
MOBILEGL_GL_API void glVertexAttrib1svARB(GLuint index, const GLshort* v) { glVertexAttrib1sv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib1fARB(GLuint index, GLfloat x) { glVertexAttrib1f(index, x); }
|
||||
MOBILEGL_GL_API void glVertexAttrib1fvARB(GLuint index, const GLfloat* v) { glVertexAttrib1fv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib1dARB(GLuint index, GLdouble x) { glVertexAttrib1d(index, x); }
|
||||
MOBILEGL_GL_API void glVertexAttrib1dvARB(GLuint index, const GLdouble* v) { glVertexAttrib1dv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2sARB(GLuint index, GLshort x, GLshort y) { glVertexAttrib2s(index, x, y); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2svARB(GLuint index, const GLshort* v) { glVertexAttrib2sv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2fARB(GLuint index, GLfloat x, GLfloat y) { glVertexAttrib2f(index, x, y); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2fvARB(GLuint index, const GLfloat* v) { glVertexAttrib2fv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2dARB(GLuint index, GLdouble x, GLdouble y) { glVertexAttrib2d(index, x, y); }
|
||||
MOBILEGL_GL_API void glVertexAttrib2dvARB(GLuint index, const GLdouble* v) { glVertexAttrib2dv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3sARB(GLuint index, GLshort x, GLshort y, GLshort z) { glVertexAttrib3s(index, x, y, z); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3svARB(GLuint index, const GLshort* v) { glVertexAttrib3sv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3fARB(GLuint index, GLfloat x, GLfloat y, GLfloat z) { glVertexAttrib3f(index, x, y, z); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3fvARB(GLuint index, const GLfloat* v) { glVertexAttrib3fv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3dARB(GLuint index, GLdouble x, GLdouble y, GLdouble z) { glVertexAttrib3d(index, x, y, z); }
|
||||
MOBILEGL_GL_API void glVertexAttrib3dvARB(GLuint index, const GLdouble* v) { glVertexAttrib3dv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4sARB(GLuint index, GLshort x, GLshort y, GLshort z, GLshort w) { glVertexAttrib4s(index, x, y, z, w); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4svARB(GLuint index, const GLshort* v) { glVertexAttrib4sv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4fARB(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w) { glVertexAttrib4f(index, x, y, z, w); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4fvARB(GLuint index, const GLfloat* v) { glVertexAttrib4fv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4dARB(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w) { glVertexAttrib4d(index, x, y, z, w); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4dvARB(GLuint index, const GLdouble* v) { glVertexAttrib4dv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NubARB(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w) { glVertexAttrib4Nub(index, x, y, z, w); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4bvARB(GLuint index, const GLbyte* v) { glVertexAttrib4bv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4ivARB(GLuint index, const GLint* v) { glVertexAttrib4iv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4ubvARB(GLuint index, const GLubyte* v) { glVertexAttrib4ubv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4usvARB(GLuint index, const GLushort* v) { glVertexAttrib4usv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4uivARB(GLuint index, const GLuint* v) { glVertexAttrib4uiv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NbvARB(GLuint index, const GLbyte* v) { glVertexAttrib4Nbv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NsvARB(GLuint index, const GLshort* v) { glVertexAttrib4Nsv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NivARB(GLuint index, const GLint* v) { glVertexAttrib4Niv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NubvARB(GLuint index, const GLubyte* v) { glVertexAttrib4Nubv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NusvARB(GLuint index, const GLushort* v) { glVertexAttrib4Nusv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttrib4NuivARB(GLuint index, const GLuint* v) { glVertexAttrib4Nuiv(index, v); }
|
||||
MOBILEGL_GL_API void glVertexAttribPointerARB(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer) { glVertexAttribPointer(index, size, type, normalized, stride, pointer); }
|
||||
MOBILEGL_GL_API void glEnableVertexAttribArrayARB(GLuint index) { glEnableVertexAttribArray(index); }
|
||||
MOBILEGL_GL_API void glDisableVertexAttribArrayARB(GLuint index) { glDisableVertexAttribArray(index); }
|
||||
MOBILEGL_GL_API void glGetVertexAttribdvARB(GLuint index, GLenum pname, GLdouble* params) { glGetVertexAttribdv(index, pname, params); }
|
||||
MOBILEGL_GL_API void glGetVertexAttribfvARB(GLuint index, GLenum pname, GLfloat* params) { glGetVertexAttribfv(index, pname, params); }
|
||||
MOBILEGL_GL_API void glGetVertexAttribivARB(GLuint index, GLenum pname, GLint* params) { glGetVertexAttribiv(index, pname, params); }
|
||||
MOBILEGL_GL_API void glGetVertexAttribPointervARB(GLuint index, GLenum pname, void** pointer) { glGetVertexAttribPointerv(index, pname, pointer); }
|
||||
MOBILEGL_GL_API void glBindAttribLocationARB(GLhandleARB programObj, GLuint index, const GLcharARB* name) {
|
||||
glBindAttribLocation(ArbName(programObj), index, name);
|
||||
}
|
||||
MOBILEGL_GL_API void glGetActiveAttribARB(GLhandleARB programObj, GLuint index, GLsizei maxLength, GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
|
||||
glGetActiveAttrib(ArbName(programObj), index, maxLength, length, size, type, name);
|
||||
}
|
||||
MOBILEGL_GL_API GLint glGetAttribLocationARB(GLhandleARB programObj, const GLcharARB* name) {
|
||||
return glGetAttribLocation(ArbName(programObj), name);
|
||||
}
|
||||
|
||||
// KHR_robustness and EXT_robustness name the same entry points with their own suffixes (an ES
|
||||
// application, ANGLE's GL backend among them, looks them up by those names).
|
||||
MOBILEGL_GL_API GLenum glGetGraphicsResetStatusKHR(void) {
|
||||
|
||||
@@ -19,14 +19,12 @@
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c). This file is the ONLY place every DSA framebuffer entry point lives, and the
|
||||
// emitter it reaches is this package's own header rather than a declaration in one of the
|
||||
// contract's: MG_Pipe/PipeMutation.h is the door MG_State has into the client and carries no
|
||||
// framebuffer row, and MG_Impl/GLImpl and MG_Impl/Pipe are the same layer (this file already
|
||||
// includes MG_Impl/Pipe/PipeFill.h for MGP_FILL).
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#endif
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -624,7 +622,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
|
||||
//
|
||||
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
|
||||
@@ -652,7 +649,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!fbo) return;
|
||||
MG_Pipe::MGPipeFramebufferEmitterInstance().EmitFramebufferByName(*fbo);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
@@ -671,10 +667,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(readFramebuffer);
|
||||
PipePublishFramebufferByName(drawFramebuffer);
|
||||
#endif
|
||||
MGP_FILL(BlitNamedFramebuffer);
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
dstY1, mask, filter);
|
||||
@@ -687,9 +681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfv);
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -701,9 +693,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfi);
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
@@ -715,9 +705,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferiv);
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -729,9 +717,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferuiv);
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
@@ -1460,9 +1446,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1489,9 +1473,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
||||
@@ -1515,9 +1497,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1545,9 +1525,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
||||
@@ -1602,9 +1580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1707,9 +1683,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
||||
@@ -1779,9 +1753,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (renderbuffer == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1791,9 +1763,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!renderbufferObject) return;
|
||||
|
||||
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
||||
@@ -1993,9 +1963,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
||||
if (!framebufferObject) return;
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
||||
@@ -2010,9 +1978,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
||||
@@ -2022,9 +1988,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!framebufferObject) return;
|
||||
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
||||
"NamedFramebufferReadBuffer_State");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
||||
|
||||
@@ -833,7 +833,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
const auto& s = programObject->GetAttachedShaders();
|
||||
// GL-visible: a shader detached since the last link is no longer attached (GL 4.6 §7.3).
|
||||
const auto s = programObject->GetGLVisibleAttachedShaders();
|
||||
GLsizei c = std::min((GLsizei)s.size(), maxCount);
|
||||
if (count) *count = c;
|
||||
for (GLsizei i = 0; i < c; ++i) {
|
||||
@@ -873,7 +874,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
case GL_ATTACHED_SHADERS: {
|
||||
const auto& attachedShaders = programObject->GetAttachedShaders();
|
||||
const auto attachedShaders = programObject->GetGLVisibleAttachedShaders();
|
||||
*params = (GLint)attachedShaders.size();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
@@ -1571,7 +1572,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// version.
|
||||
if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return;
|
||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||
programObject.MarkUBOContentDirty();
|
||||
programObject.MarkUBOContentDirtyRange(static_cast<Uint>(offset + byteOffsetInsideUniform),
|
||||
static_cast<Uint>(writeSize));
|
||||
} else {
|
||||
const auto& ttype = programObject.GetUniformTypeFacts(location);
|
||||
if (!ttype.isTexture && !ttype.isImage) return;
|
||||
|
||||
@@ -197,4 +197,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||
GLenum* type, GLchar* name);
|
||||
|
||||
// GL_ARB_shader_objects (GL_ShaderObjectsARB.cpp): the entry points that ask which kind of object
|
||||
// a handle names. The rest of the extension is the core calls under ARB names (Definitions.cpp).
|
||||
void DeleteObjectARB(GLuint obj);
|
||||
GLuint GetHandleARB(GLenum pname);
|
||||
void GetObjectParameterivARB(GLuint obj, GLenum pname, GLint* params);
|
||||
void GetObjectParameterfvARB(GLuint obj, GLenum pname, GLfloat* params);
|
||||
void GetInfoLogARB(GLuint obj, GLsizei maxLength, GLsizei* length, GLchar* infoLog);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ShaderObjectsARB.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
|
||||
|
||||
// GL_ARB_shader_objects: the entry points whose meaning is not just "the core call under another
|
||||
// name". A handle IS a shader or program name of the core's one shared shader/program name space
|
||||
// (GL 4.6 §7.1 / §7.3): glCreateShaderObjectARB and glCreateProgramObjectARB hand out exactly what
|
||||
// glCreateShader / glCreateProgram would, so every core call takes an ARB handle and every ARB call
|
||||
// takes a core name. What this file adds is the part of the extension that asks "which kind of
|
||||
// object is this handle" - deleting, querying and reading the info log of either kind through one
|
||||
// entry point - and the one query of the current program by handle.
|
||||
|
||||
#include "GL_Program.h"
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
namespace {
|
||||
// The ARB tokens that have no core spelling. The rest of GetObjectParameter's pnames ARE the
|
||||
// core values (OBJECT_DELETE_STATUS_ARB == GL_DELETE_STATUS ... 0x8B80-0x8B88, and
|
||||
// ARB_vertex_shader's two attribute counts 0x8B89 / 0x8B8A).
|
||||
constexpr GLenum kProgramObjectArb = 0x8B40;
|
||||
constexpr GLenum kShaderObjectArb = 0x8B48;
|
||||
constexpr GLenum kObjectTypeArb = 0x8B4E;
|
||||
constexpr GLenum kObjectSubtypeArb = 0x8B4F;
|
||||
|
||||
enum class ObjectKind { None, Program, Shader };
|
||||
|
||||
ObjectKind KindOf(GLuint obj) {
|
||||
if (obj == 0) return ObjectKind::None;
|
||||
if (IsProgram(obj)) return ObjectKind::Program;
|
||||
if (IsShader(obj)) return ObjectKind::Shader;
|
||||
return ObjectKind::None;
|
||||
}
|
||||
|
||||
void Fail(ErrorCode code, const char* func, const std::string& what) {
|
||||
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", func, what));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// DeleteObjectARB deletes whichever kind the handle names, with the core's deferred-deletion
|
||||
// rules (a program in use, a shader still attached). Zero is ignored, as for glDeleteShader /
|
||||
// glDeleteProgram; any other name that is not a shader or program object is INVALID_VALUE.
|
||||
void DeleteObjectARB(GLuint obj) {
|
||||
if (obj == 0) return;
|
||||
switch (KindOf(obj)) {
|
||||
case ObjectKind::Program: DeleteProgram(obj); return;
|
||||
case ObjectKind::Shader: DeleteShader(obj); return;
|
||||
case ObjectKind::None: break;
|
||||
}
|
||||
Fail(ErrorCode::InvalidValue, __func__, std::to_string(obj) + " is not a shader or program object.");
|
||||
}
|
||||
|
||||
// GetHandleARB(PROGRAM_OBJECT_ARB) is the program object in use - GL_CURRENT_PROGRAM, 0 when
|
||||
// none. It is the only pname the extension defines.
|
||||
GLuint GetHandleARB(GLenum pname) {
|
||||
if (pname != kProgramObjectArb) {
|
||||
Fail(ErrorCode::InvalidEnum, __func__, "pname must be GL_PROGRAM_OBJECT_ARB.");
|
||||
return 0;
|
||||
}
|
||||
GLint current = 0;
|
||||
GetIntegerv(GL_CURRENT_PROGRAM, ¤t);
|
||||
return static_cast<GLuint>(current);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The ARB spec's table of pnames, per kind. An unknown pname is INVALID_ENUM; a known one the
|
||||
// object's kind does not have (COMPILE_STATUS of a program, LINK_STATUS of a shader, SUBTYPE
|
||||
// of a program) is INVALID_OPERATION; a handle that names no shader or program is
|
||||
// INVALID_VALUE. True when `*out` holds the answer (every pname here is a single value).
|
||||
bool QueryObjectParameter(GLuint obj, GLenum pname, GLint* out, const char* func) {
|
||||
bool programParam = false, shaderParam = false;
|
||||
switch (pname) {
|
||||
case kObjectTypeArb:
|
||||
case GL_DELETE_STATUS:
|
||||
case GL_INFO_LOG_LENGTH: programParam = shaderParam = true; break;
|
||||
case kObjectSubtypeArb:
|
||||
case GL_COMPILE_STATUS:
|
||||
case GL_SHADER_SOURCE_LENGTH: shaderParam = true; break;
|
||||
case GL_LINK_STATUS:
|
||||
case GL_VALIDATE_STATUS:
|
||||
case GL_ATTACHED_SHADERS:
|
||||
case GL_ACTIVE_UNIFORMS:
|
||||
case GL_ACTIVE_UNIFORM_MAX_LENGTH:
|
||||
case GL_ACTIVE_ATTRIBUTES:
|
||||
case GL_ACTIVE_ATTRIBUTE_MAX_LENGTH: programParam = true; break;
|
||||
default: Fail(ErrorCode::InvalidEnum, func, "pname is not a GetObjectParameterARB parameter."); return false;
|
||||
}
|
||||
const ObjectKind kind = KindOf(obj);
|
||||
if (kind == ObjectKind::None) {
|
||||
Fail(ErrorCode::InvalidValue, func, std::to_string(obj) + " is not a shader or program object.");
|
||||
return false;
|
||||
}
|
||||
if ((kind == ObjectKind::Program && !programParam) || (kind == ObjectKind::Shader && !shaderParam)) {
|
||||
Fail(ErrorCode::InvalidOperation, func, "pname is not a parameter of this kind of object.");
|
||||
return false;
|
||||
}
|
||||
if (pname == kObjectTypeArb) {
|
||||
*out = static_cast<GLint>(kind == ObjectKind::Program ? kProgramObjectArb : kShaderObjectArb);
|
||||
} else if (pname == kObjectSubtypeArb) {
|
||||
GetShaderiv(obj, GL_SHADER_TYPE, out);
|
||||
} else if (kind == ObjectKind::Program) {
|
||||
GetProgramiv(obj, pname, out);
|
||||
} else {
|
||||
GetShaderiv(obj, pname, out);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GetObjectParameterivARB(GLuint obj, GLenum pname, GLint* params) {
|
||||
GLint value = 0;
|
||||
if (QueryObjectParameter(obj, pname, &value, __func__) && params != nullptr) *params = value;
|
||||
}
|
||||
|
||||
void GetObjectParameterfvARB(GLuint obj, GLenum pname, GLfloat* params) {
|
||||
GLint value = 0;
|
||||
if (QueryObjectParameter(obj, pname, &value, __func__) && params != nullptr) *params = static_cast<GLfloat>(value);
|
||||
}
|
||||
|
||||
void GetInfoLogARB(GLuint obj, GLsizei maxLength, GLsizei* length, GLchar* infoLog) {
|
||||
switch (KindOf(obj)) {
|
||||
case ObjectKind::Program: GetProgramInfoLog(obj, maxLength, length, infoLog); return;
|
||||
case ObjectKind::Shader: GetShaderInfoLog(obj, maxLength, length, infoLog); return;
|
||||
case ObjectKind::None: break;
|
||||
}
|
||||
Fail(ErrorCode::InvalidValue, __func__, std::to_string(obj) + " is not a shader or program object.");
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
@@ -49,11 +49,7 @@
|
||||
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
|
||||
// read as it was: its ReadPixels maps the driver PBO back into the shadow inside the call, and the
|
||||
// pull build compiles this to nothing (G1).
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) (buffer)->SyncGpuWrites(); } while (0)
|
||||
#else
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) ((void)0)
|
||||
#endif
|
||||
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { (buffer)->SyncGpuWrites(); } while (0)
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -1376,7 +1372,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
@@ -1399,7 +1394,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||
@@ -1478,7 +1472,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
@@ -1501,7 +1494,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
@@ -2197,7 +2189,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
@@ -2220,7 +2211,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
||||
@@ -6127,10 +6117,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
viewObject->SetInternalFormat(viewInternalFormat);
|
||||
viewObject->SetSamples(storageOwner->GetSamples());
|
||||
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
|
||||
#endif
|
||||
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
|
||||
}
|
||||
|
||||
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
|
||||
@@ -6592,23 +6579,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
@@ -6620,12 +6594,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
auto uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Unlike glGetTexImage's face target, this DSA call reads all six faces.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
textureObject->GetTarget() == TextureTarget::TextureCubeMap)
|
||||
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap)
|
||||
uploadTarget = TextureUploadTarget::Unknown;
|
||||
#endif
|
||||
GetTextureImageForUploadTarget(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels, __func__);
|
||||
}
|
||||
@@ -6815,7 +6786,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
|
||||
.Bind(textureObject, level, layered, layer, access, format);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// AND THE IMAGE-UNIT MARK BESIDE IT. The line above moves the TEXTURE-unit high-water
|
||||
// mark, which is a different array: a reader that needs "the highest image unit ever
|
||||
// bound" cannot take it from there without either over-walking (a texture bind at unit
|
||||
@@ -6824,7 +6794,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// (MG_Remote/Client/GpuWritePending.cpp). Push builds only, so the pull build's bytes do
|
||||
// not move (G1).
|
||||
MG_State::pGLContext->NoteImageUnitTouched(static_cast<Int>(unit));
|
||||
#endif
|
||||
MGP_FILL(BindImageTexture);
|
||||
bindImageTexture(unit, texture, level, layered, layer, access, format);
|
||||
}
|
||||
@@ -6888,22 +6857,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
if (!GetTexImage_State(target, level, format, type, pixels)) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
const auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
|
||||
CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format, type, -1, pixels,
|
||||
__func__);
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
void GetInternalformativ(GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) {
|
||||
|
||||
@@ -824,6 +824,93 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glMultiDrawArraysIndirectCount, name);
|
||||
GETPROC(glMultiDrawElementsIndirectCount, name);
|
||||
GETPROC(glPolygonOffsetClamp, name);
|
||||
// GL_ARB_shader_objects (Definitions.cpp): advertised, so every entry point resolves.
|
||||
GETPROC(glDeleteObjectARB, name);
|
||||
GETPROC(glGetHandleARB, name);
|
||||
GETPROC(glDetachObjectARB, name);
|
||||
GETPROC(glCreateShaderObjectARB, name);
|
||||
GETPROC(glShaderSourceARB, name);
|
||||
GETPROC(glCompileShaderARB, name);
|
||||
GETPROC(glCreateProgramObjectARB, name);
|
||||
GETPROC(glAttachObjectARB, name);
|
||||
GETPROC(glLinkProgramARB, name);
|
||||
GETPROC(glUseProgramObjectARB, name);
|
||||
GETPROC(glValidateProgramARB, name);
|
||||
GETPROC(glUniform1fARB, name);
|
||||
GETPROC(glUniform2fARB, name);
|
||||
GETPROC(glUniform3fARB, name);
|
||||
GETPROC(glUniform4fARB, name);
|
||||
GETPROC(glUniform1iARB, name);
|
||||
GETPROC(glUniform2iARB, name);
|
||||
GETPROC(glUniform3iARB, name);
|
||||
GETPROC(glUniform4iARB, name);
|
||||
GETPROC(glUniform1fvARB, name);
|
||||
GETPROC(glUniform2fvARB, name);
|
||||
GETPROC(glUniform3fvARB, name);
|
||||
GETPROC(glUniform4fvARB, name);
|
||||
GETPROC(glUniform1ivARB, name);
|
||||
GETPROC(glUniform2ivARB, name);
|
||||
GETPROC(glUniform3ivARB, name);
|
||||
GETPROC(glUniform4ivARB, name);
|
||||
GETPROC(glUniformMatrix2fvARB, name);
|
||||
GETPROC(glUniformMatrix3fvARB, name);
|
||||
GETPROC(glUniformMatrix4fvARB, name);
|
||||
GETPROC(glGetObjectParameterfvARB, name);
|
||||
GETPROC(glGetObjectParameterivARB, name);
|
||||
GETPROC(glGetInfoLogARB, name);
|
||||
GETPROC(glGetAttachedObjectsARB, name);
|
||||
GETPROC(glGetUniformLocationARB, name);
|
||||
GETPROC(glGetActiveUniformARB, name);
|
||||
GETPROC(glGetUniformfvARB, name);
|
||||
GETPROC(glGetUniformivARB, name);
|
||||
GETPROC(glGetShaderSourceARB, name);
|
||||
// GL_ARB_vertex_shader (Definitions.cpp): advertised, so every entry point resolves.
|
||||
GETPROC(glVertexAttrib1sARB, name);
|
||||
GETPROC(glVertexAttrib1svARB, name);
|
||||
GETPROC(glVertexAttrib1fARB, name);
|
||||
GETPROC(glVertexAttrib1fvARB, name);
|
||||
GETPROC(glVertexAttrib1dARB, name);
|
||||
GETPROC(glVertexAttrib1dvARB, name);
|
||||
GETPROC(glVertexAttrib2sARB, name);
|
||||
GETPROC(glVertexAttrib2svARB, name);
|
||||
GETPROC(glVertexAttrib2fARB, name);
|
||||
GETPROC(glVertexAttrib2fvARB, name);
|
||||
GETPROC(glVertexAttrib2dARB, name);
|
||||
GETPROC(glVertexAttrib2dvARB, name);
|
||||
GETPROC(glVertexAttrib3sARB, name);
|
||||
GETPROC(glVertexAttrib3svARB, name);
|
||||
GETPROC(glVertexAttrib3fARB, name);
|
||||
GETPROC(glVertexAttrib3fvARB, name);
|
||||
GETPROC(glVertexAttrib3dARB, name);
|
||||
GETPROC(glVertexAttrib3dvARB, name);
|
||||
GETPROC(glVertexAttrib4sARB, name);
|
||||
GETPROC(glVertexAttrib4svARB, name);
|
||||
GETPROC(glVertexAttrib4fARB, name);
|
||||
GETPROC(glVertexAttrib4fvARB, name);
|
||||
GETPROC(glVertexAttrib4dARB, name);
|
||||
GETPROC(glVertexAttrib4dvARB, name);
|
||||
GETPROC(glVertexAttrib4NubARB, name);
|
||||
GETPROC(glVertexAttrib4bvARB, name);
|
||||
GETPROC(glVertexAttrib4ivARB, name);
|
||||
GETPROC(glVertexAttrib4ubvARB, name);
|
||||
GETPROC(glVertexAttrib4usvARB, name);
|
||||
GETPROC(glVertexAttrib4uivARB, name);
|
||||
GETPROC(glVertexAttrib4NbvARB, name);
|
||||
GETPROC(glVertexAttrib4NsvARB, name);
|
||||
GETPROC(glVertexAttrib4NivARB, name);
|
||||
GETPROC(glVertexAttrib4NubvARB, name);
|
||||
GETPROC(glVertexAttrib4NusvARB, name);
|
||||
GETPROC(glVertexAttrib4NuivARB, name);
|
||||
GETPROC(glVertexAttribPointerARB, name);
|
||||
GETPROC(glEnableVertexAttribArrayARB, name);
|
||||
GETPROC(glDisableVertexAttribArrayARB, name);
|
||||
GETPROC(glGetVertexAttribdvARB, name);
|
||||
GETPROC(glGetVertexAttribfvARB, name);
|
||||
GETPROC(glGetVertexAttribivARB, name);
|
||||
GETPROC(glGetVertexAttribPointervARB, name);
|
||||
GETPROC(glBindAttribLocationARB, name);
|
||||
GETPROC(glGetActiveAttribARB, name);
|
||||
GETPROC(glGetAttribLocationARB, name);
|
||||
GETPROC(glPrimitiveBoundingBoxARB, name);
|
||||
GETPROC(glGetTextureHandleARB, name);
|
||||
GETPROC(glGetTextureSamplerHandleARB, name);
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
// THE CONTEXT ID IS GLContext::GetTextureContextId() AND NOTHING ELSE - the tree's existing
|
||||
// never-reused per-context id (TextureState::AllocateContextId; PipeInputs carries it as
|
||||
// m_textureContextId at seven fill points and the backends' own per-context memos key on it).
|
||||
// Deliberately NOT the GLContext ADDRESS that MGB_CTX_IDENTITY and MGPipeTracker::m_context
|
||||
// Deliberately NOT the GLContext ADDRESS that gPipeInputs and MGPipeTracker::m_context
|
||||
// compare, because Core.h states the reason that id exists at all: a context freed and remade
|
||||
// lands on the old heap address, which would put this same defect back one context recreation
|
||||
// later.
|
||||
@@ -81,7 +81,6 @@
|
||||
// one and this one depends on nothing of the family's. The reverse arrangement would make the
|
||||
// resolver reachable only from a translation unit that had already decided to use it, i.e.
|
||||
// dead in the build that matters and live only in the tests.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
@@ -267,4 +266,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *resolver;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
//
|
||||
// Header-only for the same ownership reason as Tracker.h: the root CMakeLists.txt that
|
||||
// would name a new .cpp is package A's and is frozen behind the p2/contract tag.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <Config.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
@@ -203,4 +202,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *cache;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state: the root
|
||||
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
|
||||
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
@@ -276,13 +275,13 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool drawOk = false;
|
||||
Bool readOk = false;
|
||||
if (shared) {
|
||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||
} else {
|
||||
if (drawFbo) {
|
||||
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
||||
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
||||
}
|
||||
if (readFbo) {
|
||||
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||
readOk = BuildBoundFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||
}
|
||||
}
|
||||
if (!drawOk && !readOk) return 0;
|
||||
@@ -415,6 +414,8 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||
Uint64 EmissionCount() const { return m_emissions; }
|
||||
Uint64 RefusedCount() const { return m_refusals; }
|
||||
// P15: bound-target builds answered from the memo (BuildBoundFramebufferState).
|
||||
Uint64 MemoHitCount() const { return m_memoHits; }
|
||||
|
||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
||||
@@ -439,6 +440,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void ResetForTest() {
|
||||
Reset();
|
||||
ResetCounters();
|
||||
for (auto& memo : m_buildMemo) memo.Valid = false;
|
||||
m_lastDraw = MGPFramebufferState{};
|
||||
m_lastRead = MGPFramebufferState{};
|
||||
m_lastNamed = MGPFramebufferState{};
|
||||
@@ -564,6 +566,40 @@ namespace MobileGL::MG_Pipe {
|
||||
return true;
|
||||
}
|
||||
|
||||
// BuildFramebufferState for a BOUND target, reusing the last record built for the same
|
||||
// framebuffer and target while nothing the build reads has moved. The build runs per draw
|
||||
// and its answer is a function of:
|
||||
// - the framebuffer: lifetime id (identity) and object version (attachments, draw and
|
||||
// read buffers, no-attachment defaults all move it);
|
||||
// - each attached texture: lifetime id, shape version (sizes, level set, internal
|
||||
// format: completeness and geometry) and its samples / fixed-locations pair, which
|
||||
// move the params version rather than the shape;
|
||||
// - each attached renderbuffer: lifetime id plus its size, format and samples, read
|
||||
// directly (its storage has no version);
|
||||
// - the handles it minted: the slot allocator's reset epoch, and the texture emitter's
|
||||
// table epoch for the sticky bind bits SurfaceOf notes (a dropped table must be
|
||||
// re-noted).
|
||||
// Not for the default framebuffer, whose geometry follows the current surface.
|
||||
// Kill switch MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO=0.
|
||||
Bool BuildBoundFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||
MGPFramebufferState& out) {
|
||||
const Bool memoOn = MG_Config::Features.PipeFramebufferStateMemo != MG_Config::QuirkOverride::ForceOff &&
|
||||
!fbo.IsDefaultFramebuffer();
|
||||
// Direct-mapped on (framebuffer, target): the build only runs when the bound framebuffer
|
||||
// changed, so a per-target single entry would miss on every A/B/A switch between targets.
|
||||
const Uint64 key = fbo.GetLifetimeId() * 4u + static_cast<Uint64>(target);
|
||||
BuildMemo& memo = m_buildMemo[static_cast<SizeT>((key * 0x9E3779B97F4A7C15ull) >> 60) % m_buildMemo.size()];
|
||||
if (memoOn && memo.Valid && MemoHolds(memo, fbo, target)) {
|
||||
out = memo.State;
|
||||
++m_memoHits;
|
||||
return true;
|
||||
}
|
||||
memo.Valid = false;
|
||||
if (!BuildFramebufferState(fbo, target, out)) return false;
|
||||
if (memoOn) FillMemo(memo, fbo, target, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
|
||||
if (MG_State::pGLContext == nullptr) return false;
|
||||
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
||||
@@ -643,6 +679,96 @@ namespace MobileGL::MG_Pipe {
|
||||
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
||||
}
|
||||
|
||||
struct MemoAttachment {
|
||||
Uint32 Point = 0;
|
||||
Bool IsTexture = false;
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 ShapeVersion = 0;
|
||||
Int Width = 0, Height = 0, Samples = 0;
|
||||
Uint32 Format = 0;
|
||||
Bool FixedSampleLocations = false;
|
||||
};
|
||||
struct BuildMemo {
|
||||
Bool Valid = false;
|
||||
Uint8 Target = 0;
|
||||
Uint16 ObjectVersion = 0;
|
||||
Uint64 FboLifetimeId = 0;
|
||||
Uint64 AllocatorEpoch = 0;
|
||||
Uint64 TextureTableEpoch = 0;
|
||||
Uint32 AttachmentCount = 0;
|
||||
Array<MemoAttachment, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>
|
||||
Attachments{};
|
||||
MGPFramebufferState State{};
|
||||
};
|
||||
|
||||
static Bool AttachmentMatches(const MemoAttachment& entry,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsEmpty() || attachment.IsTexture() != entry.IsTexture) return false;
|
||||
if (entry.IsTexture) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
return texture->GetLifetimeId() == entry.LifetimeId &&
|
||||
texture->GetShapeVersion() == entry.ShapeVersion &&
|
||||
texture->GetSamples() == entry.Samples &&
|
||||
texture->HasFixedSampleLocations() == entry.FixedSampleLocations;
|
||||
}
|
||||
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||
return renderbuffer->GetLifetimeId() == entry.LifetimeId && renderbuffer->GetWidth() == entry.Width &&
|
||||
renderbuffer->GetHeight() == entry.Height && renderbuffer->GetSamples() == entry.Samples &&
|
||||
static_cast<Uint32>(renderbuffer->GetInternalFormat()) == entry.Format;
|
||||
}
|
||||
|
||||
Bool MemoHolds(const BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target) const {
|
||||
if (memo.Target != static_cast<Uint8>(target) || memo.FboLifetimeId != fbo.GetLifetimeId() ||
|
||||
memo.ObjectVersion != fbo.GetObjectVersion() || memo.AllocatorEpoch != MGPipeSlots().ResetEpoch() ||
|
||||
memo.TextureTableEpoch != MGPipeTextureEmitterInstance().TableEpoch()) {
|
||||
return false;
|
||||
}
|
||||
const auto& all = fbo.GetAllAttachmentObjects();
|
||||
for (Uint32 i = 0; i < memo.AttachmentCount; ++i) {
|
||||
const MemoAttachment& entry = memo.Attachments[i];
|
||||
if (!AttachmentMatches(entry, all[entry.Point])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FillMemo(BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||
const MGPFramebufferState& state) {
|
||||
memo.Target = static_cast<Uint8>(target);
|
||||
memo.FboLifetimeId = fbo.GetLifetimeId();
|
||||
memo.ObjectVersion = fbo.GetObjectVersion();
|
||||
memo.AllocatorEpoch = MGPipeSlots().ResetEpoch();
|
||||
memo.TextureTableEpoch = MGPipeTextureEmitterInstance().TableEpoch();
|
||||
memo.AttachmentCount = 0;
|
||||
const auto& all = fbo.GetAllAttachmentObjects();
|
||||
for (SizeT point = 0; point < all.size(); ++point) {
|
||||
const auto& attachment = all[point];
|
||||
if (attachment.IsEmpty()) continue;
|
||||
MemoAttachment& entry = memo.Attachments[memo.AttachmentCount++];
|
||||
entry = MemoAttachment{};
|
||||
entry.Point = static_cast<Uint32>(point);
|
||||
entry.IsTexture = attachment.IsTexture();
|
||||
if (entry.IsTexture) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
entry.LifetimeId = texture->GetLifetimeId();
|
||||
entry.ShapeVersion = texture->GetShapeVersion();
|
||||
entry.Samples = texture->GetSamples();
|
||||
entry.FixedSampleLocations = texture->HasFixedSampleLocations();
|
||||
} else {
|
||||
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||
entry.LifetimeId = renderbuffer->GetLifetimeId();
|
||||
entry.Width = renderbuffer->GetWidth();
|
||||
entry.Height = renderbuffer->GetHeight();
|
||||
entry.Samples = renderbuffer->GetSamples();
|
||||
entry.Format = static_cast<Uint32>(renderbuffer->GetInternalFormat());
|
||||
}
|
||||
}
|
||||
memo.State = state;
|
||||
memo.Valid = true;
|
||||
}
|
||||
|
||||
// Direct-mapped on (framebuffer lifetime id, target); Named never reaches it.
|
||||
Array<BuildMemo, 16> m_buildMemo{};
|
||||
Uint64 m_memoHits = 0;
|
||||
Array<Uint64, 2> m_lastEmitted{};
|
||||
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
|
||||
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
|
||||
@@ -663,4 +789,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
@@ -195,4 +194,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
// Client memory becomes ordinary owned buffer resources before a draw.
|
||||
#pragma once
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/ClientFetchPlan.h>
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
@@ -60,6 +59,7 @@ namespace MobileGL::MG_Pipe {
|
||||
binding.Res = info.IndexResource;
|
||||
binding.IndexSize = info.IndexSize;
|
||||
MGPipeRouteSetIndexBuffer(binding);
|
||||
MGPipeVertexInputEmitterInstance().NoteIndexBufferRoutedElsewhere(m_context);
|
||||
m_indexBindingChanged = true;
|
||||
}
|
||||
|
||||
@@ -207,4 +207,3 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool m_indexBindingChanged = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif
|
||||
|
||||
+250
-227
@@ -39,7 +39,12 @@
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_Pipe/VerbRecordOps.h>
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Impl/Pipe/Verb/VerbPort.h>
|
||||
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-8 (c1): the client's liveness gates read the caps mirror, never MGPipeGetResourceOps().
|
||||
@@ -93,7 +98,6 @@ namespace MobileGL::MG_Pipe {
|
||||
return inputs.m_currentVertexAttribute;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (ra, CONTRACT-P5E §2.3). THE FOUR O-CLASS ROWS, AND ONLY THEY: these are the only
|
||||
// members of this block that own a frontend object rather than point into one, so they
|
||||
// are the only ones through which the apply thread can become a last owner. The raw
|
||||
@@ -105,7 +109,6 @@ namespace MobileGL::MG_Pipe {
|
||||
inputs.m_programForDraw.reset();
|
||||
inputs.m_transformFeedbackProgram.reset();
|
||||
}
|
||||
#endif
|
||||
|
||||
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
|
||||
using F = MGPipeInputField;
|
||||
@@ -132,8 +135,11 @@ namespace MobileGL::MG_Pipe {
|
||||
case F::GetBoundTransformFeedbackName:
|
||||
dst.m_boundTransformFeedbackName = ctx.GetBoundTransformFeedbackName();
|
||||
break;
|
||||
// The four object pins: assigned only when the object moved, so the residual fill of
|
||||
// every draw does not pay a reference-count increment and decrement for a pointer it
|
||||
// already holds (P14: one of these moves per Minecraft draw, the other three rarely).
|
||||
case F::GetBoundVertexArray:
|
||||
dst.m_boundVertexArray = ctx.GetBoundVertexArray();
|
||||
if (dst.m_boundVertexArray != ctx.GetBoundVertexArray()) dst.m_boundVertexArray = ctx.GetBoundVertexArray();
|
||||
break;
|
||||
case F::GetBufferBindingSlot:
|
||||
// Every global target has a slot; Index stays null - GLContext resolves it
|
||||
@@ -246,10 +252,11 @@ namespace MobileGL::MG_Pipe {
|
||||
dst.m_primitiveRestartIndex = ctx.GetPrimitiveRestartIndex();
|
||||
break;
|
||||
case F::GetProgramForDispatch:
|
||||
dst.m_programForDispatch = ctx.GetProgramForDispatch();
|
||||
if (dst.m_programForDispatch != ctx.GetProgramForDispatch())
|
||||
dst.m_programForDispatch = ctx.GetProgramForDispatch();
|
||||
break;
|
||||
case F::GetProgramForDraw:
|
||||
dst.m_programForDraw = ctx.GetProgramForDraw();
|
||||
if (dst.m_programForDraw != ctx.GetProgramForDraw()) dst.m_programForDraw = ctx.GetProgramForDraw();
|
||||
break;
|
||||
case F::GetProvokingVertexMode:
|
||||
dst.m_provokingVertexMode = ctx.GetProvokingVertexMode();
|
||||
@@ -290,7 +297,8 @@ namespace MobileGL::MG_Pipe {
|
||||
dst.m_transformFeedbackPausedPrimitiveCounter = ctx.GetTransformFeedbackPausedPrimitiveCounter();
|
||||
break;
|
||||
case F::GetTransformFeedbackProgram:
|
||||
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
|
||||
if (dst.m_transformFeedbackProgram != ctx.GetTransformFeedbackProgram())
|
||||
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
|
||||
break;
|
||||
case F::GetViewport:
|
||||
dst.m_viewport = ctx.GetViewport();
|
||||
@@ -352,6 +360,10 @@ namespace MobileGL::MG_Pipe {
|
||||
namespace {
|
||||
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
|
||||
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P13 W5: no transport, no run-ahead client.
|
||||
Bool ClientRunsAhead() { return false; }
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- P5e (ra): the fill decision (CONTRACT-P5E §3.1) -----------------------------
|
||||
//
|
||||
@@ -365,85 +377,57 @@ namespace MobileGL::MG_Pipe {
|
||||
return session != nullptr && session->RunAheadArmed();
|
||||
}
|
||||
|
||||
// THE WIRE OP A VERB BOUNDARY BECOMES, which is the join MGPipeVerbForWireOp draws in
|
||||
// the other direction. Built by walking the op space once at compile time rather than
|
||||
// written out, because a second hand-written table is a second thing to forget a row
|
||||
// in - and the generator already refuses a verb-shaped op with no row.
|
||||
constexpr MGPWireOp WireOpForVerb(MGPipeVerb verb) {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
|
||||
const auto op = static_cast<MGPWireOp>(i);
|
||||
if (MGPipeVerbForWireOp(op) == verb) return op;
|
||||
}
|
||||
return MGPWireOp::kOpCount;
|
||||
}
|
||||
|
||||
// CONTRACT-P5E §2.1's predicate, ASKED AT THE VALIDATE POINT - which is before the
|
||||
// record exists, so it is asked of the verb and the live context rather than of the
|
||||
// payload. The two halves must agree with MGPipeBarriered(op, payload, applierState),
|
||||
// which is what the server computes, so each clause is the same clause:
|
||||
//
|
||||
// 1. the static WaitClass column - the same generated table, same op;
|
||||
// 1. the static WaitClass column - of the op the verb's emitter publishes,
|
||||
// named by MGP_VERB_RECORD_OP_LIST (MG_Pipe/VerbRecordOps.h);
|
||||
// 2. kCtxVerb inside an open XFB span - ctx.IsTransformFeedbackActive() here,
|
||||
// the applied MGPContextValues mirror there, and set_context_values precedes the
|
||||
// verb on the ring, so the two read the same value (§2.1);
|
||||
// 3. a draw carrying kDrawClientArrays - NOT asked here, because under run-ahead
|
||||
// such a draw never reaches a record at all: vi refuses it on this very thread in
|
||||
// EmitDrawRecord's array arm (§5.1, ruling 2). Asking it here would be a second
|
||||
// spelling of vi's "does any enabled attribute lack a buffer" walk, and two
|
||||
// spellings of an escalation is precisely what ruling 3 replaced.
|
||||
// EmitDrawRecord's array arm (§5.1, ruling 2).
|
||||
//
|
||||
// AN OP WITH NO VERB ROW ANSWERS BARRIERED. A verb the join does not know is one this
|
||||
// file cannot reason about, and the safe answer - fill it, wait for it - is also the
|
||||
// pre-P5e answer.
|
||||
// `ctx` MAY BE NULL, and that is not a convenience: the validate point asks this
|
||||
// question BEFORE it has decided whether there is anything to fill, so that the answer
|
||||
// is about the RECORD and not about the state of the block. A null context has no open
|
||||
// transform-feedback span, so clause 2 is false for it - and the verb is a no-op below
|
||||
// either way.
|
||||
// P15: THERE IS NO FALLBACK. The answer used to come from inverting MGP_VERB_OP_LIST
|
||||
// (op -> verb, many-to-one), and a verb the inverse missed answered BARRIERED - which
|
||||
// the draw family and then the blit family each paid for until a special case named
|
||||
// them (the blit: one applier quiesce per frame on FCL). The verb -> record table has a
|
||||
// row for every verb by static_assert, so async is the default and a verb syncs only
|
||||
// because the record it publishes says so.
|
||||
//
|
||||
// `ctx` MAY BE NULL: the validate point asks this question BEFORE it has decided
|
||||
// whether there is anything to fill. A null context has no open transform-feedback
|
||||
// span, so clause 2 is false for it - and the verb is a no-op below either way.
|
||||
// MGPipeValidateForVerbBarriered's switch: the next ClientVerbIsBarriered answers true.
|
||||
Bool g_forceBarrieredFill = false;
|
||||
|
||||
// P15: A PACK-BUFFER READBACK IS FIRE-AND-FORGET, decided per call. With a pack buffer
|
||||
// bound the readback emitters publish read_pixels_to_buffer / get_texture_image_to_buffer
|
||||
// (kWaitNone) instead of the reply form, and nothing is owed to the client at the call;
|
||||
// the predicate mirrors PackBufferReadbackTarget's conditions. The one condition it
|
||||
// cannot see (the byte range past 2 GiB) sends the emitter to the reply form, which then
|
||||
// re-validates barriered (MGPipeValidateForVerbBarriered) before it publishes.
|
||||
Bool PackReadbackGoesToBuffer(GLContext* ctx) {
|
||||
if (ctx == nullptr) return false;
|
||||
const auto& pbo = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject();
|
||||
if (!pbo) return false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Ipc.PboReadbackSync != 0) return false;
|
||||
#endif
|
||||
if (pbo->IsMapped() || !MG_Record::BufferWritebackIsReachable(*pbo)) return false;
|
||||
return !MGPipeHandleIsNull(MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, pbo->GetLifetimeId()));
|
||||
}
|
||||
|
||||
Bool ClientVerbIsBarriered(MGPipeVerb verb, GLContext* ctx) {
|
||||
MGPWireOp op = WireOpForVerb(verb);
|
||||
// ---- P5e (ra2): THE JOIN IS MANY-TO-ONE ON THE DRAW FAMILY, SO THE INVERSE IS NOT
|
||||
// A FUNCTION, AND THE DEFAULT BELOW WAS ANSWERING FOR NINETEEN VERBS -----------------
|
||||
//
|
||||
// MGP_VERB_OP_LIST carries ONE row for the whole draw family - `DrawVbo ->
|
||||
// DrawArrays` - because that is the direction the SERVER needs: a draw_vbo record
|
||||
// stamps a verb boundary and DrawArrays is the name it prints. Inverting it
|
||||
// verb-first therefore answers kOpCount for DrawElements, DrawElementsBaseVertex,
|
||||
// DrawElementsIndirect, MultiDrawElementsBaseVertex and every other indexed /
|
||||
// instanced / multi / indirect verb - all of which emit exactly that same draw_vbo.
|
||||
//
|
||||
// The conservative default below reads "a verb the join does not know answers
|
||||
// BARRIERED: fill it, wait for it". THE FILL HAPPENS AND THE WAIT DOES NOT. The wait
|
||||
// is not decided here - it is decided per RECORD, by MGPipeBarriered(op, payload,
|
||||
// st) at the publish (§2.1) - and the record is a draw_vbo, whose wait class is
|
||||
// kWaitNone. So every indexed draw filled gPipeInputs while telling
|
||||
// RefusePipeInputsTouchWhileApplierOwnsIt, through isBarrieredFill, that this thread
|
||||
// was about to park behind it, and then ran on without parking. The apply thread was
|
||||
// measured inside a record at that instant, and the abort it produced named the
|
||||
// CLIENT's verb - a verb the applier cannot stamp, which is what identifies the
|
||||
// writer (report §2).
|
||||
//
|
||||
// So the family's one row is applied to the family. Every other unknown verb keeps
|
||||
// the conservative answer, which is now honest rather than hoped for: the fill site
|
||||
// establishes quiescence before it writes (MGPipeValidateForVerb below), so a
|
||||
// barriered fill no longer rests on a park that may never come.
|
||||
static_assert(MGPipeVerbForWireOp(MGPWireOp::DrawVbo) == MGPipeVerb::DrawArrays,
|
||||
"the draw family's representative row moved; the fallback below names "
|
||||
"draw_vbo because that is the record every kDraw verb emits");
|
||||
// P5e (ra2): THE CLIENT'S HALF OF ESCALATION (iii) WAS HERE AND WENT WITH IT
|
||||
// (ID-133, withdrawn by ID-136). It named MultiDrawArrays / MultiDrawElements /
|
||||
// MultiDrawElementsBaseVertex so the client would FILL for the records the server
|
||||
// was escalating - a fill the server then read as a barriered pull. Retiring that
|
||||
// pull outright (MultiDraw.cpp's BoundDrawIndirectBufferId takes a handle arm) left
|
||||
// nothing for the fill to serve, so a plain multi-draw is an ordinary kDraw verb
|
||||
// again and takes the draw_vbo answer below. The pair is the phase's own lesson
|
||||
// written twice: a wait added to make a lane green is paid by an arm, and here the
|
||||
// arm was the default tier the phone ships.
|
||||
if (op == MGPWireOp::kOpCount &&
|
||||
kMGPipeVerbClass[static_cast<SizeT>(verb)] == MGPipeVerbClass::kDraw) {
|
||||
op = MGPWireOp::DrawVbo;
|
||||
}
|
||||
if (op == MGPWireOp::kOpCount) return true;
|
||||
if (g_forceBarrieredFill) return true;
|
||||
MGPWireOp op = MGPipeRecordOpForVerb(verb);
|
||||
if (op == MGPWireOp::ReadPixels && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::ReadPixelsToBuffer;
|
||||
if (op == MGPWireOp::GetTextureImage && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::GetTextureImageToBuffer;
|
||||
// No record (answered from the caps mirror): no apply can read a fill for it.
|
||||
if (op == kMGPipeNoRecord) return false;
|
||||
if (MGPipeWaitClassFor(op) != kWaitNone) return true;
|
||||
if (MGPipeCallClassFor(op) == kCtxVerb && ctx != nullptr &&
|
||||
ctx->IsTransformFeedbackActive()) {
|
||||
@@ -481,6 +465,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// call that returns, and the lockstep client's behaviour is unchanged byte for byte
|
||||
// (G1) - under lockstep appliedSeq is already at LastPublishedSeq by construction.
|
||||
void QuiesceApplierBeforeFill(const char* surface) {
|
||||
// P15: WITH TWO BLOCKS THE FILL IS THE CLIENT'S OWN. The applier reads the server
|
||||
// block, so a GL-thread write to the client block races nothing and needs no wait -
|
||||
// which is what makes one barriered verb cost one wait (its record's) instead of a
|
||||
// quiesce before the fill, another before the residual half and one per frontend
|
||||
// mutation after it (FCL BSL: 22 of them per frame).
|
||||
if (MGPipeBlocksAreDistinct()) return;
|
||||
MG_Remote::Client::ClientSession* session = MG_Remote::Client::ClientSession::Active();
|
||||
if (session == nullptr) return; // no session: this process has no applier to outrun
|
||||
session->WaitForApplyToCatchUp(surface);
|
||||
@@ -681,11 +671,13 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
|
||||
if (&self != &gPipeInputs || !g_verify.Enabled) return;
|
||||
if (&self != &*gPipeInputs || !g_verify.Enabled) return;
|
||||
const auto index = static_cast<SizeT>(field);
|
||||
if (kMGPipeInputFieldSticky[index]) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (self.ServerStampedVerb() && field == MGPipeInputField::GetPixelStoreParameters) {
|
||||
// P13 W5: the monolith verb port applies records without a server stamp, and its readback
|
||||
// forces the neutral pack around the backend's read exactly as the server's does.
|
||||
if ((self.ServerStampedVerb() || MG_Record::MonolithPortApplyingForVerify()) &&
|
||||
field == MGPipeInputField::GetPixelStoreParameters) {
|
||||
// Compare the backend's pack field against the last payload applied to this
|
||||
// context, including the neutral-pack override during readback. Never obtain
|
||||
// the reference by reading the field being checked, or consult the client's
|
||||
@@ -701,7 +693,6 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (g_verify.InHook) return;
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) return;
|
||||
@@ -984,8 +975,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// InitialBytesNotCarried self-check aborted by name on the first
|
||||
// glBufferData(target, 0, NULL, usage) of the bsl-esc-menu trace. `GetSize() > 0` is
|
||||
// what makes the answer mean "there are bytes here" rather than "the store is defined".
|
||||
const void* initialBytes =
|
||||
(desc.HasDefinedContent != 0 && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
|
||||
//
|
||||
// P13 W4a: ON MONOLITH'S RECORD ARM THE BASE RIDES AN ORPHANING RESPECIFY TOO. That arm's
|
||||
// twin aliases this shadow and has no object to re-read it from, and an orphan is exactly
|
||||
// the call that reallocates it; the backend still uploads nothing for it, because its
|
||||
// upload is gated on the descriptor's HasDefinedContent and not on the pointer.
|
||||
const Bool carryBase = desc.HasDefinedContent != 0 || MG_Config::RecordArmAliasesFrontend();
|
||||
const void* initialBytes = (carryBase && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
|
||||
// kNeedsAck rides on the CALL and MGPipeResourceRespecifyNeedsAck(desc) decides per
|
||||
// record: only an immutable store (a glBufferStorage*) is a real synchronous
|
||||
// allocation and only it is allowed one. In monolith the acknowledgement is
|
||||
@@ -1091,7 +1087,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// one was emitted, so this cannot be false - but a zeroed record (null
|
||||
// handle, size 0) is not the answer if that pre-pass is ever relaxed.
|
||||
if (!MGPipeBuildSubDataRecord(handle, at, length, record, /*verbatimShadow=*/true)) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 (b1): the live-host-writes bit rides the content record, because
|
||||
// "someone may be writing these bytes without telling you" is a fact about the
|
||||
// CONTENT and not about the storage. It is set from the object's PUBLISHED
|
||||
@@ -1099,7 +1094,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// edge that announced it can never disagree. MGPipeBuildSubDataRecord does not
|
||||
// take the object, which is why it is set here and not in the builder.
|
||||
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
|
||||
#endif
|
||||
// `length` is this chunk's byte count, which the record also declares
|
||||
// (MGPipeBuildSubDataRecord writes it into the destination range) - passed
|
||||
// rather than re-read so the staged run and the record's own claim come from
|
||||
@@ -1125,7 +1119,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// store, or the pattern FillSubData expanded locally, and neither is this
|
||||
// client's untransformed shadow of the level.
|
||||
if (!MGPipeBuildSubDataRecord(handle, at, length, record, /*verbatimShadow=*/false)) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 (b1): THE SECOND CONTENT EMITTER, and it has to speak for the same reason
|
||||
// the first does. ApplyBufferWrite ASSIGNS the bit - a content record emitted
|
||||
// while nothing maps the buffer is how the state goes back to false - so a
|
||||
@@ -1133,7 +1126,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// map. `glBufferSubData` against a persistently mapped arena is legal and is
|
||||
// the ordinary Flywheel/Create shape, so that is not a corner.
|
||||
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
|
||||
#endif
|
||||
// The application's STAGING store, valid for the duration of the call only.
|
||||
MGPipeRouteBufferSubDataResident(record, base + (at - offset), length);
|
||||
// P7 wave 2 package C, OQ-10: `rsd=`, counted HERE rather than at the call
|
||||
@@ -1205,6 +1197,9 @@ namespace MobileGL::MG_Pipe {
|
||||
record.Res = handle;
|
||||
record.Offset = offset;
|
||||
record.Size = size;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResourceReadbacks, 1);
|
||||
}
|
||||
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's
|
||||
// epoch bump happens AFTER that writeback, never before.
|
||||
MGPipeRouteResourceReadback(record);
|
||||
@@ -1407,6 +1402,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it
|
||||
// stops a verify build.
|
||||
MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance();
|
||||
emitter.ForgetVao(lifetimeId);
|
||||
const Bool published = emitter.RecordIsPublished(handle);
|
||||
if (published) {
|
||||
MGPHandleOnly only{};
|
||||
@@ -1430,7 +1426,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// are the same call on the same handle and the second is a no-op, because Free
|
||||
// bumps no generation (the bump rides the next handout) and refuses a slot that is
|
||||
// no longer live at this generation.
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId);
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId, handle);
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::VertexElementsCso, handle);
|
||||
return published;
|
||||
}
|
||||
@@ -1969,7 +1965,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// P4a: whether a slot exists is this client's business, and a consumer that records
|
||||
// notices must not stop seeing a class announce itself.
|
||||
void NotifyAndFree(MGPipeKind kind, Uint64 lifetimeId, MGPipeHandle handle) {
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId);
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId, handle);
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle);
|
||||
}
|
||||
|
||||
@@ -2252,15 +2248,18 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
|
||||
}
|
||||
#endif
|
||||
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
|
||||
}
|
||||
|
||||
MGP_STICKY_FORWARD_PULL(HasOpenTransformFeedbackSpan);
|
||||
const auto* ctx = LiveContext();
|
||||
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
|
||||
namespace {
|
||||
// The frontend half of MGPipeNotifyCompileEnvChanged (PipeApply.h): registered once, at
|
||||
// static initialisation, by the translation unit that owns the forward.
|
||||
const Bool kCompileEnvHookRegistered = [] {
|
||||
MGPipeSetCompileEnvChangedHook([] {
|
||||
if (gPipeInputs->IsLive()) gPipeInputs->InvalidateCompileEnv();
|
||||
});
|
||||
return true;
|
||||
}();
|
||||
}
|
||||
|
||||
void PipeInputs::InvalidateCompileEnv() {
|
||||
@@ -2351,9 +2350,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// stamped, so every stamp this verb made falls behind the serial.
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGPipeClientClearVerbBoundary();
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
// The pending base instance belongs to the verb that was about to run, so leaving
|
||||
// one drops it.
|
||||
@@ -2643,6 +2640,44 @@ namespace MobileGL::MG_Pipe {
|
||||
// consumer mask gains bit 13 in the same
|
||||
// commit, or R-8 withholds the whole family.
|
||||
kMGPipeWiredBufferBindingSubsystem;
|
||||
|
||||
// P15: the dirty bits whose family this build, push mask and consumer emit - every
|
||||
// condition of the per-bit gate in MGPipeValidateForVerb except the dirty bit itself.
|
||||
Uint32 ComputeLiveDirtyBits(Uint64 pushMask) {
|
||||
Uint32 live = 0;
|
||||
for (Uint32 b = 0; b < kMGPipeDirtyCount; ++b) {
|
||||
const Uint64 subsystem = MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(b));
|
||||
if (subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||
P4aFamilyHasItsConsumer(subsystem) && P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
||||
P5eFamilyIsLive(subsystem, pushMask)) {
|
||||
live |= MGPipeDirtyBit(static_cast<MGPipeDirty>(b));
|
||||
}
|
||||
}
|
||||
return live;
|
||||
}
|
||||
|
||||
// Per verb, these conditions used to be re-asked for every bit (~15 consumer-registration
|
||||
// and dependency-table walks per draw). In monolith their only inputs are the push mask and
|
||||
// the registered consumer, so the answer is kept until either moves. Under a transport the
|
||||
// caps mirror answers and can move with a new session, so it is recomputed every verb.
|
||||
Uint32 LiveDirtyBits(Uint64 pushMask) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return ComputeLiveDirtyBits(pushMask);
|
||||
#endif
|
||||
static Bool valid = false;
|
||||
static Uint64 cachedPushMask = 0;
|
||||
static const MGPipeResourceOps* cachedConsumer = nullptr;
|
||||
static Uint32 cachedLive = 0;
|
||||
const MGPipeResourceOps* consumer = MGPipeGetResourceOps();
|
||||
if (!valid || cachedPushMask != pushMask || cachedConsumer != consumer) {
|
||||
cachedLive = ComputeLiveDirtyBits(pushMask);
|
||||
cachedPushMask = pushMask;
|
||||
cachedConsumer = consumer;
|
||||
valid = true;
|
||||
}
|
||||
return cachedLive;
|
||||
}
|
||||
|
||||
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
|
||||
// this a header that set the wrong constant - the sampler bit in the program header,
|
||||
// say - would switch the wrong family on and every gate would still pass.
|
||||
@@ -2798,7 +2833,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// and the static_assert below is that fact's trip wire rather than a claim in prose.
|
||||
//
|
||||
// NOT THREAD-LOCAL, AND THAT IS THIS FILE'S EXISTING RULE RATHER THAN A NEW ONE:
|
||||
// g_residualDue, g_omission and the verify latches beside it are file-scope too, and what
|
||||
// g_omission and the verify latches beside it are file-scope too, and what
|
||||
// keeps a single writer on them is the verb barrier (ROADMAP.md's G1 row states it for
|
||||
// gPipeInputs itself). A validate that runs on the apply thread is inside that barrier by
|
||||
// construction.
|
||||
@@ -2899,13 +2934,26 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool ApplierDerives = false;
|
||||
Bool ContextValuesWireLive = false;
|
||||
Bool P4aConsumer = false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 CapsGeneration = 0;
|
||||
#endif
|
||||
// One bit per FIELD - not per verb class. The class mask is applied at the walk
|
||||
// exactly as it always was, so "does this verb read this field" stays the walk's
|
||||
// business and this stays a statement about EMISSION alone.
|
||||
MGPipeFieldMask Supplied{};
|
||||
// P14: the walk itself, per verb class, built with Supplied: the non-sticky fields the
|
||||
// class reads (bit 7 set = copy it, clear = the emitted call supplies it), and the
|
||||
// sticky ones. The walk then visits only the ~47 fields a draw reads instead of testing
|
||||
// all 63 against three masks per verb.
|
||||
static constexpr Uint8 kCopy = 0x80;
|
||||
static_assert(kMGPipeInputFieldCount < kCopy, "field indices must leave the copy bit free");
|
||||
static constexpr SizeT kClasses = static_cast<SizeT>(MGPipeVerbClass::kClassCount);
|
||||
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Walk{};
|
||||
Array<Uint8, kClasses> WalkCount{};
|
||||
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Sticky{};
|
||||
Array<Uint8, kClasses> StickyCount{};
|
||||
// The walk's copy entries alone, in walk order (~11 of a draw's ~47): the copy loop
|
||||
// visits only these, and the stamp loop runs apart over the whole walk.
|
||||
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Copy{};
|
||||
Array<Uint8, kClasses> CopyCount{};
|
||||
};
|
||||
ResidualFillPlan g_fillPlan;
|
||||
|
||||
@@ -2978,6 +3026,26 @@ namespace MobileGL::MG_Pipe {
|
||||
MG_Remote::Client::CapsMirrorInstance().Generation() : 0;
|
||||
#endif
|
||||
g_fillPlan.Supplied = built;
|
||||
for (SizeT c = 0; c < ResidualFillPlan::kClasses; ++c) {
|
||||
const MGPipeFieldMask& classMask = kMGPipeClassFieldMask[c];
|
||||
Uint8 walk = 0;
|
||||
Uint8 sticky = 0;
|
||||
Uint8 copy = 0;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(classMask, field)) continue;
|
||||
if (kMGPipeInputFieldSticky[i]) {
|
||||
g_fillPlan.Sticky[c][sticky++] = static_cast<Uint8>(i);
|
||||
continue;
|
||||
}
|
||||
const Bool copied = !MGPipeFieldMaskHas(built, field);
|
||||
g_fillPlan.Walk[c][walk++] = static_cast<Uint8>(i | (copied ? ResidualFillPlan::kCopy : 0));
|
||||
if (copied) g_fillPlan.Copy[c][copy++] = static_cast<Uint8>(i);
|
||||
}
|
||||
g_fillPlan.WalkCount[c] = walk;
|
||||
g_fillPlan.CopyCount[c] = copy;
|
||||
g_fillPlan.StickyCount[c] = sticky;
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
@@ -3066,7 +3134,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// GLContext's m_currentVertexAttributes hold exactly those, so the diff below is
|
||||
// EMPTY on the one walk that must publish everything. The server's mirror is not
|
||||
// default: MGPipeApplierReset() clears the CSO store and the residual block and
|
||||
// leaves gPipeInputs.m_currentVertexAttribute holding the PREVIOUS context's
|
||||
// leaves gPipeInputs->m_currentVertexAttribute holding the PREVIOUS context's
|
||||
// defaults. So the InvalidateAll() a fresh context does to the set-hash
|
||||
// suppressor would have been cancelled two lines later by this diff, and the one
|
||||
// call P2 fully owns would publish nothing across a context change - exactly the
|
||||
@@ -3107,9 +3175,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// `g_attribDefaultLastHeader`, which is what it just published; there is nothing
|
||||
// the mirror could add that the wire does not already carry. A build that ever
|
||||
// needs the repair arm again has to earn it with a barriered row.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ClientRunsAhead()) return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
|
||||
#endif
|
||||
const auto* mirror = MGPipeFillAccess::VertexAttribDefaultsOf(MGPipeClientInputs());
|
||||
Bool reproduced = true;
|
||||
for (SizeT i = 0; i < kAttribs && reproduced; ++i) {
|
||||
@@ -3126,7 +3192,6 @@ namespace MobileGL::MG_Pipe {
|
||||
return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// set_context_values (P5c rv, CONTRACT-P5C.md §5.3): the residual-value record. One
|
||||
// POD carrying every value-class field no other set_* supplies - the two texture-unit
|
||||
// counters, the 15 per-target touched-buffer-binding counts and the five XFB values -
|
||||
@@ -3141,6 +3206,18 @@ namespace MobileGL::MG_Pipe {
|
||||
// server-role-only fixture) - nothing is emitted and the fields keep being pulled, byte
|
||||
// for byte as before (G1).
|
||||
Uint64 EmitContextValues(GLContext& ctx) {
|
||||
// P14: nothing the record reads has moved since the record whose hash the suppressor
|
||||
// still holds was built (same context, same input generation): it would be dropped.
|
||||
static MGPContextValues s_lastValues{};
|
||||
static Uint64 s_lastHash = 0;
|
||||
static Uint64 s_lastContextId = 0;
|
||||
static Uint64 s_lastGeneration = 0;
|
||||
auto& suppressor = MGPipeSetHashSuppressorInstance();
|
||||
const Uint64 contextId = ctx.GetTextureContextId();
|
||||
const Uint64 generation = ctx.GetContextValuesGeneration();
|
||||
if (s_lastHash != 0 && contextId == s_lastContextId && generation == s_lastGeneration &&
|
||||
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
|
||||
return 0;
|
||||
MGPContextValues values{};
|
||||
values.ActiveTextureUnit = static_cast<Uint32>(ctx.GetActiveTextureUnit());
|
||||
values.MaxTouchedTextureUnit = static_cast<Uint32>(ctx.GetMaxTouchedTextureUnit());
|
||||
@@ -3159,11 +3236,21 @@ namespace MobileGL::MG_Pipe {
|
||||
values.TransformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
|
||||
values.BoundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
|
||||
values.TransformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
|
||||
// P14: the record is rebuilt every draw and nearly always equal to the last one. When it
|
||||
// is byte-equal to the record whose hash the suppressor still holds, the suppressor
|
||||
// would drop it: skip the hash. (An InvalidateAll changes what the suppressor holds,
|
||||
// so a reset still re-emits.)
|
||||
s_lastContextId = contextId;
|
||||
s_lastGeneration = generation;
|
||||
if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 &&
|
||||
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
|
||||
return 0;
|
||||
// The whole record is the hash input, padding included - `values{}` zeroes it, so
|
||||
// the pad bytes are defined and the hash is stable.
|
||||
const Uint64 contentHash = XXH64(&values, sizeof(values), 0);
|
||||
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetContextValues,
|
||||
contentHash)) {
|
||||
s_lastValues = values;
|
||||
s_lastHash = contentHash == 0 ? 1 : contentHash; // the suppressor's own latch value
|
||||
if (!suppressor.ShouldEmit(MGPipeSuppressorSlot::SetContextValues, contentHash)) {
|
||||
return 0;
|
||||
}
|
||||
MGPipeRouteSetContextValues(values);
|
||||
@@ -3183,70 +3270,8 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeFieldMaskHas(mask, MGPipeInputField::GetBoundTransformFeedbackLifetimeId) ||
|
||||
MGPipeFieldMaskHas(mask, MGPipeInputField::GetTransformFeedbackCapturedVertices);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// set_residual_value_state (P2 brief D9, ARCHITECTURE.md 9.4).
|
||||
//
|
||||
// Since P2 the block is one Uint64 of capability bits, and every one of the 35 is
|
||||
// ALSO answerable from the assembled working block now that the contract closed the
|
||||
// FramebufferSrgb / DepthClamp / TextureCubeMapSeamless storage holes. That
|
||||
// redundancy is the whole point: the bits are read HERE from the frontend, and the
|
||||
// applier compares them against the assembled answer, so the day a later call takes
|
||||
// a capability over and forgets to carry it the block says so on the next draw
|
||||
// (Fatal{PipeResidualDiverged, "<Cap>"}).
|
||||
//
|
||||
// Building the carried bits from the ASSEMBLED block instead would make the trip
|
||||
// wire a tautology, which is exactly the failure P1's entry compare had and P2 is
|
||||
// paying to remove.
|
||||
//
|
||||
// ON THIS BRANCH IT IS STILL HALF A TAUTOLOGY, and saying so is part of the honesty
|
||||
// the trip wire is for: the applier compares these bits against gPipeInputs'
|
||||
// capability mirror, and while MGPipeDeriveRenderStateFields is a stub that mirror is
|
||||
// filled by the residual fill from the SAME IsCapabilityEnabled accessor a few lines
|
||||
// below. It becomes an independent oracle the moment package A's c1 lands and the
|
||||
// fill stops copying those fields. What it proves already is that the block is
|
||||
// emitted, sized and suppressed - the resid= byte class and the one divergence it
|
||||
// caught during development (GL_DITHER) are that evidence.
|
||||
//
|
||||
// Emitted once per context and again whenever the capability set may have moved
|
||||
// (D9). THE SHUTTER FOR THAT IS NEW_RENDER_STATE, NOT NEW_PIPELINE_STATE, and the
|
||||
// difference is a hole rather than a nicety: SetCapability's ClipDistance0..7 arms
|
||||
// are deliberately NOT BumpVersions() (RenderState.cpp says so in as many words), so
|
||||
// glEnable(GL_CLIP_DISTANCE0) moves m_version alone - and ClipDistance0..7 are 8 of
|
||||
// the 35 CapabilityInputs this block carries. Arming on the pipeline version would
|
||||
// leave the trip wire disarmed for those eight for an unbounded window, which is the
|
||||
// under-firing direction ARCHITECTURE.md 13.2 names as the dangerous one, and no gate
|
||||
// could see it: a block that is never emitted cannot diverge.
|
||||
//
|
||||
// So the arming is the coarsest always-true shutter - either render-state counter
|
||||
// moved - which is the same answer DirtySurface.def's derivation gives SetCapability.
|
||||
// It over-fires (a glViewport re-sends 8 bytes and re-runs the compare) and that is
|
||||
// the intended trade: over-firing costs one 35-bit loop on a verb that already moved
|
||||
// render state, under-firing renders stale.
|
||||
Uint64 EmitResidualValueState(GLContext& ctx) {
|
||||
ResidualValueBlock block{};
|
||||
constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
|
||||
static_assert(kCapabilityCount <= 64, "CapabilityBits is a Uint64");
|
||||
for (SizeT i = 0; i < kCapabilityCount; ++i) {
|
||||
if (ctx.IsCapabilityEnabled(static_cast<CapabilityInput>(i))) {
|
||||
block.CapabilityBits |= Uint64{1} << i;
|
||||
}
|
||||
}
|
||||
MGPipeRouteSetResidualValueState(block);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// ByteClass::ResidualValueBlock has been a placeholder that "stays at 0
|
||||
// until P2" since P0. This is what makes it non-zero.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::ResidualValueBlock,
|
||||
sizeof(ResidualValueBlock));
|
||||
}
|
||||
return sizeof(MGPResidualValueState) + sizeof(ResidualValueBlock);
|
||||
}
|
||||
|
||||
// Set when the capability set may have moved, cleared when the block goes out. It is
|
||||
// not part of the tracker because it is emission state, not a shutter: the shutter
|
||||
// (the render-state counter) has already been consumed by the time this is read.
|
||||
Bool g_residualDue = true;
|
||||
|
||||
// The residual block is the ONE emission whose gate names a subsystem constant
|
||||
// directly instead of going through MGPipeSubsystemForDirty, and the reason is that
|
||||
@@ -3440,13 +3465,23 @@ namespace MobileGL::MG_Pipe {
|
||||
field);
|
||||
}
|
||||
|
||||
void MGPipeValidateForVerbBarriered(MGPipeVerb verb) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
g_forceBarrieredFill = true;
|
||||
MGPipeValidateForVerb(verb);
|
||||
g_forceBarrieredFill = false;
|
||||
#else
|
||||
// Without a transport every fill is barriered already (fillOwed is constant true).
|
||||
MGPipeValidateForVerb(verb);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- the validate point (P2 brief D1) ----
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb) {
|
||||
// P5f (f1): the fill side's one new spelling. With MOBILEGL_IPC_ROLE_SPLIT_STATE=1 this
|
||||
// is the CLIENT-role block and gPipeInputs is the server's alone; off, or under
|
||||
// monolith transport, it folds back onto gPipeInputs and nothing below changes.
|
||||
PipeInputs& inputs = MGPipeClientInputs();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- P5e (ra), CONTRACT-P5E §3: WHO OWNS gPipeInputs FOR THIS VERB ----------------
|
||||
//
|
||||
// Under run-ahead the block is SERVER-ROLE MEMORY for an unbarriered record: the
|
||||
@@ -3461,7 +3496,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// every verb) and the guard below then fires Fatal{RoleViolation, "gPipeInputs"} on
|
||||
// the first unbarriered verb, by name.
|
||||
const Bool runAhead = ClientRunsAhead();
|
||||
#endif
|
||||
ParsePoisonOmissionKnob();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
ArmVerify();
|
||||
@@ -3508,6 +3542,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// delete it and the guard below aborts with Fatal{RoleViolation, "gPipeInputs"}
|
||||
// naming MGPipeValidateForVerb, because the guard now tests the fact rather than
|
||||
// taking the claim.
|
||||
if (runAhead) MG_Remote::Client::ClientSession::NoteBarrieredVerbForStats(kMGPipeVerbNames[static_cast<SizeT>(verb)]);
|
||||
QuiesceApplierBeforeFill("MGPipeValidateForVerb");
|
||||
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
|
||||
"MGPipeValidateForVerb", /*isBarrieredFill=*/barriered);
|
||||
@@ -3522,7 +3557,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The client is filling, so whatever the server stamped at its last verb boundary
|
||||
// is withdrawn: the stamps below are the CLIENT's again and a stale read is a
|
||||
// defect, not a residual pull. Disarming here rather than at the end of the
|
||||
@@ -3542,7 +3576,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// (PipeApplier::LeaveApplier) - which is CONTRACT-P5E §3.2's per-role stamp
|
||||
// ownership, landed as the dual block rather than as moved fields.
|
||||
MGPipeClientClearVerbBoundary();
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, verb);
|
||||
}
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
@@ -3606,25 +3639,28 @@ namespace MobileGL::MG_Pipe {
|
||||
// supplies a field: with no live session (a monolith transport, the bring-up window, a
|
||||
// server-role-only fixture) nothing is emitted and the fields keep being pulled, byte
|
||||
// for byte as before (G1).
|
||||
//
|
||||
// P13 W4d: AND ON MONOLITH'S RECORD ARM, where the route is the in-process applier
|
||||
// (Mono_SetContextValues) and the backend reads what the record put in gPipeInputs - the
|
||||
// same supplier as a wire, one call away.
|
||||
const Bool contextValuesWireLive =
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
MG_Remote::Client::ContextValuesWireLive();
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? MG_Remote::Client::ContextValuesWireLive()
|
||||
: MG_Config::RecordArmAliasesFrontend();
|
||||
#else
|
||||
constexpr Bool contextValuesWireLive = false;
|
||||
// P13 W8: a library without a transport is ALWAYS monolith's record arm, so it takes the
|
||||
// eight fields from the record exactly as the split build's monolith does. This branch
|
||||
// used to answer a constant false - the pre-W5 pull shape - and left the shipped (FCL)
|
||||
// build pulling them by residual fill while the split build's monolith carried them; the
|
||||
// recorder goldens (MG_Test/Pipe/RecorderGoldenTest.cpp) found the two streams differ.
|
||||
const Bool contextValuesWireLive = MG_Config::RecordArmAliasesFrontend();
|
||||
#endif
|
||||
// AND THE SEVENTH IS P5eFamilyIsLive (ID-106), which is the same sentence for the
|
||||
// binding-point family and asks bit 13's OWN consumer bit rather than the resource
|
||||
// family's. It answers true for every subsystem but bit 13, so nothing that emitted
|
||||
// before P5e changes.
|
||||
const auto wants = [&](MGPipeDirty bit) {
|
||||
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
|
||||
return subsystem != 0 && (pushMask & subsystem) != 0 &&
|
||||
(kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||
P4aFamilyHasItsConsumer(subsystem) &&
|
||||
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
|
||||
P5eFamilyIsLive(subsystem, pushMask) &&
|
||||
(dirty & MGPipeDirtyBit(bit)) != 0;
|
||||
};
|
||||
const Uint32 liveDirty = LiveDirtyBits(pushMask) & dirty;
|
||||
const auto wants = [&](MGPipeDirty bit) { return (liveDirty & MGPipeDirtyBit(bit)) != 0; };
|
||||
Uint64 payloadBytes = 0;
|
||||
|
||||
// A fresh context is a fresh server, and that is true of EVERY subsystem, so it is
|
||||
@@ -3701,7 +3737,6 @@ namespace MobileGL::MG_Pipe {
|
||||
// would be suppressed as unchanged and the server would draw against a window it
|
||||
// had just been told to empty.
|
||||
MGPipeShaderBufferEmitterInstance().Reset();
|
||||
g_residualDue = true;
|
||||
}
|
||||
|
||||
// P4a's segment, in ARCHITECTURE.md 5.4's RECOMMENDED order - framebuffer, then
|
||||
@@ -3781,7 +3816,6 @@ namespace MobileGL::MG_Pipe {
|
||||
payloadBytes += EmitVertexAttribDefaults(*ctx, tracker.FreshlyPrimed());
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, emitted when any covered
|
||||
// value moved. THE GATE IS THE SUBSYSTEM BIT PLUS THE WIRE BEING LIVE - the family has
|
||||
// no dirty bit (NoDirtyBitOwnsTheResidualSubsystem) and no P4a consumer predicate (it
|
||||
@@ -3792,7 +3826,6 @@ namespace MobileGL::MG_Pipe {
|
||||
VerbMaskReadsContextValues(mask)) {
|
||||
payloadBytes += EmitContextValues(*ctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
// P3a's vertex segment, in the order the design fixes: vertex elements, then the
|
||||
// vertex buffers that fill them, then the index binding. All three are LIVE now (m1):
|
||||
@@ -3848,51 +3881,42 @@ namespace MobileGL::MG_Pipe {
|
||||
const Bool applierDerives = ApplierDerivesRenderStateFields();
|
||||
// BY VALUE, NOT BY REFERENCE: the memo's storage is rebuilt in place when the key moves,
|
||||
// and the walk below holds this across 63 iterations.
|
||||
const MGPipeFieldMask supplied =
|
||||
SuppliedFieldMask(pushMask, applierDerives, contextValuesWireLive);
|
||||
for (SizeT i = 0; fillOwed && i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
(void)SuppliedFieldMask(pushMask, applierDerives, contextValuesWireLive);
|
||||
// The plan the call above just validated (or rebuilt) for this key: the class's fields in
|
||||
// index order, sticky ones apart - the same visits, in the same order, as testing all 63.
|
||||
const SizeT planClass = static_cast<SizeT>(verbClass);
|
||||
(void)mask;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
if (kMGPipeInputFieldSticky[i]) {
|
||||
// Stamped once by the first fill that sees a live context; fresh through the
|
||||
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
|
||||
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
if (kMGPipeInputFieldSticky[i]) continue;
|
||||
for (SizeT k = 0; fillOwed && k < g_fillPlan.StickyCount[planClass]; ++k) {
|
||||
// Stamped once by the first fill that sees a live context; fresh through the
|
||||
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
|
||||
const SizeT i = g_fillPlan.Sticky[planClass][k];
|
||||
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
|
||||
}
|
||||
#endif
|
||||
if (!MGPipeFieldMaskHas(supplied, field)) MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
if (fillOwed) {
|
||||
// The copies first, then the stamps: no copy reads a stamp, so splitting the one walk
|
||||
// into two loops changes nothing but the cost (the stamp loop no longer reloads its
|
||||
// bounds and the omission pair around an out-of-line CopyField per entry).
|
||||
const Uint8* const copies = g_fillPlan.Copy[planClass].data();
|
||||
const SizeT copyCount = g_fillPlan.CopyCount[planClass];
|
||||
for (SizeT k = 0; k < copyCount; ++k) {
|
||||
MGPipeFillAccess::CopyField(inputs, *ctx, static_cast<MGPipeInputField>(copies[k]));
|
||||
}
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// The value is copied either way; only the stamp is withheld for the omitted pair.
|
||||
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
|
||||
#endif
|
||||
}
|
||||
// ---- step 4b: the residual value block, and it goes out HERE ----
|
||||
// ARMED OUTSIDE THE SUBSYSTEM GATE: whether the capability set may have moved is a
|
||||
// fact about the frontend, not about which subsystems this build pushes, and a
|
||||
// per-subsystem A/B that turns the block off must not also lose the record that one
|
||||
// is owed.
|
||||
if ((dirty & (MGPipeDirtyBit(MGPipeDirty::NewRenderState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewPipelineState))) != 0) {
|
||||
g_residualDue = true;
|
||||
}
|
||||
// Its trip wire compares the carried bits against the ASSEMBLED capability mirror,
|
||||
// and that mirror is written either by the applier's derivation or by the fill loop
|
||||
// above - so the block is only meaningful once step 4 has run. Emitting it with the
|
||||
// other calls would compare against the previous verb's answer.
|
||||
if ((pushMask & kMGPipeSubsystemResidualValues) != 0) {
|
||||
// The trip wire compares against the ASSEMBLED capability mirror, so it can only
|
||||
// run at a verb whose class actually carries that mirror - IsCapabilityEnabled is
|
||||
// in seven of the nine class masks and kQuery and kXfbSpan do not read it, so at
|
||||
// those verbs the mirror is whatever the last verb that did read it left behind.
|
||||
// The change is HELD rather than dropped: dropping it would silently disarm the
|
||||
// wire for a capability that moved between two queries.
|
||||
if (g_residualDue && MGPipeFieldMaskHas(mask, MGPipeInputField::IsCapabilityEnabled)) {
|
||||
payloadBytes += EmitResidualValueState(*ctx);
|
||||
g_residualDue = false;
|
||||
// The value is copied either way; only the stamp is withheld for the omitted pair,
|
||||
// which is put back to what it held after the branch-free stamp of the whole walk.
|
||||
const Uint8* const walk = g_fillPlan.Walk[planClass].data();
|
||||
const SizeT walkCount = g_fillPlan.WalkCount[planClass];
|
||||
const Uint64 serial = filled.CurrentVerbSerial;
|
||||
const Bool omit = g_omission.Armed && g_omission.Verb == verb;
|
||||
const SizeT omitted = omit ? static_cast<SizeT>(g_omission.Field) : 0;
|
||||
const Uint64 omittedGen = omit ? filled.FilledGen[omitted] : 0;
|
||||
for (SizeT k = 0; k < walkCount; ++k) {
|
||||
filled.FilledGen[walk[k] & ~ResidualFillPlan::kCopy] = serial;
|
||||
}
|
||||
if (omit) filled.FilledGen[omitted] = omittedGen;
|
||||
#endif
|
||||
}
|
||||
if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) {
|
||||
// PipeStats::RecordDrawPayloadBytes has been implemented and unit-tested since
|
||||
@@ -3945,7 +3969,6 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeImageEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
MGPipeShaderBufferEmitterInstance().Reset();
|
||||
g_residualDue = true;
|
||||
g_attribDefaultLastHeader = MGPVertexAttribDefaults{};
|
||||
g_lastFillWasBarriered = true;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
|
||||
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
|
||||
// it.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct PipeInputs;
|
||||
@@ -34,6 +33,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// entry points with one call site per verb instead of nine.
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb);
|
||||
|
||||
// P15: the validate point again for a verb whose first validate answered UNBARRIERED for a
|
||||
// per-call reason that did not hold at emission (a pack-buffer readback that falls back to
|
||||
// the reply form). Runs the barriered fill: quiesce, then fill, exactly as a barriered verb.
|
||||
void MGPipeValidateForVerbBarriered(MGPipeVerb verb);
|
||||
|
||||
// Ends the verb in flight without starting another: bumps the serial, so every field the
|
||||
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
||||
// it aborts as Fatal{UnmigratedPipeInput, "<Field>@<none>"} - which is what such a read
|
||||
@@ -176,7 +180,3 @@ namespace MobileGL::MG_Pipe {
|
||||
// neither declared nor defined.
|
||||
#define MGP_SET_BASE_INSTANCE(BaseInstance) \
|
||||
::MobileGL::MG_Pipe::MGPipeSetPendingBaseInstance(static_cast<::MobileGL::Uint32>(BaseInstance))
|
||||
#else
|
||||
#define MGP_FILL(Verb) ((void)0)
|
||||
#define MGP_SET_BASE_INSTANCE(BaseInstance) ((void)0)
|
||||
#endif
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/CompositeResolver.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
@@ -211,29 +210,70 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
Uint64 bytes = 0;
|
||||
const MGPipeHandle cso = AcquireShaderCso(*program, bytes);
|
||||
// (ShaderCso, Version) IS the key, so the latch is the key: an unchanged pair means
|
||||
// the server already holds these bytes and re-sending them would move the record's
|
||||
// serial for nothing.
|
||||
if (cso == m_constantsCso && version == m_constantsVersion) return bytes;
|
||||
// THE KEY IS PER HANDLE, because the block is: the applier keeps one image per shader
|
||||
// CSO record, so a program drawn again after another one ran in between still holds
|
||||
// what it was last sent. An unchanged (ShaderCso, Version) pair means the server
|
||||
// already holds these bytes and re-sending them would move the record's serial for
|
||||
// nothing. The latch lives beside the record latch, so a recycled slot or a death
|
||||
// retires it with that one; a re-issued create and Reset() retire it explicitly.
|
||||
Latch* latch = MGPipeHandleIsNull(cso) ? nullptr : &LatchFor(cso);
|
||||
const Bool held = latch != nullptr && latch->ConstantsLive && latch->ConstantsEpoch == m_constantsEpoch;
|
||||
if (held && latch->ConstantsVersion == version) return bytes;
|
||||
// Taken on every send, span or whole, so the range always describes the bytes
|
||||
// written since the image the server holds for this program.
|
||||
const auto dirty = program->TakeUBODirtyRange();
|
||||
|
||||
m_lastConstants = MGPGlobalConstants{};
|
||||
m_lastConstants.ShaderCso = cso;
|
||||
m_lastConstants.Version = version;
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
// P14: the server holds this program's previous image while its latch is held (every
|
||||
// path that could drop or replace the applier's block retires the latch), so only the
|
||||
// span the frontend wrote since then goes out - when it is small enough to be worth it.
|
||||
const Uint8* image = static_cast<const Uint8*>(program->GetUBOData());
|
||||
if (held && latch->ConstantsSize == size && size <= 0xffffu) {
|
||||
Uint32 first = std::min<Uint32>(dirty.Begin, size);
|
||||
Uint32 last = std::min<Uint32>(dirty.End, size);
|
||||
// Nothing written but the version moved: one byte carries the new version.
|
||||
if (first >= last) {
|
||||
first = 0;
|
||||
last = 1;
|
||||
}
|
||||
const Uint32 length = last - first;
|
||||
if (length * 2 <= size) {
|
||||
m_lastConstants.Range = (first << 16) | length;
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image + first);
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, image + first, static_cast<Uint64>(length));
|
||||
latch->ConstantsVersion = version;
|
||||
++m_constantSets;
|
||||
++m_constantSpans;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, length);
|
||||
}
|
||||
return bytes + sizeof(MGPGlobalConstants) + length;
|
||||
}
|
||||
}
|
||||
// THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which
|
||||
// is what a monolith emission is - and the bytes ride beside it as a companion
|
||||
// pointer. Offset carries the staging address for diagnostics only; nothing reads
|
||||
// it as a length.
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image);
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
// `size` is GetUBOSize(), and it is passed because the record declares 0 - the
|
||||
// monolith convention (the bytes ride beside the record) that CONTRACT-P5 table 1
|
||||
// rule A cannot keep under split. It is the row's largest and least bounded blob,
|
||||
// per program per frame, so it is also the one R-10's max-record counter watches.
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, program->GetUBOData(),
|
||||
static_cast<Uint64>(size));
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, image, static_cast<Uint64>(size));
|
||||
if (latch != nullptr) {
|
||||
latch->ConstantsLive = true;
|
||||
latch->ConstantsEpoch = m_constantsEpoch;
|
||||
latch->ConstantsVersion = version;
|
||||
latch->ConstantsSize = size;
|
||||
}
|
||||
++m_constantSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
|
||||
@@ -468,7 +508,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// not happen to move the content version, of which a recycled slot is one.
|
||||
// Invalidated rather than re-emitted here, because this function has no business
|
||||
// deciding when the constants go out: the next EmitGlobalConstants sees an
|
||||
// unlatched key and sends them.
|
||||
// unlatched key and sends them whole.
|
||||
latch.ConstantsLive = false;
|
||||
if (m_constantsCso == handle) {
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
@@ -566,12 +607,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// The RECORD half stays, and that is the rule rather than an oversight: the applier
|
||||
// keeps its shader-CSO records across a make-current because a program lives in a share
|
||||
// group, and re-publishing one would move its Serial for nothing. The global-constants
|
||||
// key goes with the working state because its record's bytes are per (Cso, Version) and
|
||||
// a fresh server has not been told them.
|
||||
// keys go with the working state because its record's bytes are per (Cso, Version) and
|
||||
// a fresh server has not been told them: every per-handle key retires at once by epoch.
|
||||
void Reset() {
|
||||
m_boundCso = kMGPipeNullHandle;
|
||||
m_drawCso = kMGPipeNullHandle;
|
||||
m_dispatchCso = kMGPipeNullHandle;
|
||||
++m_constantsEpoch;
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
// The composite memo's freshness goes with them - and only its freshness. Its
|
||||
@@ -589,9 +631,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// ---- what a unit case reads ----
|
||||
const MGPProgramDesc& LastProgramDesc() const { return m_lastDesc; }
|
||||
const MGPGlobalConstants& LastGlobalConstants() const { return m_lastConstants; }
|
||||
// THE (Cso, Version) KEY set_global_constants is suppressed against. Exposed so a case
|
||||
// can pin that a re-issued create_shader_state invalidates it - the applier clears the
|
||||
// block on the re-issue (wire W6), so a latch that survived it would never re-send.
|
||||
// THE LAST (Cso, Version) set_global_constants sent. Exposed so a case can pin that a
|
||||
// re-issued create_shader_state invalidates it - the applier clears the block on the
|
||||
// re-issue (wire W6), so a latch that survived it would never re-send. The per-handle
|
||||
// key the suppression reads retires at the same three places.
|
||||
MGPipeHandle GlobalConstantsCso() const { return m_constantsCso; }
|
||||
Uint32 GlobalConstantsVersion() const { return m_constantsVersion; }
|
||||
// D-J3's counted refusal: programs whose linked snapshot carried more modules than
|
||||
@@ -605,6 +648,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 DrawProgramSetCount() const { return m_drawSets; }
|
||||
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
|
||||
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
|
||||
Uint64 GlobalConstantsSpanCount() const { return m_constantSpans; }
|
||||
// P5e (pg). The record a case reads back, and the two counters D-J3's rule asks for:
|
||||
// how many set_program_bindings went out, and how many programs were REFUSED because
|
||||
// one of their three sets is larger than the record can name. A refusal leaves the
|
||||
@@ -623,7 +667,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// composite.
|
||||
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
|
||||
const Uint64 lifetimeId = program.GetLifetimeId();
|
||||
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const MGPipeHandle existing =
|
||||
MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::ShaderCso, lifetimeId, program.PipeHandleHint());
|
||||
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
|
||||
// an ordinary ShaderCso handle in every other respect - the same kind, the same
|
||||
@@ -650,6 +695,13 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint32 BindingsGen = 0;
|
||||
Uint32 BindingsBackendStateVersion = 0;
|
||||
Uint32 BindingsBlockBindingVersion = 0;
|
||||
// set_global_constants' key, here for the bindings' reason: what the applier's record
|
||||
// at this handle holds, as (Version, Size), valid only while ConstantsLive and the
|
||||
// epoch is the emitter's current one.
|
||||
Bool ConstantsLive = false;
|
||||
Uint32 ConstantsVersion = 0;
|
||||
Uint32 ConstantsSize = 0;
|
||||
Uint64 ConstantsEpoch = 0;
|
||||
};
|
||||
|
||||
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
|
||||
@@ -704,8 +756,13 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle m_boundCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_drawCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
|
||||
// The last (Cso, Version) set_global_constants sent, for the unit cases; the suppression
|
||||
// itself reads the per-handle Latch keys.
|
||||
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
|
||||
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
// Moved by Reset(): a Latch's constants key holds only at the epoch it was taken in.
|
||||
Uint64 m_constantsEpoch = 1;
|
||||
Uint64 m_constantSpans = 0;
|
||||
|
||||
Uint64 m_creates = 0;
|
||||
Uint64 m_binds = 0;
|
||||
@@ -726,4 +783,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
// upload; resource_subdata, the hot one, is observed through the pure builders below
|
||||
// instead (MGPipeBuildSubDataRecord / MGPipeForEachSubDataRecordRange), which is also what
|
||||
// lets a test drive the splitter at both of its bounds without a 4 GiB buffer.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
@@ -282,7 +281,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// gating it would make the vertex-input subsystem emit null handles whenever the
|
||||
// resource subsystem is off. Only the CALLS are gated (D-A1).
|
||||
MGPipeHandle Acquire(BufferObject& buffer) {
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), buffer.PipeHandleHint());
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
||||
m_bySlot[slot].Object = &buffer;
|
||||
@@ -293,7 +293,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// The handle a buffer already has, or the null handle. Never mints - the emission
|
||||
// path calls Acquire, the query paths call this.
|
||||
MGPipeHandle Find(const BufferObject& buffer) const {
|
||||
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||
return MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(),
|
||||
buffer.PipeHandleHint());
|
||||
}
|
||||
|
||||
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
|
||||
@@ -732,4 +733,3 @@ namespace MobileGL::MG_Pipe {
|
||||
gMGPipeCallbacks.OnGpuWritten(res, 1, &whole);
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -37,10 +37,7 @@
|
||||
// detection. Neither is reproduced here and neither may be.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
#endif
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
@@ -862,7 +859,9 @@ namespace MobileGL::MG_Pipe {
|
||||
unitSampler ? unitSampler.get() : texture->GetSamplerObject().get();
|
||||
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
|
||||
|
||||
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
entry.Texture = MGPipeSlots().AcquireHinted(
|
||||
MGPipeKind::Texture, texture->GetLifetimeId(),
|
||||
texture->PipeHandleHint(ITextureObject::PipeHintKind::Texture));
|
||||
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
|
||||
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
|
||||
// since this header is included BY TextureEmit.h). One early-out per unit per
|
||||
@@ -979,7 +978,8 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle,
|
||||
Uint64& payloadBytes) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture.GetLifetimeId());
|
||||
MGPipeSlots().AcquireHinted(MGPipeKind::SamplerViewCso, texture.GetLifetimeId(),
|
||||
texture.PipeHandleHint(ITextureObject::PipeHintKind::SamplerView));
|
||||
const SizeT slot = handle.Slot;
|
||||
if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1);
|
||||
ViewLatch& latch = m_viewLatch[slot];
|
||||
@@ -1178,4 +1178,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
// Header-only for the same ownership reason as Tracker.h and CsoCache.h: the root
|
||||
// CMakeLists.txt that would name a new .cpp belongs to package A and is frozen behind the
|
||||
// p2/contract tag.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
@@ -104,6 +103,13 @@ namespace MobileGL::MG_Pipe {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The server now holds the set `contentHash` names without this slot having emitted it
|
||||
// (P14: bind_vertex_elements re-applies the array's kept set). Same 0 -> 1 remap as
|
||||
// ShouldEmit, so the next ShouldEmit of the same hash answers false.
|
||||
void Latch(MGPipeSuppressorSlot slot, Uint64 contentHash) {
|
||||
m_lastEmitted[static_cast<SizeT>(slot)] = contentHash == 0 ? 1 : contentHash;
|
||||
}
|
||||
|
||||
// A context change or a server reset: what the server has is no longer what this
|
||||
// slot last emitted, so the next resolved set must go out whatever it hashes to.
|
||||
void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast<SizeT>(slot)] = 0; }
|
||||
@@ -131,4 +137,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *suppressor;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
// HEADER-ONLY, and the wired constant below is what switches the family on, for the ownership
|
||||
// reason PipeFill.cpp states in full: that file belongs to the contract package for the whole
|
||||
// phase, so the bit that turns an emitter on is a constant in the emitter's OWN header.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
@@ -274,4 +273,3 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user