Compare commits

...
11 Commits
Author SHA1 Message Date
swung0x48 37cb446bda [CI] (scripts/data): lower the text hygiene baseline after the comment cleanup 2026-10-15 23:16:52 -04:00
swung0x48 7bb5a9ccbe [Docs] (.github/workflows, docs/Disaggregated, MG_IntegrationTest): shorten the long comments in the integration lanes and test workflow 2026-10-15 23:16:45 -04:00
swung0x48 1161d489e2 [Build] (MG_IntegrationTest): register each integration lane with one mgl_itest_lane call
- The generated test registrations are identical in the monolith, verify and split builds.
2026-10-15 22:41:35 -04:00
swung0x48 92f1b9b231 [CI] (scripts/data): lower the text hygiene baseline after the comment cleanup 2026-10-15 22:29:04 -04:00
swung0x48 265966a15d [Docs] (MG_Backend/DirectGLES, MG_Backend/MGPipe, MG_Backend/Record, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Pipe/generated, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test/Pipe, MG_Test/Wire, MG_Util/Metrics, MobileGL, scripts, scripts/ci): cite symbols, not line numbers, in the shortened files 2026-10-15 22:27:15 -04:00
swung0x48 23ee8bdb49 [Docs] (docs/Disaggregated, MG_Remote, MG_Remote/Client, MG_Remote/Protocol, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire): shorten long comments and move their design arguments into docs 2026-10-15 21:53:39 -04:00
swung0x48 37ecba200b [Docs] (docs/Disaggregated, MG_Impl/Pipe, MG_Remote/Client): shorten long comments and move their design arguments into docs 2026-10-15 21:18:49 -04:00
swung0x48 7ec1448f03 [Docs] (docs/Disaggregated, MG_Backend/MGPipe, MG_IntegrationTest/Harness, MG_Pipe): shorten long comments and move their design arguments into docs 2026-10-15 20:48:58 -04:00
swung0x48 211ca08b0c [Docs] (docs/Disaggregated): point the indexes' prose at docs/archive 2026-10-15 20:06:45 -04:00
swung0x48 aabca3c171 [Docs] (.claude/skills, .github/workflows, CMakeLists.txt, docs/archive, docs/Disaggregated, docs/MobileVA, docs/Performance, MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/Record, MG_Gbm, MG_Impl, MG_Impl/EGLImpl, MG_Impl/GLImpl, MG_Impl/GLXImpl, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Pipe/generated, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test/Pipe, MG_Test/Util, MG_Test/Wire, MobileGL, scripts, scripts/data, tools/device_bench, userguide): archive the planning notes and agent contracts under docs/archive
- DEBTS.md and OPEN-QUESTIONS.md stay maintained, now in docs/Disaggregated.
2026-10-15 20:01:45 -04:00
swung0x48 0e3e2b7761 [CI, Docs] (CONTRIBUTING.md, scripts): freeze docs/archive and treat moved files as moves 2026-10-15 19:55:42 -04:00
1134 changed files with 3853 additions and 12351 deletions
@@ -6,8 +6,8 @@ description: Bring up, rebuild, deploy, switch backends on, test and debug the "
# MobileGL on anland KDE Plasma
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
Human-readable runbook (Chinese, same content): `docs/Disaggregated/notes/anland/runbook-plasma.md`.
Background: `docs/Disaggregated/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
Human-readable runbook (Chinese, same content): `docs/archive/notes/anland/runbook-plasma.md`.
Background: `docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
parameters are documented at the top of each script and in `scripts/env.sh`.
@@ -26,7 +26,7 @@ nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2,
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. No per-process environment is needed.
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
10. X11 (`docs/Disaggregated/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
10. X11 (`docs/archive/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
## First-time prerequisites (Windows host)
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/Disaggregated/notes/anland/
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/archive/notes/anland/
# plan-x11-gpu.md): Xwayland drawing with glamor on MobileGL, and GLX windows presented through
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
#
+101 -377
View File
@@ -780,78 +780,22 @@ jobs:
ctest --output-on-failure -L integration-verify --no-tests=error --output-junit "${RUNNER_TEMP}/verify.xml"
fi
# The push-only unit tests, on the verify runtime.
#
# WHY HERE AND NOT IN `test`. The entries themselves are registered in EVERY build - they
# have to be, or `ctest -N` would stop matching name-for-name between the pull and the push
# build (gate G2). What is push-only is what they assert about: MGPipeRenderStateSpans.cpp
# and PipeApply.cpp are appended to SOURCE_FILES inside the `if (MOBILEGL_PIPE_PUSH)` block,
# which is exactly how the pull build stays symbol-identical, so in a pull build each case
# opens with `#if !MOBILEGL_PIPE_PUSH GTEST_SKIP() << "push not compiled in"`. The `test`
# job therefore runs G6's chunk-table walk and G10's residual assertions as a column of
# skips: CI executes the NAMES and never one of the assertions. This job unpacks a build
# that compiled them, so it is the first place in CI where they actually run.
#
# This artifact already carries them - the packaging step above tars
# ${BUILD_DIR}/MobileGL/MG_Test whole - so the whole cost is the run, which is ~14 s for
# ~1490 entries. --no-tests=error, because a packaging change that stopped shipping the
# unit binaries would otherwise report a green run of nothing.
# The unit suite again, on the verify library: cases compiled only under MOBILEGL_PIPE_VERIFY
# skip in the `test` job's build and assert only here, as do poison-gated cases on branches
# where that build is a monolith. The packaging step above ships MG_Test whole, so the cost
# is the run itself. --no-tests=error, so a packaging change that dropped the unit binaries
# fails here instead of passing with nothing run.
- name: Unit tests on the verify runtime (G6, G10)
working-directory: build-verify
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-verify.xml"
# The always-on P2 and P3a negative controls (G8, G10, G12), which are labelled
# integration-gpu and not integration-verify - they are about the handle key, the subsystem
# switch and the map-persistent counter, not about the comparator - so the lane above does
# not reach them. They are run HERE because this is the only CI job that unpacks a
# MOBILEGL_PIPE_PUSH build: every one of them reads a counter out of the library's summary
# line, and both the counters and their brackets are #if MOBILEGL_PIPE_PUSH, so in the pull
# `integration` job the entries exist (gate G2 requires the same names in both builds) but
# have nothing to assert.
#
# An arm whose subsystem has not landed on this tree SKIPS with the reason (never absent,
# never a green that asserted nothing), so this step is green through the P2 and P3a landing
# orders and starts asserting as each package arrives.
#
# The environment is the sibling step's, deliberately and in full: these entries run the
# same DirectVulkan binary through the same runner, so the three MOBILEGL_MAGMA_* fixes it
# needs apply here too, and a crash here has to leave a core for the same black-box flow.
# The step above is the only reason those lines exist in this job; a control that crashed
# without one would be the hardest failure in the job to diagnose.
#
# THE -R ALTERNATIVES ARE TEST-NAME PREFIXES, NOT LANE LABELS, and each one is deliberately
# the SHORTEST string that still selects only what it means to. `ResourceSubsystem` (not
# `ResourceSubsystemControl`) is what reaches the eight
# DirectGLES.ResourceSubsystemOn./Off.LargeArenaAdoptionScenario.* entries - the A/B lanes
# whose entire purpose is that the handle path and the legacy BufferBackendOps path must
# agree about an adopted store - as well as the two ResourceSubsystemControl. entries.
# `MapPersistentRoundtrip` is singular because LargeArenaAdoptionScenario's case is
# `AnAdoptionCostsExactlyOneMapPersistentRoundtrip`; the plural matched only the lane PREFIX
# of the other one. Both mistakes were silent: this is the only CI job that unpacks a push
# build, so an entry the filter misses is either never run under the P3a bits at all or runs
# only in the pull `integration` job, where a MOBILEGL_PIPE_PUSH value steers nothing
# (Config.h declares the field inside the push guard) and both arms are the same legacy path.
# A lane that cannot go red where it is installed is not a gate (ARCHITECTURE.md §13.2).
# P4a ADDS THREE ALTERNATIVES, and each one is here because this is the only CI job that
# unpacks a push build:
# * `ObjectSubsystem` reaches the three DirectGLES.ObjectSubsystemControl. entries - the
# 0x1fff-vs-0x1ff A/B and the 0x9ff dependency refusal (G12). `ResourceSubsystem` does
# NOT match it: the two families are named apart on purpose, because they are different
# phases' switches and a filter that merged them would hide one behind the other.
# * `TextureParamsWithoutASamplerView` reaches G9's four cases, the scenario the phase's acceptance gate
# names by hand. It runs in the ambient lanes, which the label already selects - but this
# step is where those cases run against a PUSH library, and G9's whole claim is about the
# push path. All four cases are green on the contract commit - including the one D-E3
# expected to be red, for the reason the scenario's header records - so this row is green
# from the day it lands and goes red only if a reachability path stops syncing texture
# parameters at all, which is the coupling ARCHITECTURE.md §3.2 exists to remove.
# * `TextureUploadShape` is RECORDED, not gated (D-D4): it asserts that the two upload-shape
# counters could be read and that they agree, and prints the shape for MEASUREMENTS.md. It
# is in the filter so that the number is actually collected on every run - an unmeasured
# shape is not a recorded one - and because its own assertions can go red.
# As with the four before them, each alternative is the SHORTEST string that selects only what
# it means to.
# Handle-ABA, CSO, subsystem and texture-parameter controls (G8, G8b, G9, G10, G12).
# Handle-ABA, CSO, subsystem-mask and texture-parameter controls, rerun on the verify library.
# They carry integration-gpu, not integration-verify, so the verify lane misses them.
# Each -R alternative is a test-name substring, the shortest that selects only its family:
# `ResourceSubsystem` also reaches the LargeArenaAdoption lanes and the mask refusals,
# `MapPersistentRoundtrip` is singular so it matches the case name as well as the lane prefix,
# and `ObjectSubsystem` and `ResourceSubsystem` stay distinct families. TextureUploadShape
# records the upload shape; it fails only when its two counters are unreadable or disagree.
- name: Push-build controls (G8, G8b, G9, G10, G12)
working-directory: build-verify
env:
@@ -1143,34 +1087,14 @@ jobs:
fi
echo "the corrupted field turned ${matched} selected split entries red, as it must"
# NEGATIVE CONTROL A ON THE OTHER COMPARATOR (P7 wave 3, V1). The step above proves the
# ENTRY compare - client thread, verb boundary, `where=entry`. The compare-at-read hook runs
# on the SERVER's apply thread at every backend read and is the only comparator that arm ever
# runs, and until MG_Impl/Pipe/PipeFill.cpp's hook honoured MOBILEGL_PIPE_VERIFY_CORRUPT it
# had no control at all: a hook that had stopped comparing was indistinguishable from a hook
# with nothing to report.
#
# NOT THE "ctest MUST FAIL" SHAPE, and the reason is mechanical rather than a preference:
# GetPixelStoreParameters is in kReadback's fill mask, so with MOBILEGL_PIPE_VERIFY_FATAL at
# its default the client's entry compare aborts the process at the ReadPixels boundary and
# the server never reaches the read. The entries therefore pin FATAL=0, survive their own
# divergence, and each asserts from inside the process that the SERVER role's half of its
# private log carries the report. This step re-reads those logs from outside, so the claim
# does not rest only on the assertion that produced them.
#
# AND IT COUNTS, NEEDING TWO (V1 fix round 2). The server reads the pack field twice inside
# the ReadPixels window - PipeApplier::read_pixels saves the application's pack before it
# installs the neutral one, the backend's ReadPixels reads it after - and the hook perturbs
# its oracle once per read, because the ID-49 overwrite inside the window wipes the first
# perturbation before the second read's compare. A step satisfied by ONE line was satisfied
# by either perturbation alone and falsified neither. Measured at landing: 3 (DirectGLES,
# whose backend reads the field twice after the install) / 2 (DirectVulkan); without the
# re-applied perturbation both drop to 1, without the first one DirectVulkan drops to 1
# while DirectGLES stays at 2 - so BOTH backends are required, and the first perturbation's
# falsifier is the DirectVulkan entry.
#
# THE GREEN HALF is the lane run above: the same hook, the same window, with no corruption
# armed, across 1070 entries at zero divergences.
# Negative control for the compare-at-read hook, which the server's apply thread runs at each
# backend read; the step above covers only the client's entry compare. The entries pin
# MOBILEGL_PIPE_VERIFY_FATAL=0, or the client's entry compare would abort at the ReadPixels
# boundary before the server reads. This step re-reads each server log and needs two
# `where=read` reports per backend: the server reads the pack field twice in the ReadPixels
# window (the saved pack, then the backend's read under the neutral pack), and each read must
# catch its own perturbation. PipeVerifyArmingScenario.CorruptedFieldIsReportedOnTheServerRead
# has the details, including why both backends are needed.
- name: Negative control A (server-side read)
working-directory: build-verify-split
env:
@@ -1408,26 +1332,13 @@ jobs:
fi
echo "libMobileGL.so defines ${remote} MG_Remote symbol(s) and the MGPipe applier (${defined} defined symbols)"
# The link-closure ratchet (CONTRACT-P6.md 12.1, ID-P7-7): the set of frontend symbols a
# server-only image would still have to link, recomputed from the per-object partition in
# scripts/link_ratchet.py and compared against the stored LIST.
#
# THIS job and not `build-linux`: build-linux passes the same three options from
# ${MOBILEGL_CI_DISAGGREGATED}, which is ON only on feat/disaggregated, cleanup and
# codex/fix-github-ci, so on every other branch it builds a monolith - MG_Remote would not
# be compiled at all, the SERVER half of the partition would be empty, and the gate would
# go green having measured nothing. This job turns the three options on unconditionally,
# and the nm step immediately above has just proved they took. The script re-reads them
# from CMakeCache.txt anyway and refuses the run if they are not all ON.
#
# Only NEW symbols fail. A symbol that has DISAPPEARED is the ratchet turning and is
# printed with an instruction to re-baseline in the same commit - the baseline stores the
# list rather than a count precisely so that clearing one symbol while adding another does
# not walk through at an unchanged total (ID-P7-7).
#
# It reads the per-object output of the MobileGL target, so it must run before anything
# that cleans the build tree; the packaging step below excludes *.o from the tarball but
# does not delete them.
# Server link-closure ratchet (ARCHITECTURE.md §13.2): scripts/link_ratchet.py recomputes
# from the per-object partition the frontend symbols a server-only image still has to link
# and compares the list with the stored baseline. Only new symbols fail; a vanished one is
# printed with a request to re-baseline in the same commit. The baseline is a list, not a
# count, so swapping one symbol for another fails. It runs here, not in build-linux, because
# this job always builds MG_Remote (the script re-checks CMakeCache.txt), and before anything
# deletes the *.o files it reads.
- name: Server link-closure ratchet
run: |
mkdir -p ci-artifacts
@@ -1567,23 +1478,11 @@ jobs:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml"
# P7 wave 2-F, PH-7 (1). THE TCP SUPERVISOR LANE NEEDS flatc, AND CI HAD NONE.
#
# `TcpLane.SupervisorProtocolControls` carries LABELS integration-tcp, so it has been in the
# selection below since wave 0 - as a SKIP, every run. The script builds the protocol's
# Python bindings with flatc, flatc is deliberately absent from the build graph
# (gen_protocol.py's header says why), and with none found it exits 77 and ctest reports
# the entry skipped. Skipped is what the entry looked like for the whole time it did not
# exist, which is the failure mode the plan's own row named ("referenced by NOTHING").
#
# So the job builds the SAME pinned flatc that flatc-check builds, from the same submodule,
# and the lane step below names the entry to junit_tally.py --require-entry-passed. If the
# build ever stops producing one, the lane reds by name rather than going green having run
# the supervisor's Busy / token / listen / version / build-policy / reap controls zero times.
# --check is passed so this step cannot quietly regenerate a different committed header;
# keeping that honest is flatc-check's job and this step must not overlap with it.
# P7 F2: `TcpLane.ControlFrameFuzz` (Ph fuzz arm 1, scripts/ci/ph_fuzz_control_frames.py)
# needs the same flatc and is required PASSED by name on the same junit_tally line.
# TcpLane.SupervisorProtocolControls and TcpLane.ControlFrameFuzz build the protocol's Python
# bindings with flatc, and without one they exit 77 and ctest reports a skip. flatc is not in
# the build graph (see gen_protocol.py), so this builds the same pinned flatc as flatc-check,
# and the lane step requires both entries PASSED by name through junit_tally.py. --check keeps
# this step from regenerating the committed header; checking that header is flatc-check's job.
- name: Check out FlatBuffers (TCP lane)
run: git submodule update --init 3rdparty/flatbuffers
@@ -1692,36 +1591,12 @@ jobs:
fi
echo "the split lane resolved MOBILEGL_TRANSPORT=inproc"
# EXIT GATE E1's NEGATIVE CONTROL and EXIT GATE E3(a)'s, in one step because they have the
# same two-state shape and the same reason for it.
#
# Both controls turn a knob that MUST make a split scenario red: MOBILEGL_IPC_VERB_BARRIER=0
# removes the lockstep fence R-1 rests on, and MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 turns the
# persistent-map push off. A control of the verify lane's shape - "this step passes when
# ctest FAILS" - cannot be written yet, because while packages c1/s1/v1 are landing the
# Split entries SKIP and ctest reports green whatever the knob says, so an unconditional
# control would be red for the whole of P5 for a reason that is not a defect.
#
# So the expected state is DERIVED FROM BEHAVIOUR rather than assumed. The first version read
# MGITEST_REMOTE_CLIENT_PRESENT out of the generated *_tests.cmake, which was a restatement of
# the CMake source probe review finding M-1 falsified; the arming condition is a runtime fact
# inside each test process (MG_Config::Transport, ClientSession::Active() and
# ImplementedVerbCount(), read by Harness/SplitRuntimePeek), so the only honest way to ask it
# from a shell is to look at what the entries DID. When entries passed, the controls MUST
# fire; when every one of them skipped, the step says so loudly and does not pretend.
#
# THE BODY OF THIS STEP IS scripts/ci/split_negative_controls.sh, and the move is the point
# rather than tidiness. A `run:` block executes nowhere but on a runner, so these lines were
# unreviewable and untestable: when the wave-1 cross-family review said they were broken,
# CONFIRMING it needed a hand-made copy of them (wave1-codex-verify.md 8), and a copy is not
# the thing. scripts/ci/control_smoke_test.sh now drives the very file this step runs.
#
# What that smoke test pins, and what ID-46 finding 8 found missing: each control asserts its
# OWN failure reason. A non-zero ctest exit used to be enough, so a timeout, a setup abort or
# any unrelated assertion printed "turned N selected entries red, as it must" and this step
# went green. The arming run's `|| true` had the matching defect - it counted a case that ran
# and FAILED as evidence the lane was live, so the controls could be measured against a
# baseline that was already red.
# Negative controls for the verb barrier (MOBILEGL_IPC_VERB_BARRIER=0) and the persistent-map
# push (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0): each knob must turn its selected entries red.
# The body is scripts/ci/split_negative_controls.sh, so scripts/ci/control_smoke_test.sh can
# run the same lines off a runner. It needs a green baseline with at least one passed entry,
# and accepts a red only when it carries the control's own diagnostic, so a timeout, a setup
# abort or an unrelated failure never counts.
- name: Negative controls (barrier, map push)
if: ${{ !cancelled() }}
working-directory: build-split
@@ -1821,23 +1696,14 @@ jobs:
MOBILEGL_IPC_STRICT_ERRORS: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-strict.xml"
# ---- P5e (ra), MG_Remote/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
# ---- Strict split lane: residual pulls under MOBILEGL_IPC_STRICT_ERRORS=1 ----
#
# This lane was written as an EXPECTED-RED control: under strict every integration-split
# entry aborts on its first BARRIER-PULLED read, so the step gated on the run failing and
# on the strict marker being the reason. P5e turns it into a HARD GREEN GATE - once the
# client runs ahead, an unbarriered record's pull is a Fatal whatever this knob says
# (§3.3), so what strict adds is only the BARRIERED rows' pulls, and those are a short,
# named list. A revert of any handle arm in vi/sb/pg/tx2/fb then goes red HERE, by field
# and verb, instead of in a picture nobody diffs.
#
# THE ARM IS CHOSEN BY THE TREE, NOT BY A SECOND EDIT. kMGPipeP5eRunAheadReady in
# MG_Backend/Init.cpp is the one constant the integration commit flips, and it is exactly
# the fact this lane's expectation depends on: with it false no server publishes
# kCapRunAheadApply, every record is barriered, every scenario still aborts on its first
# pull and the expected-red control is the honest gate. Reading it here means the lane
# changes meaning in the same commit the runtime does - a lane whose flip is a separate
# human step is a lane that is wrong for as long as that step is outstanding.
# An unbarriered pull is always fatal, and strict also stops any barriered pull that is not
# admitted (ARCHITECTURE.md §9.2.1). The arm comes from kMGPipeP5eRunAheadReady in
# MG_Backend/Init.cpp, so the expectation changes in the commit that changes the runtime. When
# true (run-ahead), the lane must be green and its markers must match the admitted list and
# strict-expected-markers.txt; when false, every record is barriered, the lane must be red
# (strict aborts every drawing entry), and only the marker census is checked.
- name: Strict split lane
working-directory: build-split
env:
@@ -1912,64 +1778,23 @@ jobs:
exit 1
fi
# THE ADMITTED MARKERS COME FROM THE GENERATOR, NOT FROM THIS FILE (ID-116).
#
# A hand-kept copy of §7 lived here for one round and was wrong in BOTH directions at
# once: it omitted GetFramebufferBindingSlot@ReadPixels - 21 entries, the largest
# survivor in the lane - and it carried GetTextureObject@CopyImageSubData, which
# nothing made safe, because resource_copy_region is kWaitNone. Neither error is
# visible by reading the list; both are obvious the moment the list is DERIVED.
#
# The rule is ID-84 said in tables that already exist (ID-116, refined by ID-125): a
# <field>@<verb> pull is admitted iff the field's ownership row is BARRIER_PULLED, the
# verb has a stamp row, the field is inside the verb class's may-read mask, and EITHER
# the verb's wire op is statically barriered (kMGPipeWaitClasses != kWaitNone, i.e. the
# client really is parked behind the record) OR the field's retiring phase does not
# name P5e. The second disjunct is the honest reading of ID-84: this comparison only
# ever sees pulls on records the server stamped BARRIERED - an unbarriered one aborts
# before any of this - so the pull is legal by construction and the only question left
# is whether THIS phase still owes the migration. Without it the lane would go red on
# GetTransformFeedbackProgram@DrawArrays, a row CONTRACT-P5E §5.7 rules out of P5e
# entirely and no package here is allowed to touch.
# `--print-admitted` prints exactly the table MGPipeBarrierPullAdmitted() answers
# from, so this shell and the C++ cannot drift: retiring a verb's wait class narrows
# the lane with no edit here, and a new BARRIER_PULLED row widens it only on verbs
# that wait.
# THE RATCHET, BOTH SIDES (ID-119). `no-fatal` is what makes this arm the hard gate:
# - any Fatal marker fails;
# - an ADMITTED marker outside the generated allowlist fails, which can only mean the
# committed PipeFieldOwnership.inc and the generator disagree;
# - a pair that appears and is not in strict-expected-markers.txt fails: it is a debt
# somebody now owes and it gets written down with the phase that retires it;
# - AND a pair in that file that no longer appears fails, so the lane CANNOT ROT
# GREEN. Without that half, a retired debt leaves its row behind as a permanent
# licence and the next regression to re-introduce the read reads as "expected".
# ADMITTED-ESCALATED markers are checked against the expected set but NOT against the
# allowlist: they are a runtime fact about the record's payload (an open XFB span, a
# draw carrying client vertex arrays) that no table indexed by (field, verb) can carry,
# and widening the static list to hold them would forgive the ordinary draw path too.
# The admitted pairs come from gen_pipe_field_ownership.py --print-admitted, the table
# MGPipeBarrierPullAdmitted() reads, so this list and the C++ cannot drift. A pair is
# admitted when its verb has a stamp row, the field is BARRIER_PULLED and in the verb's
# may-read mask, and either every wire op stamping the verb waits or the field's retiring
# phase is not the generator's THIS_PHASE. The census fails on an admitted marker outside
# that list and on any difference from strict-expected-markers.txt in either direction;
# `no-fatal` adds any Fatal marker. Escalated markers depend on the record's payload (an
# open XFB span, client vertex arrays), so they are checked only against the expected set.
python3 "${helper}" markers "${RUNNER_TEMP}/lane.json" '.' \
"${RUNNER_TEMP}/allowed.txt" "${expected}" no-fatal
# `rsp`, AND THE PIN THAT REPLACED A VACUOUS ONE (ID-119). What stood here was a grep
# for `rsp=` over split-logs/ - a counter no entry in that directory emits, because
# PipeStats is off unless a lane asks for it - testing a sentence that was true of every
# possible implementation: CountBarrierPull's unbarriered arm is [[noreturn]] and runs
# BEFORE ++g_residualPulls, so an unbarriered pull can never reach the counter whatever
# the code does. It has been replaced by an assertion inside a lane entry that actually
# counts: DirectGLES.Split.StrictArming., which runs with MOBILEGL_PIPE_STATS=1,
# MOBILEGL_PIPE_STATS_PERIOD=1 and a private log path of its own (a whole-lane env flip
# races under -j, since the library opens that path "w"), and asserts
# vbs > 0 - the server stamped a verb boundary on a DRAWING inproc entry, which is
# ID-115's positive control: everything strict checks is downstream of
# that stamp, and before it existed "the lane is green" and "strict was
# never armed" were the same observation;
# rsp == 0 or rsp >= draws - the checkable shape of the debt. While the draw path's
# pull is unretired every draw pulls, so rsp scales with draws; the
# retirement is visible as rsp ceasing to scale. That is what the device
# measured (rsp ~= the draw count under inproc, 0 under monolith).
# So the assertion lives where the counter is, and this step only has to insist the
# entry RAN - a positive control that can be skipped away is not one.
# Arming proof: DirectGLES.Split.StrictArming. runs with MOBILEGL_PIPE_STATS=1 and a
# private log and asserts, over one drawing frame, that the server stamped a verb
# boundary (vbs > 0) and that no residual pull happened (rsp == 0). Only that entry
# enables PipeStats, so the counters are checked there, not grepped from these logs; rsp
# counts only barriered pulls, as an unbarriered one aborts before CountBarrierPull.
# This step only requires that the entry ran: a skipped positive control proves nothing.
if ! grep -q 'DirectGLES\.Split\.StrictArming\.' "${RUNNER_TEMP}/strict-split.txt"; then
echo "::error::the strict lane did not run DirectGLES.Split.StrictArming., so nothing in this run proves the server ever stamped a verb boundary. A green lane without it is indistinguishable from a lane where strict was never armed (ID-115)."
exit 1
@@ -2091,26 +1916,12 @@ jobs:
run: |
ctest -L integration-magma-split --no-tests=error -j 2 --output-on-failure
# ---- P7 PACKAGE L: THE SAME MAGMA CASES IN TWO PROCESSES -----------------------------
#
# EXIT GATE 1's FIRST HALF, and until this package there was nothing to run: every arm of
# the split registration macro hardcoded MOBILEGL_BACKEND_TYPE=DirectGLES, so the shared
# `integration-split` / `-spawn` / `-tcp` labels held ZERO DirectVulkan entries and
# `integration-magma-split` was inproc by construction. These two lanes are the same
# scenarios the step above runs, with the transport as their one difference - so a red
# here that the step above did not see is a TWO-PROCESS fact and not a Magma one.
#
# THEY ARE GATED BECAUSE THEY MEASURED GREEN, and only for that reason: 52 of 52 entries
# on each arm, with the two ClipDistance cases skipping for the reason the inproc arm's
# allowlist already names. The 21 entries the inproc arm has and these do not are
# MagmaRunAheadScenario and MagmaWireCacheScenario, which are inproc-only BY
# CONSTRUCTION (they hold and peek THIS process's server loop); the CMakeLists and
# spawn_lane_parity.py both name them rather than widening a tolerance.
#
# THE INFORMATIONAL TIER - `integration-magma-all-{split,spawn,tcp}`, every scenario the
# split arms run, on Magma - IS DELIBERATELY NOT HERE. Its job is to be able to go red
# while CI stays green; a case moves into a gating step with the commit that retires its
# blocker.
# The Magma scenarios of the step above over the spawn and TCP transports, so a failure here
# that the inproc step did not show is a two-process fact, not a Magma one. Entries that can
# run only inproc (such as those that peek this process's server loop) are listed in
# spawn_lane_parity.py's MAGMA_INPROC_ONLY. The informational tier,
# integration-magma-all-{split,spawn,tcp}, is not gated: it may go red while CI stays green,
# and a case moves into a gating step once its blocker is fixed.
- name: Magma spawn and TCP lanes
working-directory: build-split
env:
@@ -3190,29 +3001,13 @@ jobs:
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
if-no-files-found: warn
# The retrace arm of the split shape: retrace-verify with MOBILEGL_PIPE_VERIFY=1 swapped for
# MOBILEGL_TRANSPORT and the comparator's symbol swapped for an MG_Remote one. Exit gate E2 is
# one row - OpenRA at SSIM >= 0.99 - but the lane is no longer one row: as of P6 it is
# {backend, case, transport} over the whole CI matrix, both backends, inproc AND spawn.
#
# inproc IS THE CONTROL AND SPAWN IS THE CLAIM. Same library, same codec, same applier, same
# picture; the only difference is which PROCESS the server role runs in. Running only spawn
# would leave "did the transport break it?" and "did the phase break it?" answerable only
# together, which is the question every failure in this phase turned out to hinge on.
#
# IT RUNS THE UNCHANGED CTEST NAMES with the transport exported in the JOB environment, rather
# than the SPLIT-suffixed variant entries. build-retrace configures without
# -DMOBILEGL_BUILD_DISAGGREGATED, so the variant entries are deliberately not registered there
# (a name that says SPLIT in a build that cannot be one is worse than no name), and the
# unchanged names are what retrace-verify already proves this shape works with. The variant
# entries exist for a build that DOES configure the option - a local build-split with trace
# replay on - where `ctest -L retrace-split` is self-describing and needs no environment ritual.
#
# WHAT MAKES IT FALSIFIABLE is not the SSIM. A monolith run of OpenRA also scores 1.000: measured
# on this branch, a pull library under MOBILEGL_TRANSPORT=inproc produced ssim=1.0 and was caught
# only by run_trace_case.cmake's transport-resolution assertion. So there are two guards, and the
# SSIM is neither of them: the nm check below (this library carries MG_Remote) and the
# transport-resolution line in the library's own log (this PROCESS resolved inproc).
# Trace retrace on the split library: the retrace-verify shape with MOBILEGL_TRANSPORT set
# instead of MOBILEGL_PIPE_VERIFY, over {backend, case, transport} with inproc and spawn.
# inproc is the control and spawn the claim: same library and picture, only the server role's
# process differs, so a spawn-only failure is a transport fact. It runs the unchanged ctest
# names with MOBILEGL_TRANSPORT exported around ctest, since build-retrace does not register the
# split variant entries. SSIM cannot catch a monolith run (OpenRA scores 1.000 either way); the
# guards are the nm check for MG_Remote and run_trace_case.cmake's transport-resolution check.
retrace-split:
name: retrace split (${{ matrix.backend }} / ${{ matrix.transport }}, ${{ matrix.case }})
runs-on: ubuntu-latest
@@ -3400,39 +3195,13 @@ jobs:
ctest -V --no-tests=error --timeout 10800 \
-R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
# THE RETRACE LANE'S OWN ALWAYS-ON NEGATIVE CONTROL, and its absence was review finding M-6:
# this job was a clone of retrace-verify with the one step removed that makes the lane mean
# anything. retrace-verify's "Negative control (corrupted snapshot)" step requires a corrupted
# snapshot field to red the retrace, so that "79 traces, zero divergences" is not a statement
# about a comparator nobody watched; the same reasoning applies here word for word.
#
# The control swaps the PULL library into the frozen path and requires the SAME replay to
# fail. It is the sharpest one available, because OpenRA scores ssim 1.000000 either way -
# measured - so this step fails if and only if run_trace_case.cmake's transport-resolution
# assertion has stopped working, which is the single thing standing between this job and a
# green that ran monolith end to end.
#
# NOT the control BRIEF 7 E2 names ("patch the Clear emitter to drop one emission; SSIM must
# fall below threshold"). That one needs an emitter, i.e. package c1, and it is carried as an
# explicit debt in t1-v1.md rather than silently substituted - which is what the first
# version of this package did.
#
# The rerun replays into the same case directory, so the good run's images are put aside and
# restored whichever way the control goes; "Upload actual image" below runs `if: always()`
# and would otherwise ship the deliberately-wrong run's output under the good run's name.
# THE BODY OF THIS STEP IS scripts/ci/retrace_transport_control.sh, for the reason the
# split lane's control gives: a `run:` block cannot be executed off a runner, so these lines
# could not be tested until they ran in CI. scripts/ci/control_smoke_test.sh drives that file.
#
# Two holes ID-46 finding 8(b) found in this block, both CONFIRMED against the REAL ctest in a
# REAL build tree, both closed in the script: it had NO selection guard at all - unlike the
# split lane's run_control - so a case/backend regex matching nothing exited 8 through
# `--no-tests=error` and was read as "the pull library turned it red"; and only "non-zero
# ctest" was checked after the nm identity check, so a loader failure, a missing fixture or a
# timeout passed it. The red must now carry run_trace_case.cmake's own sentence.
# P13 W2: THE LIBRARY IS THE PUSH MONOLITH WITHOUT MG_Remote (the shape FCL embeds); until W2 it
# was the pull build, run side by side with this one for a wave (W1) to show both red here for
# the same sentence. What makes the library red is that it has no transport, not that it pulls.
# Negative control: copy a library without MG_Remote (the push monolith FCL embeds) over the
# frozen path and require the same replay to fail. OpenRA scores ssim 1.000 either way, so
# the picture cannot catch the swap; this proves that run_trace_case.cmake's
# transport-resolution check still does. The body is scripts/ci/retrace_transport_control.sh,
# which control_smoke_test.sh runs off a runner. It counts the selection first, requires the
# red to carry run_trace_case.cmake's own sentence, and restores the split library and the
# good run's images, since "Upload actual image" runs `if: always()`.
- name: Negative control (library without a transport)
working-directory: build-retrace/tools/trace_replay
env:
@@ -3447,23 +3216,12 @@ jobs:
bash "${GITHUB_WORKSPACE}/scripts/ci/retrace_transport_control.sh"
'${{ matrix.case }}' '${{ matrix.backend }}'
# EXIT GATE E2's OTHER HALF, and the one BRIEF-P5 §7 E2 actually names: "drop an emission
# and the SSIM must fall below the threshold". The control above proves the lane runs a
# SPLIT library; it cannot prove the PICTURE came through the wire, because OpenRA scores
# ssim 1.000000 against a monolith library too and the transport assertion is what reds it.
#
# It drops the DRAWS and not the clears, and that is a measurement rather than a
# preference: with MOBILEGL_IPC_E2_DROP_CLEAR=1 armed, read, and all 29 of OpenRA's Clear
# records dropped, the retrace still scored ssim=1.000000 / mismatchPixels=0 - OpenRA
# overdraws every pixel it clears before the snapshot (joint-v1.md §3 found the green;
# scripts/ci/retrace_drop_draw_control.sh's header has the trace census that explains it).
# With the DrawVbo records dropped instead: 758 dropped, ssim=0.000036,
# mismatchPixels=295296.
#
# THE BODY IS A SCRIPT for the reason the two steps above give: a `run:` block cannot be
# executed off a runner, and scripts/ci/control_smoke_test.sh drives this file against a
# stubbed ctest in seven modes - including the two that matter most, a red whose SSIM never
# fell and a knob that armed and dropped nothing.
# Negative control for the picture: the step above cannot show that the image came through
# the wire, because OpenRA also scores ssim 1.000 on a monolith library. This drops the
# DrawVbo records and requires the SSIM to fall. Dropping clears does not work: OpenRA
# overdraws every pixel it clears (retrace_drop_draw_control.sh's header has the census).
# The body is a script so control_smoke_test.sh can run it against a stubbed ctest, including
# a red whose SSIM never fell and a knob that armed but dropped nothing.
- name: Negative control (dropped draws)
working-directory: build-retrace/tools/trace_replay
env:
@@ -3682,66 +3440,32 @@ jobs:
fi
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
# A GATE as of P2, which is when MG_Pipe/DirtySurface.def exists to diff the scan against
# (the first mapping round was part of that phase's deliverable). --check fails BOTH directions: a
# mutator the scanner finds with no row in the def, and a row naming a mutator the scan no
# longer finds - so a deleted mutator cannot leave a stale row behind claiming coverage.
#
# --self-test is the half that keeps --check honest, and it is not optional. A completeness
# check that silently stopped checking produces exactly the same green as a complete
# mapping; the self-test feeds it two canned negative controls (a mutator withheld from the
# def, a row naming a function that does not exist) and fails if either fails to trip. Same
# shape as gen_pipe.py --self-test and check_include_closure.py above.
#
# What this gate does NOT cover is written into DirtySurface.def's header rather than left
# implicit: the scanner attributes a mutation inside a lambda to the enclosing function,
# reads a mutation published through a helper as deferred, and scans only MG_Impl/GLImpl -
# so the four MGP_NOTE_MUTATION sites in MG_State are outside it entirely. This is a
# completeness gate over what the scanner can see; the semantic proof is the verify lane.
# Dirty-surface completeness (ARCHITECTURE.md §5.2.1). --check fails in both directions: a
# mutator the scanner finds with no row in MG_Pipe/DirtySurface.def, and a row naming a
# mutator the scan no longer finds. --self-test feeds canned negative controls and fails if
# one does not trip, so a check that stopped checking cannot stay green. The scanner matches
# text; the semantic proof is the verify lane.
- name: Dirty-surface mapping complete (G9)
run: |
python3 scripts/gen_pipe_dirty_surface.py --check
python3 scripts/gen_pipe_dirty_surface.py --self-test
# A GATE as of P5 (R-7.1, CONTRACT-P5.md table 2). The eighth generator, and the only one
# whose output the step above cannot cover: `gen_pipe.py` does not write
# PipeFieldOwnership.inc, so regenerating the seven and diffing MG_Pipe/generated leaves a
# drifted ownership table green. The build-level static_assert only catches an
# UNCLASSIFIED row; a hand edit that CHANGES a class compiles clean, and --check is the
# only thing in the tree that catches it.
#
# NO BRANCH GUARD, deliberately, unlike the two G5 steps below: this asks "does the
# committed table still follow from the .def files", which is a question every branch can
# answer and every branch wants answered. The table outlives feat/disaggregated.
#
# --self-test is not optional, for the reason the two gates above give and for one this
# generator learned the hard way: its first version counted eleven trips when one control
# was a silent duplicate of another, because the harness asked "did something exit" rather
# than "did THIS exit". Every control now asserts its own message, and a harness control
# asserts that the harness still rejects someone else's exit.
# PipeFieldOwnership.inc is written by gen_pipe_field_ownership.py, not gen_pipe.py, so the
# gen_pipe.py regenerate-and-diff step above does not cover it. The build's static_assert
# catches only an unclassified row; --check also catches a changed class. No branch guard:
# every branch needs the committed table to follow from the .def files. Each --self-test
# control asserts its own message, and a harness control checks that one control's exit is
# never credited to another.
- name: Field-ownership table complete (R-7.1)
run: |
python3 scripts/gen_pipe_field_ownership.py --check
python3 scripts/gen_pipe_field_ownership.py --self-test
# R-16 APPLIED TO THE NEGATIVE CONTROLS THEMSELVES. The split lane's E1/E3(a) controls and the
# retrace lane's pull-library control are gates, and until ID-46 finding 8 neither could be
# made red by anyone: their bodies were `run:` blocks, which execute only on a runner. Both
# bodies now live in scripts/ci/, and this step runs them against a stubbed ctest that
# reproduces the finding - a NON-EMPTY selection failing with UNRELATED_CONTROL_FAILURE, and a
# case/backend regex matching no tests - and requires each control to report FAILED. The same
# stub, failing with the diagnostics the scenarios really emit, must make them report PASSED.
#
# NO BRANCH GUARD: this asks "do the negative controls still reject a red that is not theirs",
# which is a question every branch can answer and none of which depends on the TEMPORARY
# feat/disaggregated trigger at the top of this file. It costs a couple of seconds and needs
# no build.
# P7 adds the last two. THE DEVICE MATRIX DRIVER HAD NO CI STEP AT ALL: its 12 stub
# regressions (now 18, with the DirectVulkan half) were run by hand in WSL and recorded in
# a note, which is the same "a gate nobody can make red" shape the paragraph above is
# about - and it guards the tool that decides whether a device golden run PASSED.
# compare_actuals is the new half of that evidence (actual-vs-actual SSIM between
# repeats); both are stub/synthetic-only, need no build, no GPU and no device.
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2).
# control_smoke_test.sh runs the split and retrace control scripts against a stubbed ctest:
# an unrelated failure or an empty selection must make each report FAILED, and the
# scenarios' real diagnostics must make it report PASSED. The Python tests run the trace and
# TCP lane tooling on stubs and synthetic data. No branch guard; no build, GPU or device.
- name: Control and trace-tool smoke tests (R-16)
run: |
bash scripts/ci/control_smoke_test.sh
+3 -3
View File
@@ -600,7 +600,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
endif()
# P13 W5 (notes/p13/W5-DESIGN.md): the record arm is part of every library - the one without
# P13 W5 (docs/archive/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).
@@ -712,7 +712,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.cpp
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
# (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
# (docs/archive/notes/anland/plan-ahb-dmabuf.md).
MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
@@ -1009,7 +1009,7 @@ if (UNIX AND NOT APPLE)
endif()
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
# docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# docs/archive/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
# it through dlsym. Self-contained: nothing outside this block refers to either.
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
+3 -1
View File
@@ -70,6 +70,7 @@ Example:
- Generated code, only together with a CI step that regenerates it and fails on any difference.
- Binary test fixtures.
- Not raw logs, and not working papers such as agent briefs, review rounds and drafts. Keep those outside the repository and commit the conclusion: a design-document change, a `DEBTS.md` entry, a measurement under `docs/Performance`.
- `docs/archive/` keeps the planning notes and agent contracts of the disaggregation work as they were. It is frozen: no file there is added, edited or removed.
## Checks
@@ -78,7 +79,8 @@ Example:
These fail on any violation:
- commit messages, as described above;
- a new file under `docs/Disaggregated/notes/` or a new `.log` file;
- a new file under `docs/Disaggregated/notes/` or a new `.log` file (moving a file is not adding it);
- any change under `docs/archive/`;
- a missing or wrong license header;
- a text file that is not UTF-8;
- an external project name in a comment;
+3 -3
View File
@@ -657,7 +657,7 @@ namespace MobileGL::MG_Config {
// (WireTables.cpp), half of the stream link's 24-entry answer store.
// Measurements: docs/Performance/ipc-create-window.md.
Uint32 CreateWindow = 10;
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, docs/archive/contracts/CONTRACT-P9.md §1): 1 = a glReadPixels
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
@@ -665,7 +665,7 @@ namespace MobileGL::MG_Config {
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
// wait per read.
Uint32 PboReadbackSync = 0;
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
// MOBILEGL_IPC_POLL_ESCALATE (P10, docs/archive/contracts/CONTRACT-P10.md §1): a glClientWaitSync with a
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
@@ -697,7 +697,7 @@ namespace MobileGL::MG_Config {
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
// MOBILEGL_IPC_RUN_AHEAD (P5e, docs/archive/contracts/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
+1 -1
View File
@@ -616,7 +616,7 @@ namespace MobileGL::MG_ConfigLoader {
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// P5e (docs/archive/contracts/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
+1 -1
View File
@@ -163,7 +163,7 @@ namespace MobileGL {
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
// thread. And "the legacy pull path runs for it" is false under split - the client has
// no pull path to fall back to - so the records are simply lost, and only a later
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
// respecify's self-healing create (EmitResourceRespecify in TextureEmit.h) ever brings one back.
//
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
// MG_State::Init() no handshake is in flight, and the one that will land is started
+2 -2
View File
@@ -656,7 +656,7 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
@@ -756,7 +756,7 @@ namespace MobileGL {
(void)region;
return true;
}
// IMPLICIT SYNC BY FLUSH (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the session
// IMPLICIT SYNC BY FLUSH (docs/archive/notes/anland/plan-x11-gpu.md): the session
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
// its last boundary is published as written and read by one fence of everything it
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
@@ -3078,7 +3078,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Uint>(std::max(g_GLESCapabilities.MaxImageUnits, 0)));
// P4a e3 (D-G2, D-J2), CORRECTED BY THE REVIEW'S MAJOR-1.
//
// THE RECORD'S WINDOW IS NOT THIS SWEEP'S MEMBERSHIP. D-J2 and PipeApply.h:461-464
// THE RECORD'S WINDOW IS NOT THIS SWEEP'S MEMBERSHIP. D-J2 and PipeApply.h's MGPipeWorkingState
// are a rule about RECORD RETENTION - entries outside the var-tail window are not
// cleared in the applier - and say nothing about which DRIVER units a re-issue
// sweep has to visit. The membership this loop needs is "every unit the driver
@@ -3100,7 +3100,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Start is as undescribed as one above Start + Count).
//
// A8, which the review adds to this package's assumption list: NOTHING IN THE
// CONTRACT PINS THE WINDOW'S MEMBERSHIP. PipeApply.h:461-464 says only that the
// CONTRACT PINS THE WINDOW'S MEMBERSHIP. PipeApply.h's MGPipeWorkingState says only that the
// record is the last set as received, and that Start + Count above the bound is
// Fatal{ProtocolCorruption}. Until package C documents in ImageEmit.h that the
// window covers every unit the driver holds, this union is load-bearing; the
@@ -10391,7 +10391,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// format, not only the redundant-encoding ones this mirror serves, and marks the store
// driver-written where it cannot. The store is what answers a split glGetTexImage the
// driver refuses to read back (WireTextureReadback.inc's store route); every other split
// read of the destination reads the GPU level itself (notes/p8/E.md).
// read of the destination reads the GPU level itself (docs/archive/notes/p8/E.md).
//
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
@@ -10425,7 +10425,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
// P8-E (notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
// P8-E (docs/archive/notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
// so every byte the store holds for them is on the driver, and the copy below is a raw
// texel-block move - the store makes the same move, which keeps the destination level
// answerable from here when the driver will not read it back (ReadTextureImageWire's store
@@ -12668,7 +12668,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on
// it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a
// D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil
// (notes/p8/SE.md). Inproc only passed because client and server share the process.
// (docs/archive/notes/p8/SE.md). Inproc only passed because client and server share the process.
if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return;
@@ -14659,7 +14659,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (DirectGLES.h; docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) ------
// ---- SHARED IMAGES (DirectGLES.h; docs/archive/notes/anland/plan-ahb-dmabuf.md) ------
namespace SharedImageImpl {
#if defined(__ANDROID__)
namespace {
+1 -1
View File
@@ -515,7 +515,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace RenderStateImpl
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) ----
// ---- SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md) ----
//
// A shared image reaches the driver only through the Android import path: its AHardwareBuffer
// becomes an EGLImage (EGL_ANDROID_get_native_client_buffer + EGL_ANDROID_image_native_buffer)
+11 -11
View File
@@ -2720,7 +2720,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target);
const IntVec3 extent = MG_Record::StagedTextureUploadExtent(stored->Desc, record);
const SizeT runBytes = static_cast<SizeT>(record.Blob.Size);
// P8-E (notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW
// P8-E (docs/archive/notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW
// (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it
// did not write, so only the texels it names land - its regions, or its union box
// when it carries none (D-D3's "the box is the whole story"). A run the store cannot
@@ -4555,7 +4555,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return PipeRecordAt(MG_Pipe::MGPipeApplier().ShaderCsos, cso);
}
// N-7: D KEYS ON THE UPLOAD HALF; WIRE (AccumulatePendingUpload, PipeApply.cpp:814) KEYS ON
// N-7: D KEYS ON THE UPLOAD HALF; WIRE (AccumulatePendingUpload, PipeApply.cpp) KEYS ON
// THE WHOLE PACKED FIELD. That asymmetry is resolved HERE, in D's favour and with the
// reason, rather than by widening the two comparisons: D's question is per (uploadTarget,
// level) because that is the granularity its upload loop iterates - it is handed a
@@ -4567,10 +4567,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// So the low byte is CROSS-CHECKED instead of ignored. The comparand is the resource's own
// descriptor Target, which B fills from exactly the expression the sub-data's low byte
// comes from (TextureEmit.h:223 / :517 against :1037), so the two are carried and equal by
// construction and any disagreement is a seam defect, not a shape to learn. This is also
// MGPResourceDesc::Target's first reader in this package (esprytobj-v2.md §2(6) recorded it
// as having none).
// comes from (TextureEmit.h's MGPipeBuildTextureResourceDesc and EmitResourceCreate against
// EmitOneLevel), so the two are carried and equal by construction and any disagreement is a
// seam defect, not a shape to learn. This is also MGPResourceDesc::Target's first reader in
// this package (esprytobj-v2.md §2(6) recorded it as having none).
static void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget,
Uint16 level) {
const Uint8 carried = MG_Pipe::MGPipeSubDataResourceTargetOf(packedTarget);
@@ -4932,8 +4932,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// THE KEY IS THE ATTRIBUTE INDEX, NOT MGPVertexAttribWire::BindingIndex, and the two
// live in different spaces. Espryt consumes RESOLVED attributes, so the client emits
// set_vertex_buffers as one entry per attribute slot with
// MGPVertexBuffer::BindingIndex == the attribute index (VertexInputEmit.h:190-193,
// :229 - "Espryt consumes RESOLVED attributes, so the set is one entry per attribute
// MGPVertexBuffer::BindingIndex == the attribute index (EmitVertexBuffers in VertexInputEmit.h -
// "Espryt consumes RESOLVED attributes, so the set is one entry per attribute
// slot with BindingIndex == the attribute index"), each carrying that attribute's
// already-folded buffer, stride and divisor. MGPVertexAttribWire::BindingIndex is the
// OTHER thing: the GL binding POINT the attribute was attached to by
@@ -5769,7 +5769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_forceTextureParamsResync = true;
return;
}
// P9 W2 (notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
// P9 W2 (docs/archive/notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
// generated mips, image stores) and the staged store holds what the client uploaded;
// the client is never asked, so no pull protocol exists. Reading only the store would
@@ -7174,7 +7174,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
: (obj)->GetStorageType())
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). The client defined
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). The client defined
// level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor
// still says so, but the texels are the image's, so nothing below may allocate or upload.
Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) {
@@ -14009,7 +14009,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// =====================================================================================
// P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES
// (MG_Remote/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature
// (docs/archive/contracts/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature
// in c0e so both sides compile from day one")
// =====================================================================================
//
+8 -8
View File
@@ -1557,7 +1557,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// applier stored, so the answer is the same whichever side resolved it first.
Bool BackendUsesNativeBaseInstance();
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2; declared by c0e, bodied by id/vi) ----
// ---- P5e SEAM (docs/archive/contracts/CONTRACT-P5E.md §4.2; declared by c0e, bodied by id/vi) ----
//
// THE VAO TWIN, RESOLVED FROM THE HANDLE THE RECORD CARRIED - MGPipeApplierState::
// BoundVertexElements - instead of from the frontend VertexArrayObject the draw's
@@ -1733,7 +1733,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// flag, allocator or pixel-store object participates. P9 W2: a level
// the driver does not hold (defined after the last sync) is never read
// back, and a readback the driver refuses falls back to the staged
// level when the store covers it (notes/p9/W2-REMINT.md).
// level when the store covers it (docs/archive/notes/p9/W2-REMINT.md).
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -2057,7 +2057,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// table's answer is an array element and only a GetOrCreate that GROWS the table moves
// it, which this function has already done by the time it returns.
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res);
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// ---- P5e SEAM (docs/archive/contracts/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// id/tx2) --------------------------------------------------------------------------
//
// EVERY DRAW-PATH TEXTURE ENTRY, BY HANDLE. The caller holds a Texture handle - a
@@ -2084,7 +2084,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glCopyImageSubData passes: it tries FollowCopyImageInStagedStore first and marks the store
// itself when that fails.
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture, Bool storeFollowsTheWrite = false);
// P8-E (notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
// P8-E (docs/archive/notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
// store too (StagedTextureStore::FollowCopy), so the destination level's bytes stay its
// content. Box in each image's own texel grid. False when the store cannot follow: a cube
// map or 1D array endpoint (copy-image puts faces / layers on z, the store does not), a view,
@@ -2143,7 +2143,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
~BackendFramebufferObject();
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
// P5e SEAM (MG_Remote/CONTRACT-P5E.md §5.4; declared by c0e, bodied by fb): the same
// P5e SEAM (docs/archive/contracts/CONTRACT-P5E.md §5.4; declared by c0e, bodied by fb): the same
// sync keyed on the framebuffer HANDLE. The record's eleven surfaces ARE the point
// set - the emitter refuses a point at or above the wire width - so the attachment
// walk needs no frontend FramebufferObject and no m_pushedSyncHandle handshake: the
@@ -3143,7 +3143,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src);
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.5; declared by c0e, bodied by
// ---- P5e SEAM (docs/archive/contracts/CONTRACT-P5E.md §4.2, §5.5; declared by c0e, bodied by
// id/pg) ---------------------------------------------------------------------------
//
// THE PROGRAM TWIN, RESOLVED FROM MGPipeApplierState::DrawProgram / DispatchProgram /
@@ -3239,9 +3239,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// BoundSamplerStates[unit])` (the record arm of BindCurrentUnitSamplers, E's S4) could
// never find a twin, the record arm bound nothing on every draw, and every glBindSampler
// reached the driver only through the pre-handle program pass - S-1's confusion one
// loop over, and exactly what SamplerEmit.h:201-205 forbids ("a backend must NOT key a
// loop over, and exactly what ARCHITECTURE.md §2.3.1 forbids (a backend must NOT key a
// sampler twin on a SamplerObject's lifetime id; the twin's life is
// create_sampler_state -> delete_sampler_state").
// create_sampler_state -> delete_sampler_state).
//
// This is the twin keyed the way the record is: resolved-or-created AT THE CSO HANDLE
// (GetOrCreateByHandle, the same slot table, a slot the identity family can never hold)
+1 -1
View File
@@ -540,7 +540,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GetOrCreate from a twin destructor cannot resurrect it.
//
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
// kind the CLIENT frees it after the destroy call returns (MGPipeSlotAllocator::Free - the
// Gen bump rides the next handout, so a double free cannot skip a generation). An
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
// left alone: the successor's own GetOrCreate resets it.
@@ -327,7 +327,7 @@
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// P8-E (docs/archive/notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
@@ -73,7 +73,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (MG_Remote::Client::EmitObjectDeathRecord, CONTRACT-P5C §5.2).
//
// WHY THAT MATTERS, measured rather than argued (package L's probe,
// notes/p7/magma-two-process-first-run.md §6): under SPAWN the client process has no
// docs/archive/notes/p7/magma-two-process-first-run.md §6): under SPAWN the client process has no
// DirectVulkan backend at all, so InstallClientWireTables' own emitter already
// answered the notice and CtWireScenario's two death cases PASSED. Under INPROC the
// server role's backend is a thread of this process, Transport is Split rather than
@@ -247,7 +247,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// SPIRV-Reflect rebuild and `ClearProgramResourceCaches`, verbatim; only this build drops
// them - and with them the rehash hazard the ordering comment below the block-binding
// setter documents. Retiring the pull half is the day the member stops needing its guard,
// which is a G1/P13 decision and not a wave-2 one; recorded in notes/p7/magma-c.md.
// which is a G1/P13 decision and not a wave-2 one; recorded in docs/archive/notes/p7/magma-c.md.
void ClearProgramResourceCaches() {
// Nothing to clear: there is no cache in this build. Kept as an entry point so
// BackendObject_DirectVulkan's two EGL-teardown call sites stay statement for
@@ -811,7 +811,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// is not the answer either: the transient arena carries no
// {UNIFORM,STORAGE}_TEXEL_BUFFER usage (VkBufferManager::InitializeTransientArenas),
// so no VkBufferView can be made over it, and widening a shared per-frame
// allocation to buy an unreachable path is the wrong trade. See notes/p7/magma-a.md.
// allocation to buy an unreachable path is the wrong trade. See docs/archive/notes/p7/magma-a.md.
return writable ? false : placeholder();
}
VkBufferViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO};
@@ -942,7 +942,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.inc,
// which belong to wave-2 packages B and C, not to this file. Losing the
// dispatch with a named line in the role log beats returning stale counters
// with no error at all. notes/p7/magma-a.md carries the recipe.
// with no error at all. docs/archive/notes/p7/magma-a.md carries the recipe.
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' is bound at offset %llu, which is not a "
"multiple of this device's minStorageBufferOffsetAlignment (%llu); %s ranges cannot be "
"expressed as a descriptor and the shader's writes would be dropped by a read-only "
@@ -1114,7 +1114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, and that is a multiple of four on every
// device - but GL 4.6 core 6.1.1 puts no such rule on the SIZE, so `copied` is free
// to end mid-word and the Redmi Minecraft run lands there
// (notes/p5f/magma-inproc-fix.md #5). An unaligned start is not reachable through
// (docs/archive/notes/p5f/magma-inproc-fix.md #5). An unaligned start is not reachable through
// the public API and only a protocol-level offset can produce one; the sub-word arm
// handles it anyway rather than leaving a second shape to trip over later. Both arms
// copy the same bytes to the same place: the sub-word one pays one extra transient
@@ -115,7 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// attribute whose base offsets cannot be added is one Vulkan cannot fetch, the
// monolith arm never computes that sum at all, and masking is strictly safer than
// a Fatal for a number no application can reach on purpose. Recorded in
// notes/p7/magma-c.md rather than silently reconciled.
// docs/archive/notes/p7/magma-c.md rather than silently reconciled.
const auto describe = [&](const char* reason, VkFormat format) {
MGLOG_E_ONCE("Magma wire vertex layout: %s location=%u type=%u size=%u normalized=%u integer=%u long=%u bgra=%u stride=%d offset=%llu format=%d bufferWindow=%u+%u activeMask=0x%x",
reason, location, attr.Type, static_cast<Uint32>(attr.Size), static_cast<Uint32>(attr.Normalized),
@@ -593,7 +593,7 @@ public:
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): a server shared image's
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md): a server shared image's
// AHardwareBuffer bound as a single-level 2D colour image on THIS session's device, on dedicated
// memory imported from the buffer. Android only (false with `why` elsewhere, or when the device
// did not take the AHardwareBuffer extension). The image is born UNDEFINED; whoever uses it
@@ -664,7 +664,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// and the default draw framebuffer alike. The CAPABILITY decline inside
// ResolveWireDepthStencil (a stencil-only resolve without VK_EXT_shader_stencil_export
// on a device without VK_KHR_depth_stencil_resolve) is still decided per aspect, after
// colour ran; rule I (a) accepts that (notes/p7/magma-b2.md §2.10).
// colour ran; rule I (a) accepts that (docs/archive/notes/p7/magma-b2.md §2.10).
Bool DeclineWireDepthStencilResolveShape(const WireImage& source, const WireImage& destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1);
@@ -884,7 +884,7 @@
// (BlitDepthAcrossFormats, shared with the monolith arm) has no flip either. Both
// decline by name, each under its OWN once-site (review round 2: one site covered both,
// so after the first X decline a cross-format Y decline logged nothing); §6 in
// notes/p7/magma-b2.md carries them. These are declines of a LEGAL blit - GL would
// docs/archive/notes/p7/magma-b2.md carries them. These are declines of a LEGAL blit - GL would
// raise no error here - so the INVALID_OPERATION is rule I (a)'s named refusal, not
// GL's answer.
const Bool mirrorX = (spanX < 0) != (dstSpanX < 0);
@@ -1159,7 +1159,7 @@
// so a shader decline (a stencil aspect without VK_EXT_shader_stencil_export, a format
// that is not SAMPLED_IMAGE + DEPTH_STENCIL_ATTACHMENT, a failed pipeline create -
// WireMultisampleResolve.inc) that lands on the render pass is the resolve most likely
// to read back as zeroes with no GL error. A recorded debt (notes/p7/g5-msrbo.md §4).
// to read back as zeroes with no GL error. A recorded debt (docs/archive/notes/p7/g5-msrbo.md §4).
if (resolvedByShader) {
MGLOG_I_ONCE("DirectVulkan::ResolveWireDepthStencil: resolved by the shader pass "
"(prefer-shader=%d, render-pass arm available=%d)",
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Included by VulkanRenderer.cpp
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). Included by VulkanRenderer.cpp
// inside the backend namespace, after WireFramebuffer.inc, whose readback the present side's copy
// is modelled on: the same flush, the same write-visibility barrier, the same transition of the
// default framebuffer's image.
+1 -1
View File
@@ -85,7 +85,7 @@ namespace MobileGL::MG_Backend {
// (VkBufferManager::RegisterWireResourceOps, VkBufferManager.cpp): under a transport it
// owns buffer stores and claims bit 7 (the P4a mask, see ConsumedSubsystemsFor below);
// under monolith it registers none and its buffers keep the legacy BufferBackendOps path.
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
// P5e (docs/archive/contracts/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
//
// kCapRunAheadApply says "this server applies an unbarriered record without reading
// client memory". That is only true once EVERY per-draw family reads records instead
+21 -79
View File
@@ -55,24 +55,14 @@ namespace MobileGL::MG_Pipe {
Uint64 g_residualPulls = 0;
#if MOBILEGL_PIPE_VERIFY
// ---- P7 wave 3 (V1): negative control B's SERVER half ----------------------------
// ---- MOBILEGL_PIPE_POISON_OMIT, server half ----
//
// MOBILEGL_PIPE_POISON_OMIT withholds the STAMP of one (verb, field) pair, and until a
// verify build first ran on the split arm it was only ever read by the client's filler
// (MG_Impl/Pipe/PipeFill.cpp's ParsePoisonOmissionKnob). On the split arm that is not
// where the stamp the backend reads comes from: the stamp below re-stamps every field
// the verb's class can answer out of applied records, so it restamped the very field
// the client had withheld and the control went GREEN - measured, both backends, the
// child's glGenerateMipmap returned and the process exited 0 with
// GenerateMipmap:GetActiveTextureUnit omitted. The knob means "this verb's stamp of this
// field was never written", and under a transport the stamp that answers the backend is
// this one, so this one honours it too.
//
// PARSED HERE, NOT SHARED WITH THE CLIENT'S PARSER, because this file is in the server
// library and MG_Impl is not. A malformed value is the client's Fatal{PipeVerifyBadKnob}
// (it parses first, at the first fill of the process); this side simply does not arm.
// VERIFY BUILDS ONLY: the knob belongs to the verify harness, and a push build's stamp
// stays the branch-free fill the P5d profile asked for.
// The knob withholds the stamp of one (verb, field) pair, a negative control for the
// poison check. Under a transport the backend reads the server's stamp, which re-stamps
// every field the verb's class can answer, so the server must withhold the pair too or
// the control cannot go red. Parsed here because this file is in the server library and
// MG_Impl is not; a malformed value is the client's Fatal{PipeVerifyBadKnob}, and this
// side simply does not arm. Verify builds only, so the shipped stamp stays branch-free.
struct ServerPoisonOmission {
Bool Parsed = false;
String Value;
@@ -128,25 +118,13 @@ namespace MobileGL::MG_Pipe {
std::abort();
}
// P5e (gl), ID-117: THE ADMITTED MARKER, AND WHY IT IS ONLY THE TAG THAT CHANGED.
//
// `<field>@<verb>` and the `[BARRIER-PULLED, …, retires in <phase>]` tail are IDENTICAL
// to the Fatal above; the leading tag is `Admitted{` instead of `Fatal{`. Every filter
// written since P5c matches `Fatal{UnmigratedPipeInput`, so all of them keep meaning
// exactly "red" and none of them has to learn a new grammar to keep meaning it. A reader
// grepping for the pair still finds it, and the lane can ratchet on both sets with one
// regex per tag (ID-119's two-sided ratchet).
//
// THE DEDUPE IS NOT TIDINESS. An 852-draw Minecraft frame reaches an admitted readback
// row once per draw; without this, one frame writes 852 identical lines, the log file is
// the size of the run and the marker census cannot be read at all. So it is once per
// (field, verb) per process, which is the granularity the allowlist is written in.
//
// A PLAIN ARRAY, NOT AN ATOMIC, and that is the file's existing rule rather than a
// shortcut: CountBarrierPull is reachable only after the server stamped a verb boundary,
// which happens on the apply thread inside PipeApplier::ApplyOne, and g_residualPulls
// beside it is a plain Uint64 for the same reason. A racing writer here would at worst
// log a duplicate line, never lose one.
// The admitted marker: the same `<field>@<verb>` and `[BARRIER-PULLED, ...]` tail as the
// strict Fatal above, with `Admitted{` in place of `Fatal{`, so filters on
// `Fatal{UnmigratedPipeInput` still mean only "red" and the lane can match each set with
// one regex. Logged once per (field, verb) per process, the granularity of the allowlist;
// otherwise an ~850-draw frame writes one line per draw. A plain array, not an atomic:
// CountBarrierPull runs only on the apply thread after a server verb stamp, like
// g_residualPulls, and a race would at worst log a duplicate line.
void AdmittedBarrierPullOnce(MGPipeInputField field, MGPipeVerb verb, Bool escalated) {
const SizeT fieldIndex = static_cast<SizeT>(field);
const SizeT verbIndex = static_cast<SizeT>(verb);
@@ -170,49 +148,13 @@ namespace MobileGL::MG_Pipe {
kMGPipeFieldRetiringPhase[fieldIndex]);
}
// One place decides what a BARRIER-PULLED read does, so the field accessors and the
// seven sticky forwards cannot drift apart on it.
//
// ---- P5e (ra), CONTRACT-P5E §3.3: THE DETECTOR IS UNCONDITIONAL UNDER AN UNBARRIERED
// RECORD, AND THAT IS WHAT MAKES THE STRICT LANE A GATE -------------------------------
//
// The knob exists because under LOCKSTEP a pulled row is a real, ordered, fresh value:
// the client filled it and then parked, so "count it and carry on" is an honest
// measurement of remaining debt and MOBILEGL_IPC_STRICT_ERRORS is the operator asking
// for the debt to be loud instead. None of that survives run-ahead. With the client
// running ahead of this apply, the row was either never filled for this verb (§3.1
// skips the fill) or is being overwritten by a verb two frames later - so the value is
// torn or stale BY CONSTRUCTION and there is nothing for a counter to count. A
// "count it" arm here would be a wrong picture with a number beside it.
//
// P5e (gl), ID-119: THE OLD PIN HERE - "rsp is 0 on unbarriered records" - WAS VACUOUS
// AND HAS BEEN WITHDRAWN. The unbarriered arm below is [[noreturn]] and runs BEFORE
// ++g_residualPulls, so an unbarriered pull can never reach the counter whatever this
// function is written to do; the sentence was true of every possible implementation and
// therefore checked nothing. What rsp actually counts is BARRIERED pulls, and under the
// strict knob it counts exactly the ADMITTED ones, because every other barriered pull
// aborts two lines down. That is the form CONTRACT-P5E §7 now states and the lane checks.
//
// ---- P5e (gl), ID-117: AN ADMITTED PULL IS LOUD, NOT FATAL ---------------------------
//
// Before this, strict aborted on EVERY barrier-pulled read, admitted or not - which made
// the CI's allowlist comparison unreachable code (the run's rc != 0 exits first) and made
// "hard green" impossible with the knob as written. So there is a third state, and the
// two existing ones are untouched:
//
// unbarriered -> Fatal, no knob. The value is torn by construction.
// barriered, unadmitted -> Fatal under strict. A debt no phase has taken.
// barriered, admitted -> ONE MGLOG_W per (field, verb), and the entry completes.
//
// "Admitted" has three disjuncts and the third is a runtime one (ID-128): the generated
// table answers the first two, and the record's own escalation flag answers the third.
//
// An admitted pull is a debt this phase deliberately leaves standing: the field's row is
// BARRIER_PULLED, the verb's op is statically barriered so the client really is parked
// behind the record, and the field is inside the verb's own may-read mask - so the value
// is real, ordered and fresh, exactly ruling 4's "barriered records keep P5C semantics".
// MGPipeBarrierPullAdmitted is generated from those three tables (ID-116); there is no
// list here to drift.
// Decides what a BARRIER-PULLED read does, for the field accessors and the sticky forwards:
// unbarriered -> Fatal, whatever the knob: the client is not parked behind the record,
// so the row is torn or stale and there is nothing to count.
// barriered -> counted (g_residualPulls); under MOBILEGL_IPC_STRICT_ERRORS it is Fatal
// unless admitted, and an admitted one logs one MGLOG_W per (field, verb).
// Admitted: the generated MGPipeBarrierPullAdmitted table says so, or the record was
// barriered by an escalation, which only its payload shows.
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: UNDER THE DUAL-BLOCK REHEARSAL EVERY
// BARRIER-PULLED READ IS A NAMED FATAL, unconditionally - ahead of the barriered
+46 -175
View File
@@ -51,26 +51,12 @@ namespace MobileGL::MG_Pipe {
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// ---- 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
// - which is what turns the generated table from a document into a runtime mechanism:
//
// RECORD-SUPPLIED / APPLIER-DERIVED the server could answer it and did not: a real
// defect. Fatal, exactly as today.
// BARRIER-PULLED the server is reading the value the client's
// residual fill left in gPipeInputs while the verb
// barrier holds both threads apart (R-1). LEGAL, and
// COUNTED: PipeStats::CallClass::ResidualPulls. Under
// MOBILEGL_IPC_STRICT_ERRORS=1 it is Fatal instead.
// FATAL no carrier and the reduced path never reads it.
//
// AND IT IS ARMED ONLY INSIDE A SERVER-STAMPED VERB (PipeInputs::ServerStampedVerb).
// A split BUILD running monolith transport - which is every unit and integration-gpu lane
// of build-split - has a client that fills and stamps all 63 fields at every verb, so a
// stale read there is the same defect it is in a verify build and gets the same Fatal.
// Without that condition the leniency would apply to lanes whose stamps are the client's,
// and 1842 unit cases would quietly stop being able to go red.
// The server-side read check (ARCHITECTURE.md §9.2.1). Inside a server-stamped verb
// (PipeInputs::ServerStampedVerb) a stale read is judged by the field's ownership class:
// RECORD_SUPPLIED or APPLIER_DERIVED is a role violation, FATAL aborts, and BARRIER_PULLED
// (no production row) inside the verb's class counts as a residual pull unless
// CountBarrierPull (PipeInputs.cpp) aborts it. Outside such a verb every stale read is the
// plain poison Fatal, because there the client stamped every field itself.
void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped);
// The same decision for an accessor that takes an argument the table narrows on
// (kMGPipeFieldArgumentOwnership). Called BEFORE the freshness test, because the narrowed
@@ -194,25 +180,14 @@ namespace MobileGL::MG_Pipe {
static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount,
"the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows");
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
// sed is type-neutral:
//
// V (value) copied out of GLContext at fill time by calling the same accessor;
// no derivation logic is re-implemented here, which is what keeps the
// copy semantically identical by construction.
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
// slot/array for the accessors that return a non-const reference into
// the context. Identity is what phase C turns into a handle.
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
// context may be spelled). Sticky: stamped once by the first fill that
// sees a live context.
//
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
// The block the backends read instead of GLContext (ARCHITECTURE.md §9.2). Each accessor
// keeps the name, parameters and return type of its GLContext counterpart (Core.h).
// V (value) copied from GLContext at fill time through the same accessor, so no
// derivation is re-implemented here.
// O (object) a SharedPtr copy, or a raw pointer into a GLContext-owned slot or array.
// F (forward) argument-keyed lookups and reverse-channel calls, defined in PipeFill.cpp
// (MG_Impl/Pipe); sticky, stamped by the first fill that sees a live context.
// Other accessors run MGP_INPUT_CHECK then MGP_INPUT_VERIFY_READ; both compile away when off.
struct PipeInputs {
using GLContext = MG_State::GLState::GLContext;
using BufferObject = MG_State::GLState::BufferObject;
@@ -252,23 +227,12 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_PIPE_POISON
const MGPipeFilledState& FilledState() const { return m_filled; }
#endif
// 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
// a residual pull rather than an ordinary monolith call. A split build running
// monolith transport never sets it, which is why build-split's unit and integration
// cases behave exactly as a verify build's do.
//
// CLEARING IT IS THE APPLIER'S JOB AND NOT THE CLIENT'S, even though the client also
// does it. MGPipeValidateForVerb and MGPipeLeaveVerb both call
// MGPipeServerClearVerbBoundary, which is sufficient for inproc, where both roles share
// one process and one gPipeInputs - and misleading for P6, where MG_Impl is not in the
// server at all. There this flag would latch TRUE for the life of the server after the
// first stamp, every later read anywhere would be judged against the last verb's mask,
// and MGPipeStickyForwardPull would stop being a no-op outside a verb - so
// InvalidateCompileEnv reached from a later context's backend initialisation, the exact
// case the sticky exemption was written for, would be counted and, under strict, would
// abort. So: PipeApplier clears on leaving the applier. Not optional.
// True between the server's verb-boundary stamp and the applier clearing it. Only inside
// such a verb is a BARRIER_PULLED read counted rather than Fatal, and only there is a
// sticky forward a residual pull. PipeApplier::LeaveApplier must clear it after every
// record: a server process has no client half to do it, so the flag would stay set and
// later reads (InvalidateCompileEnv during another context's backend initialisation)
// would be judged against the last verb's mask.
Bool ServerStampedVerb() const { return m_serverStampedVerb; }
// ---- V: values ----
@@ -423,9 +387,9 @@ namespace MobileGL::MG_Pipe {
}
// THE ONE FIELD TABLE 2 NARROWS BY ARGUMENT. m_pixelStore[2] is one array indexed by
// this accessor's own argument, exactly as m_bufferBindingSlot[15] is indexed by a
// BufferTarget, and Coverage.def:62-69 already rules that such a field stays ONE row.
// BufferTarget, and Coverage.def's GetBufferBindingSlot row 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:
// (MGPipeApplySetPixelPackState) - so the field is APPLIER-DERIVED and the UNPACK half is FATAL:
// every gPipeInputs->GetPixelStoreParameters site in the tree passes false
// (DirectGLES.cpp's PackStateFromContext, StoreReadbackRowsToClient and ReadPixels, Utils.cpp's
// ReadbackImpl::StoreClientRows, RecordVerbSink::ReadBoundFramebufferTight and
@@ -799,63 +763,22 @@ namespace MobileGL::MG_Pipe {
TextureUnit* m_textureUnitBase = nullptr;
};
// 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
// spells - now one reference per THREAD, resolving to a served session's own block when the
// calling thread belongs to one. See the block after it.
//
// LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for
// pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply
// leaks the global singletons to the OS instead of running destructors during static
// teardown". This block breaks that rule if it is a value, because its O-class members
// are SharedPtrs to FRONTEND objects: a VertexArrayObject that the application deleted
// while it was bound has its last reference here, and destroying this block from
// __run_exit_handlers therefore runs ~VertexArrayObject -> ~BufferObject at exit. Those
// destructors are not exit-safe and cannot be made so - they reach the client's slot
// allocator, the resource tracker, the vertex-input emitter, the applier AND, through
// MGPipeApplyResourceDestroy, the backend's own twin tables, deferred-release queue,
// buffer pool and driver entry points, every one of which is either already destroyed or
// about to be. So the reference is never dropped: nothing here can start such a chain.
// A live context releases these SharedPtrs the ordinary way, at the fill point.
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
// The process-wide block, used by every thread outside a served session (see gPipeInputs
// below). Deliberately leaked at exit, like pGLContext and pActiveBackendObject: its O-class
// members can hold the last reference to frontend objects (a deleted but still bound VAO),
// and destroying them from exit handlers would reach the slot allocator, emitters, applier
// and backend twin tables after they are gone. A live context releases these references at
// the fill point.
inline PipeInputs& gPipeInputsDefault = *new PipeInputs();
// ============================================================================
// P14 S2: WHOSE BLOCK IS `gPipeInputs`?
// Which block `gPipeInputs` names
// ============================================================================
//
// ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it
// 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 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
// server's session registry and never replaced; null - the client process, the pull build, a
// unit case - makes every thread resolve to gPipeInputsDefault, which is the old behaviour
// byte for byte.
//
// BINDING IS PER THREAD AND HAPPENS AT FIRST USE, so a thread must be inside its session's
// scope before it touches the block. Both of a session's threads are: the control thread opens
// its scope before it reads a byte of the Hello, and the apply thread opens it as its first
// act (ServerLoop::ApplyThreadMain).
// Each served session applies on its own thread, so gPipeInputs resolves per thread through
// g_pipeInputsThreadResolver, installed once by the server's session registry (null in a
// client: every thread gets gPipeInputsDefault). The slot is a constant-initialized pointer
// (no TLS init call per access) bound at first use, so a thread enters its session's scope
// first: the control thread before the Hello, the apply thread in ServerLoop::ApplyThreadMain.
using PipeInputsThreadResolver = PipeInputs* (*)();
inline PipeInputsThreadResolver g_pipeInputsThreadResolver = nullptr;
@@ -891,26 +814,13 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_BUILD_DISAGGREGATED
// ============================================================================
// P5f (f1), P5F-WIRE-COMPLETENESS.md §4: THE DUAL-BLOCK REHEARSAL
// The client block (MOBILEGL_IPC_ROLE_SPLIT_STATE)
// ============================================================================
//
// One gPipeInputs served both roles because the verb barrier (R-1) made "the client filled
// it" and "the server is reading it" mutually exclusive in TIME on the same object. The
// rehearsal removes the same-object half of that sentence: under
// MOBILEGL_IPC_ROLE_SPLIT_STATE=1 the client's residual fill writes the CLIENT block below
// and the backend/applier keep reading gPipeInputs, which is the SERVER block. Every path
// that worked only because the two were one object - a BARRIER-PULLED field, a sticky
// forward's read of the fill side's stamps - then has no value to read, and the read
// becomes a NAMED Fatal{UnmigratedPipeInput, "<field>@<verb>"} (CountBarrierPull's
// 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 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.
//
// Same leak-at-exit storage as gPipeInputs above, for the same reason.
// With the knob on and a real transport, the client's residual fill writes this block
// (MGPipeClientInputs) while the applier and backend read gPipeInputs, the server block; a
// read that relied on one shared object then aborts by name (CountBarrierPull) instead of
// returning a cross-role value. Otherwise both roles use the one shared block. Leaked at
// exit like gPipeInputsDefault.
inline PipeInputs& gPipeInputsClientBlock = *new PipeInputs();
// PipeInputs.cpp. Whether the REHEARSAL is armed: the knob AND a real transport (and not
@@ -1023,52 +933,13 @@ namespace MobileGL::MG_Pipe {
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
// ============================================================================
// P5: the server-side verb stamp, and the counter that sizes what it leaves behind
// The server-side verb stamp
// ============================================================================
//
// THE PREREQUISITE NOBODY ELSE OWNS (CONTRACT-P5.md section 3). Nothing stamps the poison
// generations on the applier side today and that is deliberate (see MGPipeApplyAccess'
// comment above: a stamp is the filler's statement, not the applier's). Under split the
// filler is in the other role, so without this every FilledGen[] would stay 0,
// MGPipeInputFieldIsFresh would answer false for EVERYTHING, and a purely server-side read
// would abort on the first field inside SyncRenderState - before any interesting case.
//
// THE RULE, in three lines, and the third one is the load-bearing one:
//
// 1. bump CurrentVerbSerial and set the verb, so a Fatal names it instead of "<none>";
// 2. stamp every RECORD-SUPPLIED and APPLIER-DERIVED field with the new serial - those
// are exactly the fields the records this verb carried can answer;
// 3. ZERO every BARRIER-PULLED and FATAL field's stamp.
//
// (3) is what makes the instrumentation real. The client's residual fill stamps ALL 63
// fields at its own verb boundary (PipeFill.cpp step 4), so without the zeroing every
// field would read fresh on the server, `rsp` would be identically 0, and the gate would
// be decoration - the precise "an inproc implementation proves nothing" failure R-2
// exists to prevent. Zeroing also cancels the sticky exemption for free: generated/
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so gen == 0 wins.
//
// The value a BARRIER-PULLED read then gets is still the client's residual fill's, and it
// is still CORRECT - because the verb barrier (R-1) leaves exactly one of the two threads
// runnable. That is the debt, not a bug; `rsp` is its size.
//
// v1 calls this from Server/PipeApplier::StampVerbBoundary. MGPipeVerbForWireOp maps the
// record's op onto a verb and answers kVerbCount for an op that is not verb-shaped, which
// is the case the applier must NOT stamp on: a set_dynamic_state between two draws is not
// a new verb, and stamping there would retire the previous verb's answers early.
//
// TWO THINGS THE CALLER INHERITS AND SHOULD NOT REDISCOVER:
//
// (a) The records BETWEEN two boundaries apply under the earlier boundary's stamp - its
// serial, its class mask and its verb NAME. That is correct today because every
// MGPipeApply* entry point touches gPipeInputs through MGPipeApplyAccess, which
// carries no MGP_INPUT_CHECK; the day one of them calls back into the backend, its
// reads will be judged against a verb they do not belong to.
// (b) The verb a draw record stamps is MGPipeVerb::DrawArrays for ALL TWENTY draw verbs.
// The class mask is right (FillPoints.def puts all twenty in kDraw) and the NAME in a
// Fatal is not: a glDrawElements that aborts will say "@DrawArrays". draw_vbo carries
// no verb id, so fixing it means either a field on the record or a second argument
// here; it is cosmetic for the verdict and misleading for the reader, and it belongs
// with whatever phase widens draw_vbo to the multi-draw family (P8).
// Called by PipeApplier::StampVerbBoundary for each op MGPipeVerbForWireOp maps to a verb.
// Bumps CurrentVerbSerial, sets the verb, stamps the RECORD_SUPPLIED and APPLIER_DERIVED
// fields in the verb's class and zeroes every other stamp; without the zeroing every read
// would look fresh, since the client's fill stamped all fields. Records between boundaries
// apply under the earlier stamp; all draws stamp as DrawArrays (ARCHITECTURE.md §9.2.1).
void MGPipeServerStampVerbBoundary(MGPipeVerb verb);
// MANDATORY for the applier when it leaves the verb. The client's own MGPipeValidateForVerb
// and MGPipeLeaveVerb call it too, which is enough for inproc and NOT enough for a spawned
@@ -309,7 +309,7 @@ namespace MobileGL::MG_Record {
return false;
}
// ---- P9 (W1, MG_Remote/CONTRACT-P9.md §1): the pack-buffer readbacks ------------------------
// ---- P9 (W1, docs/archive/contracts/CONTRACT-P9.md §1): the pack-buffer readbacks ------------------------
//
// WHY THE ROWS GO THROUGH THE APPLIER'S BUFFER-WRITE GATE AND NOT THROUGH THE DRIVER'S OWN
// PBO READ. Monolith binds the driver buffer as GL_PIXEL_PACK_BUFFER and lets glReadPixels
@@ -503,7 +503,7 @@ namespace MobileGL::MG_Record {
slot);
}
// P5b d1 (MG_Remote/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
// P5b d1 (docs/archive/contracts/CONTRACT-P5B.md §2 d1): draw_vbo's whole cross product. The record
// carries the GL call verbatim (rule D) and this body reproduces the backend call the
// monolith makes for that shape, reading only the record and the backend's own
// barrier-pulled state; the twenty GL entry points collapse onto the arms below:
@@ -812,7 +812,7 @@ namespace MobileGL::MG_Record {
}
// -----------------------------------------------------------------------------------
// P5b: the stubs the four migration packages replace (MG_Remote/CONTRACT-P5B.md).
// P5b: the stubs the four migration packages replace (docs/archive/contracts/CONTRACT-P5B.md).
//
// Each dies by the GL slot's own name. The record has crossed and been validated by the
// codec by the time one of these runs, so the only thing missing is the backend call, and
@@ -821,7 +821,7 @@ namespace MobileGL::MG_Record {
// in the same words), the call, and a tally the lane can assert moved.
// -----------------------------------------------------------------------------------
// ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
// ---- i1 ---- (docs/archive/contracts/CONTRACT-P5B.md §2 i1; landed by package p5b/i1)
//
// RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the
// record and from server state, and NOTHING ELSE: the backend goes on reading the frontend
@@ -973,7 +973,7 @@ namespace MobileGL::MG_Record {
return true;
}
// ---- t2 ---- (MG_Remote/CONTRACT-P5B.md §2 t2)
// ---- t2 ---- (docs/archive/contracts/CONTRACT-P5B.md §2 t2)
//
// SIX BODIES, SIX BACKEND CALLS, NO STATE OF THEIR OWN. Rule D: the record IS the call, and
// everything the backend reads around it - the capture program, the capture-buffer
+1 -1
View File
@@ -45,7 +45,7 @@ namespace MobileGL::MG_Record {
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.
// ---- P5b (docs/archive/contracts/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
@@ -10,7 +10,7 @@
// StagedShadow.h (R-11), held to its four rulings (CONTRACT-P5C.md §2.1).
//
// THE DEFECT THIS ENDS (T1/T5). resource_subdata's texture half stages the level bytes into
// SEG_STAGE and ApplyTextureUpload (PipeApply.cpp:989-1036) drops the pointer after the gate,
// SEG_STAGE and ApplyTextureUpload (PipeApply.cpp) drops the pointer after the gate,
// the accumulation and the serial. Espryt then re-reads the client's MipmapStorage at sync
// time (Managers.cpp:6940, :7129, :7198, :7315), reads per-level shape off the client object
// (:6928, :6936, :7314, :7332, DirectGLES.cpp:8502-8507, :8528-8529), and Magma WRITES the
@@ -524,7 +524,7 @@ namespace MobileGL::MG_Record {
EraseSession(m_driverWritten, session);
}
// ---- P8-E (docs/Disaggregated/notes/p8/E.md): WHEN THESE BYTES ARE THE LEVEL'S CONTENT ----
// ---- P8-E (docs/archive/notes/p8/E.md): WHEN THESE BYTES ARE THE LEVEL'S CONTENT ----
//
// The store holds what the client uploaded. It is also the only CPU copy of a level the
// server has, so a readback the driver refuses (a format the ES driver will not attach -
+1 -1
View File
@@ -8,7 +8,7 @@
// mobilegl_gbm.so: a GBM backend (the loader's backend ABI, gbm_backend_abi.h) whose buffer
// objects are MobileGL shared images - colour buffers the MobileGL server allocates and exports
// as dma-bufs (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Selected with
// as dma-bufs (docs/archive/notes/anland/plan-ahb-dmabuf.md). Selected with
// GBM_BACKEND=mobilegl; the loader then finds <dir>/mobilegl_gbm.so on its backend path.
//
// THE DEVICE DESCRIPTOR IS OPAQUE. The fd a gbm_device is created on is only the identity the
@@ -8,7 +8,7 @@
#pragma once
// WHEN glFlush IS A SHARED-IMAGE BOUNDARY (docs/Disaggregated/notes/anland/plan-x11-gpu.md).
// WHEN glFlush IS A SHARED-IMAGE BOUNDARY (docs/archive/notes/anland/plan-x11-gpu.md).
//
// A dma-buf producer that renders into its buffers and hands them on after a glFlush (an X
// server's glamor committing a window pixmap) relies on implicit sync: whoever samples the buffer
@@ -44,7 +44,7 @@
// call sites below are declarations only, exactly as the frontend's mutators are.
#include <MG_Pipe/PipeMutation.h>
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): AN UNPACK SOURCE IS READ FROM THE BUFFER'S CLIENT SHADOW,
// P9 (W1, docs/archive/contracts/CONTRACT-P9.md §1): AN UNPACK SOURCE IS READ FROM THE BUFFER'S CLIENT SHADOW,
// and under a transport a GPU write into that buffer - a read_pixels_to_buffer, which is how a
// glReadPixels into a pack buffer now crosses, or a shader store - reaches the shadow only through
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
+1 -1
View File
@@ -8,7 +8,7 @@
#pragma once
// GLX WINDOWS PRESENTED THROUGH DRI3 + PRESENT (docs/Disaggregated/notes/anland/plan-x11-gpu.md).
// GLX WINDOWS PRESENTED THROUGH DRI3 + PRESENT (docs/archive/notes/anland/plan-x11-gpu.md).
//
// A GLX window's frames are drawn by the server into a pbuffer. Instead of reading each frame back
// and putting it on the window, the frame is copied GPU-side into one of a few server-allocated
+7 -27
View File
@@ -4,33 +4,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// WHICH ELEMENTS OF A CLIENT-MEMORY ARRAY A DRAW ACTUALLY FETCHES.
//
// A client-memory vertex array has no extent the object could report, so "how much of it
// has to exist before this draw can run" is a question only the DRAW can answer: the
// index bytes it will read, the instance count and base instance it shifts by, and - for
// the indirect forms - the command block a shader may have written moments earlier.
//
// Two roles ask it and for the same reason. The wire arm asks it to snapshot the fetched
// elements into owned buffers before the draw crosses (MGPipeOwnedDrawInputs), and the
// monolith arm asks it to size the per-draw upload the backend has always done from the
// application's own pointer (DirectGLES' SyncClientSideAttributesForDraw*, Magma's
// UploadAndBindVertexBuffers). The question is identical, so the answer lives here once:
// two copies of this arithmetic would be two chances to disagree about which vertices a
// draw reads, and disagreeing is a wrong picture rather than an error.
//
// WHAT IT DELIBERATELY DOES NOT DO: dereference application memory. The plan names element
// INDICES; reading the bytes behind them is the caller's, because only the caller knows
// whether the elements are read from the client's own array or out of a store the server
// owns.
//
// COMPILED IN EVERY FLAVOR, and the guard it used to carry (`MOBILEGL_BUILD_DISAGGREGATED &&
// MOBILEGL_PIPE_PUSH`) was the same mistake the monolith arm's callers carried: the
// MONOLITH arm is not a split-arm fallback, it is the arm the pull build runs. An indexed or
// indirect draw over a client-memory array staged nothing there either, so the question has
// to be answerable - and answered - in the transport-free library too. The plan itself is
// pure arithmetic over the draw's own words; what changes per flavor is only who reads the
// bytes (the caller's reader), never the answer.
// Which elements of a client-memory vertex array a draw fetches. Only the draw can say: the
// index bytes it reads, its instance count and base instance, and for indirect forms the
// command block. The wire arm snapshots those elements into owned buffers
// (MGPipeOwnedDrawInputs); the monolith arm sizes the backend's per-draw upload from the
// application pointer. One copy of the arithmetic keeps the two from disagreeing about which
// vertices a draw reads. The plan names element indices only; the caller's reader dereferences
// memory. It is built in every configuration, since the monolith arm needs it too.
#pragma once
#include <MG_Pipe/MGPipeTypes.h>
+20 -112
View File
@@ -9,78 +9,14 @@
#pragma once
#include <Includes.h>
// P4a's PROGRAM-PIPELINE COMPOSITE, on the client side.
//
// GLContext::GetProgramForDraw() already flattens a bound pipeline into one hidden composite
// ProgramObject entirely in the frontend - it joins every graphics stage, computes the
// pipeline's draw-program signature, looks it up in the pipeline's own cache and, on a miss,
// attaches each stage's LINKED SNAPSHOT into a fresh ProgramObject and links it. All of that
// is frontend work and none of it moves. What this file adds is the one thing the wire needs:
// the composite gets ONE handle, out of the ShaderCso reserved high band, and
// create_shader_state goes out for it exactly as for an ordinary program. THE SERVER NEVER
// LEARNS IT IS A COMPOSITE and needs no "resolved draw program" hook at all.
//
// WHY A BAND RATHER THAN A FLAG ON THE HANDLE: a flag would have to be carried, honoured and
// masked off by every consumer of a ShaderCso handle, on both sides; a reserved slot range is
// a property of the allocator instead, so "an ordinary program can never be handed a composite
// slot" is true by construction. MGPipeSlotAllocator::Allocate refuses the band outright and
// AllocateComposite is the only door in.
//
// WHAT THIS FILE IS ACTUALLY FOR: the composite's slot has TWO INDEPENDENT RELEASE PATHS and
// either order has to free it exactly once.
// * the pipeline cache drops the composite when the draw-program signature moves. In the
// frontend that overwrite drops the last SharedPtr, so the composite's own destructor
// usually runs first; the resolver still speaks the release, because "usually" is not a
// contract and a client that only reacted to destructors would leak a slot the moment the
// frontend started holding a second reference.
// * the composite ProgramObject's own ~ProgramObject, which is an ordinary program's death
// path and takes the same helper.
// Both go through MGPipeEmitShaderCsoDestroyAndFree, and whichever runs second is a PROVEN
// no-op: MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and
// bumps no generation of its own, so a double release cannot skip a generation either.
//
// THE MEMO's KEY IS (CONTEXT ID, PIPELINE GL NAME) AND THE CONTEXT HALF IS NOT OPTIONAL.
// This resolver is a PROCESS singleton while a pipeline's GL name is per context: GLContext
// owns m_programPipelines AND its own name generator m_programPipelineNames (Core.h), so name
// N names two different ProgramPipelineObjects in two contexts, each with its own composite
// and its own handle. Keyed on the name alone, the first emission after a make-current found
// the OTHER context's entry, matched nothing - two composites are two ProgramObjects with two
// lifetime ids, so the handles differ even when the stage set and the signature are identical
// - and released it: a delete_shader_state and a cleared publication latch for a composite
// whose frontend ProgramObject is alive, its band slot handed back and re-issued at gen + 1,
// and the server rebuilding that program (glslang + SPIR-V + spirv-opt, the very cost the
// signature below exists to avoid) once per context switch.
//
// 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 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.
//
// WHAT RELEASES A DESTROYED CONTEXT's ENTRIES: nothing in this file, and that is the correct
// answer rather than an omission. Destroying a context drops m_programPipelines, which drops
// each ProgramPipelineObject, which drops the composite it cached; ~ProgramObject then runs
// MGPipeEmitShaderCsoDestroyAndFree - the composite's OWN release path, the second of the two
// above - and the slot goes back exactly once. The entries those composites leave behind can
// never be found again (no future Observe can carry a dead context id) and could not release
// anything if they were (the allocator erases the lifetime-id mapping on Free), so Reset()
// DROPS them instead of releasing them. That is also what bounds the vector; see Reset().
//
// THE SIGNATURE IS ComputeDrawProgramSignature(), the per-graphics-stage {lifetimeId,
// GetLinkVersion()} array - and DELIBERATELY NOT GetBackendStateVersion(), which is what made
// the SSO conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) on every draw,
// because a glUniform1i to a sampler moves it.
//
// HEADER-ONLY, for the ownership reason Tracker.h states: a new .cpp would need the root
// CMakeLists.txt, which is the contract package's.
//
// IT IS INCLUDED BY ProgramEmit.h AND NOT THE OTHER WAY ROUND, deliberately: the composite is
// a special case of the program family's own emission, so the family header depends on this
// 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.
// Client side of the program-pipeline composite. GLContext::GetProgramForDraw() builds the
// composite in the frontend, and the program emitter gives it one ShaderCso handle from the
// allocator's reserved band (AllocateComposite), so the server never learns it is a composite.
// This resolver remembers each (context, pipeline)'s composite and releases the old slot when
// the draw-program signature moves; the composite's ~ProgramObject is the other release path,
// both go through MGPipeEmitShaderCsoDestroyAndFree, and the later one is a no-op. Entries are
// keyed on (GLContext::GetTextureContextId(), pipeline GL name); see ARCHITECTURE.md §5.6.1.
// Included by ProgramEmit.h; it depends on nothing of the program family.
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeMutation.h>
@@ -162,28 +98,12 @@ namespace MobileGL::MG_Pipe {
return handle;
}
// A make-current, and it RELEASES NOTHING. The entries name composites that belong to
// the frontend objects of the context being left, those objects outlive the switch, and
// releasing them would emit a delete for a live program.
//
// NOR IS ANY MEMO INVALIDATED, and that is what the context key bought. This used to
// clear a per-entry `Fresh` flag beside `Live`, because with a name-only key an entry
// could not say whether it described "my own pipeline before the switch" or "another
// context's pipeline of the same name" - and exactly one of those two properties could
// hold at a time. The key answers the question directly now, so the freshness flag and
// its one reader (a HandleFor() accessor that had no caller anywhere in the tree) are
// both gone rather than left as scaffolding: `Live`, the release obligation, is the
// entry's only state and nothing but ReleaseEntry may clear it.
//
// WHAT IS LEFT TO DO HERE IS RECLAMATION, and this is the one moment the client is told
// that a context boundary was crossed. An entry whose composite slot is no longer live
// has had its obligation discharged elsewhere - by that composite's own ~ProgramObject,
// which is precisely what happened to EVERY entry of a context that has just been
// destroyed - so it is DROPPED rather than released: a release would resolve nothing
// anyway (the allocator erases the lifetime-id mapping on Free) and no reader is left.
// Without this the vector would grow by one per (context, pipeline name) pair the
// process ever used, where the name-only key bounded it by the highest pipeline name;
// with it, it is bounded by the pairs whose composite slot is actually live.
// Called at a make-current. It releases nothing, since the entries' composites still
// belong to live frontend objects, and invalidates nothing, since entries are keyed by
// context. It only drops entries already released or whose composite slot is no longer
// live, freed by the composite's ~ProgramObject (every entry of a destroyed context ends
// this way); releasing those again would resolve nothing. This bounds the vector by the
// (context, pipeline) pairs whose composite is still live.
void Reset() {
SizeT kept = 0;
for (SizeT i = 0; i < m_entries.size(); ++i) {
@@ -208,24 +128,12 @@ namespace MobileGL::MG_Pipe {
private:
struct Entry {
// NO FRONTEND SharedPtr, and that is the exit-order rule rather than a style
// choice: a static that held one would put a frontend destructor on an exit
// handler's path into a torn-down pipe. A GL name, a signature of plain integers,
// a handle and a lifetime id are all this needs.
// KEYED ON (CONTEXT ID, GL NAME), and the name half is the GL name because a
// ProgramPipelineObject has no lifetime id - ComputeDrawProgramSignature reads the
// STAGE programs' ids and the pipeline itself carries none. The context half is
// GLContext::GetTextureContextId(); see the file header for why the name alone was
// wrong and why the context ADDRESS would be too.
//
// WITHIN ONE CONTEXT glGenProgramPipelines recycles names, so a deleted-and-
// recreated pipeline can still inherit its predecessor's entry; that is bounded and
// self-correcting rather than a hazard. The first Observe on the new object finds a
// signature and a handle that do not match and releases the old entry, and that
// release resolves NOTHING - the allocator erases the lifetime-id mapping on Free,
// so a stale CompositeLifetimeId emits no delete and frees no slot; all it costs is
// one redundant, idempotent death notice, which is the same shape the composite's
// own second release path already has.
// Holds no frontend SharedPtr: the resolver is a never-destroyed static, and a
// frontend destructor must not run at exit into a torn-down pipe. Keyed on
// (context id, pipeline GL name) because a ProgramPipelineObject has no lifetime
// id; see the file header. A name recycled within one context can inherit its
// predecessor's entry; the first Observe then releases it, a no-op because the old
// composite's ~ProgramObject already freed the slot and its lifetime-id mapping.
Uint64 ContextId = 0;
Uint PipelineName = 0;
DrawProgramSignature Signature{};
+8 -29
View File
@@ -9,35 +9,14 @@
#pragma once
#include <Includes.h>
// The render-state CSO cache (ARCHITECTURE.md 4.5.2 / 5.3, P2 brief D7).
//
// THE LOOKUP, and the first step is the whole point:
// 1. m_pipelineStateVersion (widened) did not move -> reuse the last handle. ZERO hashing,
// zero probing, and nothing is emitted unless m_version also moved. That is the steady
// state of every frame, and it is why the tracker asks the cache at all only when the
// dirty walk says the pipeline version moved.
// 2. moved -> hash the 396 pipeline bytes, probe, and on a hit CONFIRM WITH A MEMCMP
// before reusing the handle. ARCHITECTURE.md 4.1 says content addressing on an
// xxHash; a bare 64-bit equality would let a collision alias two different render
// states onto one CSO, which is silent wrong pixels with no gate that can see it.
// Hashed constant-state caches confirm a hit with a full-key memcmp for the same reason.
// The memcmp only ever runs on a pipeline-version change, i.e. never in the steady state.
// 3. miss -> mint a slot, emit create_render_state with every pipeline chunk, then bind.
//
// CAPACITY 64 (ROADMAP.md P2). 64 x (8 + 8 + 396 + 8) = about 26 KB per context. ROADMAP.md
// open question 4 says 64 is provisional and the counters retune it at P13; this ships 64
// and publishes the mint / bind / evict counters that retune reads.
//
// THE NEGATIVE CONTROL. kMGPipeBehaviourNoCsoContentAddressing (bit 63 of the runtime
// MOBILEGL_PIPE_PUSH bitmask) turns off the PROBE and the handle reuse, not the records:
// every pipeline-version change then mints a fresh CSO, binds it and evicts, which is
// precisely "whole-block content addressing" and reproduces the regression
// RenderState.h records. It is what separates "push is slower" from "the CSO design is
// slower", and CsoContentAddressingScenario (package E) is the always-on ctest that stops
// the switch from rotting.
//
// 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.
// The render-state CSO cache (ARCHITECTURE.md §2.3, §5.3). EmitRenderState calls Acquire only
// when the pipeline-state version moved, so the steady state does no hashing. Acquire hashes the
// 396 pipeline bytes, probes, and confirms a hit with a memcmp: a bare 64-bit match could alias
// two render states onto one CSO, which is silently wrong pixels. A miss mints a slot and emits
// create_render_state with every pipeline chunk. 64 entries, LRU, about 26 KB per context; the
// counters give the hit rate. kMGPipeBehaviourNoCsoContentAddressing (bit 63 of
// MOBILEGL_PIPE_PUSH) disables reuse as a negative control, which CsoContentAddressingScenario
// keeps exercised.
#include <Config.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
+15 -50
View File
@@ -9,22 +9,9 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's framebuffer family: set_framebuffer_state, emitted at the validate
// point once per bound TARGET that moved, or once with Target = Both when the two bindings
// name the same object.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT, and the
// split is the whole reason it exists this early. MG_Impl/Pipe/PipeFill.cpp is the contract
// package's for the entire phase - it carries Coverage.def's enum-coupled block, the validate
// point and the death helpers - so the emitter package must not edit it. What it edits instead
// is this header: the emitter's BODY, and the value of kMGPipeWiredFramebufferSubsystem below.
// That is what makes "no file is touched twice by two packages" structural rather than a
// convention, and it is what the bb2a236d semantic-merge trap taught (two branches green
// separately, the integrated tree not compiling).
//
// 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.
// Client side of the framebuffer family: set_framebuffer_state, emitted at the validate point
// once per bound target that moved, or once with Target = Both when both bindings name the
// same object, and before a DSA call that names a framebuffer (EmitFramebufferByName).
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/TextureEmit.h>
@@ -179,24 +166,13 @@ namespace MobileGL::MG_Pipe {
}
// ---------------------------------------------------------------------------------
// D-C4: ContentHash, and the one input it must not swallow
// ContentHash
// ---------------------------------------------------------------------------------
//
// XXH64 over the WHOLE record with ContentHash itself zeroed, computed field-wise into a
// zero-initialised staging copy so that no padding byte can enter the hash. Two jobs: the
// server's render-pass memo key, and this client's emission suppressor.
//
// IT MUST COVER Fbo. A recycled framebuffer handle whose successor happens to carry an
// identical attachment set would otherwise be suppressed against its predecessor; Fbo
// carries Gen, so it cannot be.
//
// IT MUST COVER DrawBuffers[8], and this is the trap worth naming. The backend derives the
// fragColor BROADCAST COUNT from the draw-buffer array, and it does that at the verb, from
// the framebuffer state it then holds, precisely so a program can relink inside the same
// draw. A hash that did not cover the array would let a suppressed set_framebuffer_state
// mean "the draw buffers did not move" when they had, and the shader would be specialised
// for the previous output shape. With the array in the hash, a suppression provably means
// the array did not move, which provably means the broadcast count did not move.
// XXH64 over the whole record with ContentHash zeroed, copied field by field into a zeroed
// staging record so padding never enters it. It keys the server's render-pass memo and this
// client's suppressor, so it must cover Fbo (its Gen separates a recycled handle with the
// same attachments) and DrawBuffers (the backend derives the fragColor broadcast count from
// them at the verb, so a suppressed record must mean that count did not move).
inline void MGPipeCopySurfaceForHash(MGPSurface& dst, const MGPSurface& src) {
dst.Res = src.Res;
dst.InternalFormat = src.InternalFormat;
@@ -319,23 +295,12 @@ namespace MobileGL::MG_Pipe {
return bytes;
}
// ID-19(c): EVERY DSA ENTRY POINT THAT HANDS A FRAMEBUFFER TO THE SERVER BY NAME IS
// PRECEDED BY A RECORD FOR IT, and that is the phase's main correction rather than a
// nicety. With only the two BOUND-target records, glClearNamedFramebufferfv(fbo) on an
// unbound fbo made the backend mint a fresh driver framebuffer with NO ATTACHMENTS,
// find no record for it, decline, and issue the clear against it anyway -
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
// correctly (esprytobj C-1).
//
// THE TARGET IS Named ONLY WHEN THE OBJECT IS BOUND TO NEITHER BINDING. A record always
// writes FramebufferRecords[Fbo.Slot]; Draw/Read/Both ADDITIONALLY set the bound
// handle(s). So handing a currently-bound framebuffer a Named record would overwrite
// the bound record's Target with one that says "no binding" while BoundFramebuffer
// still names it, and the server would read a record whose Target contradicts the
// binding it is resolved through. Re-asserting the binding the object already has is
// free (the content hash suppresses it) and keeps the two consistent.
//
// Returns the bytes that went on the wire.
// Emits a record for `fbo` before a DSA call hands it to the server by name; without
// it, a named clear of an unbound framebuffer would find no record and run against an
// empty driver framebuffer. Target is Named only when `fbo` is bound to neither binding:
// every record writes FramebufferRecords[Fbo.Slot], and a Named record for a bound
// object would leave a Target that contradicts BoundFramebuffer. Re-asserting the
// existing binding is suppressed by the content hash. Returns the bytes sent.
Uint64 EmitFramebufferByName(const FramebufferObject& fbo) {
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
MGPipeFramebufferTarget target = MGPipeFramebufferTarget::Named;
+15 -49
View File
@@ -9,29 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of set_shader_images, the third of P4a's kVarTail unit sets. It rides
// SamplerEmit.h's subsystem bit (kMGPipeWiredSamplerSubsystem): one family, one A/B.
//
// TWO INVARIANTS THAT MUST SURVIVE INTO THE BODY, and they are the kind an optimisation
// deletes:
// 1. THE HIGH-WATER-ZERO EARLY-OUT. An image high-water mark of 0 emits nothing, BEFORE any
// hash - that is what makes every Minecraft draw pay one integer test for a feature it
// does not use.
// 2. THE SWEEP'S GATE IS KEYED ON FRONTEND GENERATIONS AND DELIBERATELY NOT ON A BACKEND
// RE-MINT COUNTER. A texture bound ONLY to an image unit is re-minted INSIDE the sweep,
// so a server-side epoch would be bumped after the gate had already declined. The
// client's bit-14 shutter is Mix(Mix(textureContent, textureParams), programImageUnitVersion)
// - all three FRONTEND counters - so the property is preserved by construction, and it is
// written here because it is invisible from the shutter itself.
//
// The record carries the APPLICATION's format and access; the bind-format recast (a GL_RG32F
// bind is INVALID_VALUE on 19 of 26 non-core formats on Adreno) and the buffer-texture split
// view stay SERVER-side and unchanged. ContentHash therefore has to cover InternalFormat and
// Access as well as the binding, because the format the shader was built against is live
// glBindImageTexture state and the format-less image bake keys on it.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
// Client side of set_shader_images, under SamplerEmit.h's subsystem bit (one family, one A/B).
// Two invariants an optimisation must keep. An image window of 0 emits nothing before any hash,
// so a draw that uses no images pays one integer test. The shutter gating the sweep mixes only
// frontend counters (texture content and params, program image units, texture binds), never a
// backend re-mint counter: a texture bound only to an image unit is re-minted inside the sweep,
// after the gate has decided. The record carries the application's format and access; the
// bind-format recast and buffer-texture split view stay on the server, so the content hash
// covers InternalFormat and Access too.
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -118,32 +103,13 @@ namespace MobileGL::MG_Pipe {
public:
using GLContext = MG_State::GLState::GLContext;
// set_shader_images. Start is 0 and Count is the image-unit window described below.
//
// WHERE THE HIGH-WATER MARK COMES FROM, because this is the one place a reader will look
// for it. DirectGLES keeps g_imageUnitHighWaterMark, but that is written from inside its
// own per-unit sync and lives on the far side of the boundary.
//
// AND IT IS STILL NOT TextureState's MARK, EVEN THOUGH ONE NOW EXISTS. P5d round 3
// (package C) added TextureState::NoteImageUnitTouched - a push-build-only image-unit
// high-water mark, fed by glBindImageTexture - for the split client's per-draw
// GPU-write sweep. That mark answers "which units could hold a binding"; the window
// below answers the strictly narrower "which units could a SHADER READ", which is what
// makes the zero early-out fire for an application that binds an image no shader names.
// Swapping one for the other would widen this emitter for no record's benefit, and the
// paragraph that used to stand here - "adding a counter to TextureState would resize the
// pull build's object, which G1 forbids outright" - is answered by that mark being
// compiled only into push builds, where the object is already a different size.
//
// So the window is derived instead, from the one thing that decides whether an image
// unit can matter at all: the highest image unit the CURRENT PROGRAM names, memoised
// per program state in SamplerEmit.h's shared inversion, UNIONED with a sticky mark of
// every unit this emitter has already described. A program with no image uniforms
// gives MaxImageUnit == -1 and, with nothing sticky yet, a window of 0 - which is the
// zero early-out, taken BEFORE any hash and before any 192-entry walk, exactly as
// property 1 requires. The mark is sticky so that a program which stops naming a unit
// does not silently stop describing it: the window only grows, and shrinking it is how
// a stale binding would become invisible to the server.
// set_shader_images. Start is 0; Count is the image-unit window: the highest image unit
// the current draw program names (memoised in SamplerEmit.h's program inversion),
// unioned with a sticky mark of every unit already described. TextureState's
// touched-unit mark is not used: it counts units holding a binding, not units a shader
// reads, and would defeat the zero early-out for an application that binds an image no
// shader names. The window only grows, so a unit a later program stops naming is still
// described and a stale binding stays visible to the server.
Uint64 EmitShaderImages(GLContext& ctx) {
const auto& program = ctx.GetProgramForDraw();
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(program.get());
File diff suppressed because it is too large Load Diff
+23 -75
View File
@@ -47,86 +47,34 @@ namespace MobileGL::MG_Pipe {
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
void MGPipeLeaveVerb();
// PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, AT THIS MASK, EMIT FOR THIS P4a FAMILY?
// (ID-39, widened by S-3 / ID-41.) The four conjuncts are the operator's per-subsystem bit
// in MOBILEGL_PIPE_PUSH, the family's own kMGPipeWired*Subsystem constant (`wired`, which
// the caller passes because it lives in the family's emit header and this header may not
// include one), and - for the four families P4a migrates - a backend having registered
// MGPipeResourceOps (the same per-backend signal `MGPipeResourceSubsystemEnabled()` has
// applied to P3a's buffers since the phase began) and every D-K2 dependency bit of the
// family being set in the same mask.
//
// THE LAST TWO CONJUNCTS ARE THE ONES THIS DECLARATION EXISTS FOR, and they are the same
// defect twice. Magma (DirectVulkan) registers no table and has no P4a twins; at a mask like
// 0x7ff Espryt REFUSES the texture family server-side because D-K2's fourth row says bit 10
// requires bit 11. In both cases the client emitted anyway, the applier accepted, the
// emitters cleared their per-level dirty flags on that acceptance, and the legacy upload
// path that still owed those texels found nothing to upload (66 DirectVulkan cases at ID-39,
// 47 DirectGLES cases at ID-41). With them the four families emit NOTHING in that state and
// the legacy pull path runs exactly as it does on a pull build.
//
// D-K2's ROWS ARE IN MG_Pipe/SubsystemDeps.def, ONCE (P3b/P4b R-5), and this comment no
// longer restates them - restating them here is half of how the rule ended up with six
// statements, two of which were wrong. PipeFill.cpp's kMGPipeP4aFamilyDependencies is still
// the client's own copy and is still the bit-for-bit mirror of the four
// `Resolve<Family>SubsystemArm()` refusals in DirectGLES/Managers.cpp; switching it to read
// the .def is the integrator's one-line change (PipeFill.cpp is the contract package's file
// for the whole phase, so the emitter packages do not edit it).
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp drives BOTH readers at every interesting
// mask and compares them against the .def, so the two cannot drift in the meantime.
//
// It is exported for the unit gate and for no other caller: the gate itself is
// FamilyIsLive() inside PipeFill.cpp, every birth hook and every `wants()` row resolves
// through it, and this returns that same expression rather than a second copy of it.
// PipeFill.cpp. Whether this build, backend and MOBILEGL_PIPE_PUSH mask emit for `subsystem`:
// the FamilyIsLive() expression every birth hook and `wants()` row uses, exported only for
// unit tests. It needs the subsystem bit, the family's kMGPipeWired*Subsystem constant
// (`wired`; this header cannot include the emit headers), a backend consumer for the family
// and its dependency bits from MG_Pipe/SubsystemDeps.def. Without the last two the texture
// emitters clear dirty levels on acceptance that nothing then uploads.
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp checks this and Espryt's
// Resolve<Family>SubsystemArm() refusals against the .def.
Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired);
// PipeFill.cpp. DOES AN EMITTED CALL SUPPLY THIS FIELD at the environment named, i.e. would
// the validate point's residual fill SKIP it? This is step 4's own predicate, which P5d
// round 3 (package C) turned from a per-field-per-verb conjunction into a memo keyed on the
// three arguments plus the P4a consumer signal it reads for itself - the profile had it at
// 63 CapsMirror reads per verb for one answer.
//
// IT IS EXPORTED FOR THE UNIT GATE AND FOR NO OTHER CALLER, for MGPipeP4aFamilyEmits'
// reason and one of its own: which fields the fill copies has NO other observable, because
// in a push build every emission this predicate asks about is routed straight into the
// applier, which writes the same storage the fill would have written. So the only way to
// state "the memo's key is complete" - the one thing a memo can get wrong that the
// expression it replaced could not - is to ask it directly. A call re-keys the memo, which
// is exactly what the case is for.
//
// AND A STALE ANSWER IS SILENT, WHICH IS WHY THAT CASE IS THE ONLY GUARD. It is tempting to
// say a wrongly-skipped field aborts as Fatal{UnmigratedPipeInput}; it does not. The walk
// stamps FilledGen from the verb serial whether or not it copied (PipeFill.cpp, step 4), so
// a field the fill skips reads FRESH with the PREVIOUS verb's value - a stale binding slot
// rendered without a word, caught only by the verify lane's comparison. Poison catches an
// UNSTAMPED read, not a stamped-but-uncopied one.
// PipeFill.cpp. Whether an emitted call supplies `field` in this environment, i.e. whether
// the residual fill (step 4) skips it. Memoized on the last three arguments plus what it reads
// itself (the consumer signal, the caps-mirror generation); uncached it cost 63 caps-mirror
// reads per verb. Exported only for unit tests, the sole check that the memo key is complete,
// since emissions write the same storage the fill would. A wrong answer is silent: step 4
// stamps FilledGen whether or not it copied, so a skipped field reads as fresh with the
// previous verb's value. Poison only catches unstamped reads; the verify comparison is the
// other net.
Bool MGPipeResidualFillSuppliesField(MGPipeInputField field, Uint64 pushMask, Bool applierDerives,
Bool contextValuesWireLive);
// PipeFill.cpp. P3a D-H2.1: the DRAW's raw vertex-fetch base instance, which
// set_vertex_buffers now carries as an explicit field.
//
// It replaces an ambient process global the backend read at VAO sync time, which is a
// shape that cannot cross a pushed boundary. The client sends the raw value and never a
// pre-shifted offset: whether to emulate the fetch shift or let GL_EXT_base_instance do
// it is the SERVER's decision. It is also an input to set_vertex_buffers' content hash
// and to the tracker's bit-9 shutter, so a draw whose only change is its base instance
// still reaches the emitter and still goes out.
//
// DO NOT CALL IT DIRECTLY FROM A GL ENTRY POINT - use MGP_SET_BASE_INSTANCE below. This
// whole declaration block is inside #if MOBILEGL_PIPE_PUSH, so a bare call would not even
// compile in a pull build, and the three call sites are in a file that is compiled in
// both. The macro is the same shape MGP_FILL already has, for the same reason.
//
// The validate point consumes and clears it - on both of its exits - and MGPipeLeaveVerb
// clears it too, so a plain draw that follows a base-instanced one sees 0 again. The
// tracker's Reset() deliberately does NOT clear it (Tracker.h): a make-current happens
// BETWEEN the setter and the fill that reads it.
//
// The three GL entry points that make this call (ID-10's grant) are
// MG_Impl/GLImpl/Drawing/GL_Drawing.cpp's DrawElementsInstancedBaseVertexBaseInstance,
// DrawElementsInstancedBaseInstance and DrawArraysInstancedBaseInstance - one line each,
// immediately above the MGP_FILL, carrying the RAW baseinstance argument.
// PipeFill.cpp. The draw's raw vertex-fetch base instance for the next set_vertex_buffers,
// which carries it explicitly; the server decides whether to emulate the fetch shift or use
// GL_EXT_base_instance. It also feeds that record's content hash and the tracker's bit-9
// shutter, so a draw that changes only its base instance still goes out. GL entry points
// call it through MGP_SET_BASE_INSTANCE, one line above MGP_FILL in GL_Drawing.cpp's three
// *BaseInstance draws. The validate point clears it on both exits; the tracker's Reset()
// must not, because Update() resets on a context switch, after the setter and before use.
void MGPipeSetPendingBaseInstance(Uint32 baseInstance);
// What the next set_vertex_buffers will carry. The unit gate reads it to pin that a
// make-current between the setter and the fill does not eat it
+25 -65
View File
@@ -9,32 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's program family: create/bind/delete_shader_state,
// set_draw_program, set_dispatch_program and set_global_constants.
//
// WHERE create_shader_state IS EMITTED FROM, and why it is not the tracker's business: the
// tracker's bit-6 shutter reads GetCurrentProgram() and DELIBERATELY NOT GetProgramForDraw(),
// because the tracker must not force a compile just to answer "did the shader move". So the
// tracker keeps its shutter and the EMITTER joins - from the same GetProgramForDraw() /
// GetProgramForDispatch() call the verb is about to make anyway, so no join happens that would
// not have happened. Emitting from the compile pool's terminal continuation is a real
// asynchronous win and is a LATER phase's: in monolith the applier is one function call away,
// so it is unmeasurable here.
//
// WHAT THE SERVER STILL SPECIALISES, so nobody reads create_shader_state as self-contained
// and produces a per-draw rebuild: the draw-FBO clamp masks, the fragColor broadcast count,
// the storage-block binding signature, the atomic-counter set, the live image formats and the
// patch parameters are all inputs a backend program depends on BEYOND the artefacts. This call
// publishes the ARTEFACTS; the server specialises at the verb from the state it holds. The
// clause count does not shrink - its inputs move.
//
// THE ARTEFACTS DO NOT TRAVEL IN MONOLITH. All seven of MGPProgramDesc's blob refs are
// declared with Size 0 and the LinkArtifacts / SpirvArtifacts ride beside the record through
// MGPipeApplyCreateShaderState's companion pointers, so the codec is never called on the hot
// path; the verify build is where it is exercised.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
// Client side of the program family: create/bind/delete_shader_state, set_draw_program,
// set_dispatch_program and set_global_constants. create_shader_state is emitted from the
// GetProgramForDraw() / GetProgramForDispatch() join the verb makes anyway; the tracker's
// shutter reads GetCurrentProgram() so asking "did the shader move" never forces a compile.
// The record carries the artifacts only; the server specialises at the verb for the draw-FBO
// clamp masks, fragColor broadcast, storage-block bindings, atomic counters, image formats and
// patch parameters (ARCHITECTURE.md §7). In monolith the artifacts ride beside the record as
// companion pointers with Size 0 blob refs, so the codec runs only in the verify build.
#include <MG_Impl/Pipe/CompositeResolver.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -72,24 +54,14 @@ namespace MobileGL::MG_Pipe {
return version != kMGPipeGlobalConstantsNeverUploaded;
}
// A program's identity for the wire, out of the SNAPSHOT the last link consumed and never
// out of the live attach list: glAttachShader and glCompileShader take effect only at the
// NEXT link and neither moves m_linkVersion, so a stage mask built from GetAttachedShaders
// would describe a program that does not exist yet. GetLinkedShaderStages() is also what
// indexes GetGeneratedSpirv(), so the two halves of this descriptor are guaranteed to agree
// by construction rather than by care.
// P5e (pg). ONE ENTRY OF THE STORAGE-OVERRIDE SIGNATURE, and it is a VERBATIM copy of the
// mix in `MG_Backend/DirectGLES/Managers.cpp`'s ComputeShaderStorageBlockBindingSignature -
// the function whose answer this record replaces on the handle arm. The two halves cannot
// share one definition today because MG_Backend includes nothing from MG_Impl (checked at
// this head), so they are twins with a named pointer at each other and a unit case in
// ProgramEmitTest that pins the number. Order-independent by construction: the source is an
// UnorderedMap, the combine is commutative, and the entry mixes name and binding so two
// entries cannot trade halves and cancel out.
//
// OVER THE VALUES, NOT OVER A CHANGE COUNTER, which is the property the pipeline composite's
// uniform mirror depends on: it re-sets every override every draw, and re-setting a block to
// the binding it already carries must produce the same signature and force no rebuild.
// One entry of the storage-override signature (MGPProgramBindings::Signature), the one
// number the server stamps on a program twin and compares on the draw path (DirectGLES'
// ComputeShaderStorageBlockBindingSignature takes it verbatim). The entry mixes name with
// binding so two entries cannot trade halves; entries are summed, so the UnorderedMap's order
// cannot matter. Over values, not a change counter: re-setting a block to its current binding
// (the pipeline composite's uniform mirror does so every draw) forces no rebuild.
// MGPipeStageMaskOf below reads the last link's stages, not the attach list: attach and
// compile take effect only at the next link, and the same stages index GetGeneratedSpirv().
inline Uint64 MGPipeStorageOverrideSignatureEntry(const String& blockName, Int binding) {
Uint64 entry = std::hash<String>{}(blockName);
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
@@ -135,7 +107,7 @@ namespace MobileGL::MG_Pipe {
// published ahead of it are wiped by the create that follows. What the draft was
// protecting - "a rebuild inside the verb must already see the bindings" - is
// satisfied by both records going out at the SAME validate point, ahead of the verb,
// which is what this ordering does. See MG_Remote/CONTRACT-P5E.md §5.5 and the
// which is what this ordering does. See docs/archive/contracts/CONTRACT-P5E.md §5.5 and the
// record's own comment in PipeApply.h.
if (drawProgram) bytes += EmitProgramBindings(*drawProgram, drawCso);
// THE COMPOSITE'S SECOND RELEASE PATH is spoken here, not in a destructor: when the
@@ -281,26 +253,14 @@ namespace MobileGL::MG_Pipe {
return bytes + sizeof(MGPGlobalConstants) + size;
}
// ---- P5e (pg): set_program_bindings, opcode 80 -----------------------------------
//
// THE THREE POST-LINK MUTABLE REFLECTION FIELDS, and why an archive alone is not an
// answer. `uniformBlockBinding[i]`, `uniformSamplerOrImageUnitIndex[loc]` and the
// name-keyed `shaderStorageBlockBinding` map are all MEMBERS OF LinkArtifacts - so the
// archive create_shader_state carries does hold them - but glUniformBlockBinding,
// glUniform1i on a sampler uniform and glShaderStorageBlockBinding all move them AFTER
// the link that produced that archive, without relinking. A server answering a draw
// from the archive alone would bind the uniform blocks the program was LINKED with
// rather than the ones it is BOUND with. This record is the delta carrier; the applier
// overlays it on the archive, and a re-issued create drops it because the indices no
// longer mean anything.
//
// LATCHED ON THE TWO COUNTERS THAT MOVE WHEN ANY OF THE THREE DOES, plus the handle:
// m_backendStateVersion (glUniformBlockBinding and glUniform1i both bump it) and
// m_blockBindingVersion (glUniformBlockBinding and glShaderStorageBlockBinding both
// bump it, and the storage setter deliberately bumps ONLY it). Neither setter moves a
// counter on an unchanged value - both have an equality bail-out - so an application
// that re-sets the same bindings every frame emits nothing, which is what the pipeline
// composite's uniform mirror needs (it replays every override per draw).
// ---- set_program_bindings ----
// glUniformBlockBinding, glUniform1i on a sampler uniform and glShaderStorageBlockBinding
// change LinkArtifacts' block bindings, sampler/image unit indices and storage-block map
// after the link that produced the archive, without relinking. This record carries those
// values; the applier overlays it on the archive, and a re-issued create drops it.
// Latched on the handle plus m_backendStateVersion and m_blockBindingVersion, which
// together move on any of the three setters but not on an unchanged value, so re-setting
// the same bindings every draw (the pipeline composite's uniform mirror) emits nothing.
Uint64 EmitProgramBindings(const ProgramObject& program, MGPipeHandle cso) {
if (MGPipeHandleIsNull(cso)) return 0;
const Uint32 backendStateVersion = program.GetBackendStateVersion();
+37 -126
View File
@@ -9,35 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P3a's resource family (brief D-A, D-B, D-C, D-D).
//
// WHERE IT RUNS, and it is the ONE exception to push-at-validate (ARCHITECTURE.md 5.1):
// the seven BufferBackendOps hooks already dispatch at the GL call that causes them, so
// their pipe calls are emitted from the same BufferObject dispatchers - not from
// MGPipeValidateForVerb. Nothing about buffers moves to validate time in P3a.
//
// WHAT LIVES HERE
// * the sticky BindMask, one constexpr BufferTarget -> bit table with a static_assert
// that it covers every enumerator, so a new target cannot be silently unmapped;
// * the lifetimeId -> {slot, gen} mint (through MGPipeSlots(), the one allocator) and
// the slot -> BufferObject* INVERSE the reverse channel resolves a writeback through;
// * the nine MGPipeEmitResource* bodies, declared in MG_Pipe/PipeMutation.h so that
// MG_State sees a declaration and never this file (the same layering PipeMutation.h
// already has for MGP_NOTE_MUTATION: declare in MG_Pipe, define in MG_Impl);
// * the MGPSubData range splitter, because one record's box caps the destination at a
// 2^31-1 offset and a 2^32-1 size;
// * the map-persistent-roundtrips counting site.
//
// HEADER-ONLY, for the ownership reason Tracker.h states in full: the root CMakeLists.txt
// that would name a new .cpp belongs to the contract package and is frozen behind the tag.
// MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
//
// NO TIMER, and no per-call record copy on a HOT path. The two observables a unit case
// needs - the last emitted descriptor and the per-call counts - are written only by
// resource_create and resource_respecify, which run once per glBufferData rather than per
// 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.
// Client side of the buffer resource family. Its records go out at the GL call that causes the
// change, from the BufferObject dispatchers that already run the BufferBackendOps hooks, not at
// the validate point (ARCHITECTURE.md §5.1). This file holds the sticky BindMask table, the pure
// record builders and the MGPSubData range splitter, the lifetime id -> {slot, gen} mint, the
// slot -> BufferObject* inverse that writebacks resolve through, and the reverse-channel
// consumers. The MGPipeEmitResource* bodies are in PipeFill.cpp and declared in
// MG_Pipe/PipeMutation.h, so MG_State never includes this file. resource_subdata, the hot path,
// records no test observables; tests drive MGPipeBuildSubDataRecord and the splitter directly.
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
@@ -195,24 +174,13 @@ namespace MobileGL::MG_Pipe {
return desc;
}
// The buffer half of MGPSubData: the destination range rides in the box's first
// coordinate and first extent, and MGPipeSetSubDataBufferRange is the ONLY spelling of
// that convention. Returns false, with the record untouched, when the range does not fit
// one record - which is where MGPipeForEachSubDataRecordRange comes in.
//
// `sourceIsVerbatimLevelShadow` is the record's own question - "are these bytes an
// untransformed level shadow?" - and it is a PARAMETER because the answer differs by
// caller: resource_subdata hands over the client's own shadow at an offset into it and
// says yes; buffer_subdata_resident hands over the application's staging store, or the
// locally expanded pattern FillSubData built, and both say no. Nothing reads it on the
// buffer path today, which is exactly why it must not be a hard-coded 1 that becomes
// wrong the moment something does.
//
// Blob is FILLED, exactly: Seg is kMGHostSpanSegNone (monolith - the bytes travel beside
// the record through the entry point's companion pointer) and Size is the piece's own
// byte length, which is what the applier's ONE Blob rule holds a non-zero declaration to
// (PipeApply.cpp's SubDataBoxFault: != 0 && != MGPipeSubDataBufferSize is refused).
// Leaving it 0 would be legal too; declaring it correctly is the stronger of the two.
// The buffer half of MGPSubData: the destination range rides in the box's first coordinate
// and extent, written only through MGPipeSetSubDataBufferRange. Returns false when the range
// does not fit one record (see MGPipeForEachSubDataRecordRange). The verbatim-shadow flag is
// a parameter because callers differ: resource_subdata passes the client's own shadow (true),
// buffer_subdata_resident a staging store or a FillSubData pattern (false). Blob.Seg is
// kMGHostSpanSegNone (the bytes travel through the entry point's companion pointer) and
// Blob.Size is the piece's byte length, the only non-zero size SubDataBoxFault accepts.
inline Bool MGPipeBuildSubDataRecord(MGPipeHandle res, Uint64 offset, Uint64 size, MGPSubData& out,
Bool sourceIsVerbatimLevelShadow) {
out = MGPSubData{};
@@ -372,37 +340,14 @@ namespace MobileGL::MG_Pipe {
m_bySlot[slot].BindMask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
}
// Accumulates into the sticky mask every target `buffer` is bound to RIGHT NOW, and
// returns the accumulated value.
//
// [DEVIATION, recorded in client-v2.md] D-A3 asks for the OR at every glBindBuffer /
// glBindBufferBase / glBindBufferRange / VAO element-slot bind, and C.1 points at
// MG_State/GLState/BufferState/BufferState.{h,cpp} for it - a file this package DOES
// own. The brief is wrong about where the entry points are: BufferState only VENDS
// BindingSlot<BufferObject>& / BindingSlotRange1D&, and the .Bind() calls are
// MG_Impl/GLImpl/Buffer/GL_Buffer.cpp's (BindBuffer_State, BindBufferBase_State,
// BindBufferRange_State), which C.5 assigns to no package. So the mask is accumulated
// by SAMPLING the frontend's live binding state instead - here, at every create and
// respecify, which is where the value is PUBLISHED - and ORed into a per-slot sticky
// field that is never cleared.
//
// WHAT SAMPLING ALONE CANNOT SEE is not "a bind after the last respecify" (which the
// specified design misses too) but a TRANSIENT bind: bind an EBO, draw, unbind, then
// define it through DSA - the respecify's sample sees no binding at all, and the DSA
// idiom makes that the common case rather than a corner (TryAdoptLargeStorage's own
// comment names glNamedBufferSubData as what MC 26.3 streams with). That hole is
// closed for the two bits anything keys on by NoteBoundAs above, called from
// EmitVertexBuffers / EmitIndexBuffer at every draw. What is left unpublished is a
// buffer that is bound, never drawn from, and never re-specified afterwards; the
// remaining fix is one line in each of GL_Buffer.cpp's three *_State binders, for the
// seven bits nothing keys on yet, and it stays handed to whoever owns that file.
//
// The scan is skipped unless a binding-slot version moved since the last one, which
// is one Uint16 load per global target and none per binding point. It is NOT called
// from the content emitters, deliberately: it walks the whole context's binding state
// and writes the tracker, and one of those emitters (resource_subdata) is on the path
// D-A2 preserves as reachable off the render thread. Extra sampling could only widen
// a sticky union, but not at the price of a context-wide read from the wrong thread.
// ORs into the sticky mask every target `buffer` is bound to right now and returns the
// result. The bind entry points (GL_Buffer.cpp's *_State binders) do not record targets,
// so the mask is sampled here, at create and respecify where it is published. Sampling
// misses a transient bind (bind, draw, unbind, then define through DSA); NoteBoundAs at
// every draw covers the vertex and element-array bits, but other bits of a buffer never
// drawn from or respecified after its bind stay unpublished. The scan runs only when
// BindEpoch moved, and never from the content emitters: it reads the whole context, and
// resource_subdata can run off the render thread.
Uint16 RefreshBindMask(GLContext& ctx, const BufferObject& buffer, MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (slot >= m_bySlot.size()) return 0;
@@ -461,34 +406,13 @@ namespace MobileGL::MG_Pipe {
void NoteDestroy() { ++m_destroys; }
void NoteMapPersistent() { ++m_mapPersistents; }
// A unit fixture's per-case reset, and the library never calls it. THE RULE, stated
// rather than left as an absence, because "nothing resets this" is not a reason:
//
// A buffer handle and the applier record it names are SHARE-GROUP OBJECT STATE.
// A GL object lives in a share group, not in a context, so a make-current changes
// neither. The applier's MGPipeApplierReset() is a make-current and deliberately
// keeps its Resources / VertexElementsCsos (PipeApply.h says so beside them); the
// ONLY things that drop a record are the object's own death signal -
// resource_destroy, which ~BufferObject raises through
// MGPipeEmitResourceDestroyAndFree, and delete_vertex_elements - and
// MGPipeApplierReleaseObjectRecords(), which is the SERVED CONTEXT's teardown and
// is deliberately wired to nothing in the monolith (there is one applier behind
// every context, so calling it on one context's destruction would drop every other
// context's records).
//
// So this tracker needs no re-publication path on a fresh context and must not have
// one: re-emitting resource_create for a record the applier still holds would move
// its Serial for nothing. What the client owes instead is the destroy - which
// ~BufferObject already emits, in the fixed emit-then-free order (D-L) - and that is
// the whole of the client's side of the record lifecycle.
//
// The vertex-input emitter's latches are the OTHER half and are genuinely per
// context: MGPipeVertexInputEmitter::Reset() is called from the FreshlyPrimed arm
// because the applier's vertex-input WORKING state (the bound handle, the window, the
// fetch shift) IS cleared there. Its vertex-elements RECORDS are not, which is why
// the emitter's Reset drops the "already published" latches but no create is lost:
// the latch is what says "re-publish", and re-publishing an unchanged configuration
// is a bounded over-fire, not a dropped write.
// Per-case reset for unit fixtures; the library never calls it. Buffer handles and the
// applier records they name are share-group state: MGPipeApplierReset (a make-current)
// keeps them, and only the object's own death (resource_destroy from ~BufferObject,
// delete_vertex_elements) or MGPipeApplierReleaseObjectRecords drops one. So this tracker
// has no re-publication path on a fresh context; a second resource_create would move the
// record's Serial for nothing. Per-context latches live in the emitters instead, such as
// MGPipeVertexInputEmitter::Reset(), which the fresh-context path calls.
void ResetForTest() {
m_bySlot.clear();
m_bindEpoch = 0;
@@ -569,26 +493,13 @@ namespace MobileGL::MG_Pipe {
// D-D: the client's half of the reverse channel
// ---------------------------------------------------------------------------------
// The backend produced the bytes of a readback and hands them back through the channel.
// The client resolves the handle to its own object and writes the shadow; the epoch bump
// stays SERVER-side and happens AFTER this returns, never before (ARCHITECTURE.md 7.4:
// the reverse channel needs the same ordering guarantee as the forward one).
//
// THE BLOBREF'S THREE ARMS (CONTRACT-P5C §4.3, replacing the P3a monolith guard that
// rejected every Seg != kMGHostSpanSegNone and would have dropped every writeback EVENT
// on arrival):
// Seg == kSegEvent -> the wire shape (CONTRACT-P5C §1's reverse-channel row):
// Offset is the byte offset of the inline payload inside
// SEG_EVENT, resolved through the client session's OWN
// SegmentTable, bounds-checked against the announced size;
// Seg == kMGHostSpanSegNone -> monolith only: Offset IS the backend's mapped address
// (MGPipeTypes.h says so in as many words). With an active
// transport this is rule B in the reverse direction and is
// Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"};
// anything else -> the same Fatal.
//
// `producedAtSeq` is the event's ProducedAtSeq (the record the server read the bytes back
// at); 0 for the monolith callback, whose answer is synchronous with its request.
// Writes the bytes of a backend readback into the client's shadow of the buffer `res` names.
// The server bumps its epoch only after this returns (ARCHITECTURE.md §8.2). With
// Seg == kSegEvent, `bytes` is an offset into SEG_EVENT, resolved and bounds-checked through
// the client session; with Seg == kMGHostSpanSegNone it is a backend address, legal only in
// the monolith. Anything else, or a host address under an active transport, is
// Fatal{ProtocolCorruption} (a build without a transport drops it with an error).
// `producedAtSeq` is the record the server read the bytes back at, 0 for the monolith.
inline void MGPipeClientOnBufferWritebackAt(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes,
Uint64 producedAtSeq) {
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
+57 -220
View File
@@ -9,34 +9,13 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's sampler family: the content-addressed sampler CSO cache, the
// identity-addressed sampler view per texture object, and the two unit sets
// set_sampler_views and bind_sampler_states. The third unit set, set_shader_images, is
// ImageEmit.h's - the same subsystem bit, a different resolution.
//
// TWO THINGS THIS FILE OWNS THAT ARE EASY TO GET WRONG, both stated where the body will go:
// * SamplerParameters is 100 bytes with THREE BYTES OF TRAILING PADDING, so the CSO cache
// hashes and memcmp-confirms over a ZERO-INITIALISED canonical copy built field by field,
// never over the object's own bytes. Without that the 256-entry cache's hit rate is zero
// and nobody notices, because the pixels are right.
// * every emission goes through a VERSION-FIRST SKIP before it hashes anything: the sampler
// view latches (params version, shape version) per handle, and the two sets latch their
// SetHashSuppressor slots. A 192-entry walk per verb without a latch is not affordable.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full. kMGPipeWiredSamplerSubsystem below covers this file AND
// ImageEmit.h: the three unit sets, the sampler CSO and the sampler view are ONE family and
// one subsystem bit, because an operator switching samplers off has to get the whole family's
// legacy arm rather than two thirds of it.
//
// WHAT THIS FILE IS THE CLIENT HALF OF, named so a reader can check it against the oracle:
// DirectGLES' ResolveAndBindUnitTextures (the per-unit sampler-view resolution),
// BindCurrentUnitSamplers (the per-unit sampler-object walk) and the program pass' sampler
// override. TWO BACKEND POST-PROCESSINGS DELIBERATELY STAY ON THE SERVER and act on the
// RESOLVED set: Espryt's raw-depth-fetch sampler substitution and Magma's feedback-loop
// detection. Neither is reproduced here and neither may be.
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
// Client side of the sampler family: the content-addressed sampler CSO cache, one sampler view
// per texture object (addressed by identity), and the unit sets set_sampler_views and
// bind_sampler_states. set_shader_images is ImageEmit.h's, under the same subsystem bit.
// It is the client half of DirectGLES' ResolveAndBindUnitTextures, BindCurrentUnitSamplers
// and the program pass' sampler override (ARCHITECTURE.md §5.5). Espryt's raw-depth-fetch
// sampler substitution and Magma's feedback-loop detection act on the resolved set and stay on
// the server. Every emitter checks version latches before hashing anything.
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -59,60 +38,23 @@
namespace MobileGL::MG_Pipe {
// WIRED. The sampler CSO, the sampler view and all three unit sets - set_shader_images
// included, whose emitter lives in ImageEmit.h - have bodies, so this file's family
// contributes its bit to kMGPipeWiredSubsystems. ONE bit for the whole family, because an
// operator switching samplers off has to get the whole family's legacy arm rather than two
// thirds of it.
//
// WHAT THE BIT DOES, and it really is a gate - c0b changed this and the note that used to
// say the opposite was true only against the contract commit. The validate point's
// `wants()` (PipeFill.cpp) now asks FOUR things: the bit maps to a subsystem, the
// operator's MOBILEGL_PIPE_PUSH mask carries it, `kMGPipeWiredSubsystems` carries it -
// i.e. this constant is non-zero - and the dirty bit fired. The birth hooks' own gate
// (`FamilyIsLive`) is the same pair one level in, so the client paths and the validate
// point cannot disagree. So an emitter with a body and this constant still 0 is called by
// nothing and emits nothing, and flipping the constant is what switches the family on.
//
// IT IS ALSO A COMPILE-TIME CONTRACT. While it is non-zero, PipeFill.cpp's `if constexpr`
// seam instantiates the forward to this family's entry points - EmitSamplerCso and
// EmitSamplerView below - so a missing or misspelled one is a build error in this file's
// own commit rather than a surprise at the merge.
//
// The A/B that switches this family off at RUNTIME is still the mask; this constant is
// what a build declares it emits for, and it feeds EmittedCallSuppliesTheWholeField's
// guard. ONE bit for the whole family, for the reason above.
// The subsystem bit this build emits for, covering the sampler CSO, the sampler view and all
// three unit sets (set_shader_images in ImageEmit.h included): one bit, so switching samplers
// off returns the whole family to the legacy path. PipeFill.cpp ORs it into
// kMGPipeWiredSubsystems, which the validate point's `wants()` tests; the birth hooks'
// FamilyIsLive tests this constant. While it is non-zero the ForwardWhenWired seam calls
// EmitSamplerCso / EmitSamplerView, so a misspelled entry point fails to build. The runtime
// switch is the MOBILEGL_PIPE_PUSH mask. Set it together with kMGPipeWiredTextureSubsystem.
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = kMGPipeSubsystemSamplers;
// ---------------------------------------------------------------------------------
// D-F1: the canonical SamplerParameters copy, and why it is not a memcpy
// ---------------------------------------------------------------------------------
//
// sizeof(SamplerParameters) == 100 and its members occupy 97 of those bytes: six 4-byte
// enums, four Floats, two more 4-byte enums, three 16-byte colour vectors and the one-byte
// borderColorForm. Bytes 97, 98 and 99 are PADDING and no writer ever touches them.
//
// A cache that hashed or memcmp'd the object's own bytes would therefore read
// uninitialised memory. In practice SamplerObject value-initialises its member and never
// rewrites it, so in practice those bytes are stable - and "in practice" is not a
// contract. The whole point of a content-addressed cache is that a false MISS mints a
// fresh CSO per call: a 256-entry cache with a hit rate of zero, on a path nobody looks
// at, because the pixels are right either way.
//
// So every hash and every confirm runs over a copy that is memset to zero FIRST and then
// assigned field by field, which makes the padding deterministically zero on both sides of
// the comparison. The field list is MGP_FIELDS_SamplerParameters', in its order, and the
// verify build compares the same sixteen fields one at a time - without that
// PipeFields.def row the blob would be compared as bytes and G4 would be a coin flip.
//
// ONE COPY PER MINT ATTEMPT, never per draw: the version-first skip in the two emitters
// below decides whether to come here at all.
// THE PRIMITIVE TAKES AN OUT-PARAMETER, and that is not a style preference either. A
// returned SamplerParameters is copied, and a copy of a trivially copyable type leaves the
// padding UNSPECIFIED - so a canonicaliser that returned by value would hand its caller a
// value whose three trailing bytes are whatever the copy left there, which is the very
// thing this function exists to make deterministic. Every producer of canonical bytes in
// this file, and every consumer that stores them, goes through this and through memcpy.
// ---- Canonical SamplerParameters copy ----
// SamplerParameters is 100 bytes with 3 bytes of trailing padding no writer touches, so the
// CSO cache never hashes or compares an object's own bytes: a padding mismatch is a silent
// false miss (correct pixels, a fresh CSO per call). The copy is memset to zero, then
// assigned field by field in MGP_FIELDS_SamplerParameters' order. It is an out-parameter
// because returning by value copies and leaves the padding unspecified; canonical bytes are
// stored only with memcpy. It runs once per mint attempt; the emitters' version checks keep
// it off the per-draw path.
inline void MGPipeCanonicaliseSamplerParameters(const SamplerParameters& src, SamplerParameters& canon) {
static_assert(std::is_trivially_copyable_v<SamplerParameters>,
"the canonical copy is memset and then assigned field by field");
@@ -151,59 +93,13 @@ namespace MobileGL::MG_Pipe {
return MGPipeHashSamplerParameters(canon);
}
// ---------------------------------------------------------------------------------
// D-F1: the content-addressed sampler CSO cache, capacity 256
// ---------------------------------------------------------------------------------
//
// Unlike P3a's vertex-elements CSO, content addressing is RIGHT here: a SamplerObject is a
// pure 100-byte value with no driver-side per-object binding state, two identical samplers
// can share one CSO with no extra work on either side, and Espryt's BackendSamplerObject
// is a driver name plus a parameter shadow - nothing a second frontend object would have
// to re-establish.
//
// THE LOOKUP IS CsoCache.h's, one for one: hash, probe, and CONFIRM WITH A MEMCMP before
// reusing a handle, because a bare 64-bit equality would alias two different sampler
// states onto one CSO and that is silent wrong filtering with no gate that can see it.
//
// WHAT KEEPS A LIVE HANDLE OUT OF THE LRU's REACH IS A REFERENCE COUNT, not the capacity
// (ID-17). The capacity argument - "one pass acquires at most kMGPipeMaxTextureUnits == 192
// CSOs and touches every one of them, so LRU can only evict an entry from an EARLIER pass"
// - closes the INTRA-PASS case only, and the case that matters is the other one: a
// MGPTextureParams record published in an earlier pass names its texture's BuiltinSampler
// for as long as that texture's parameters do not move, the applier deliberately does not
// resolve that handle, and an eviction is not a parameter change, so nothing re-emits and
// nobody refuses. With more than 256 distinct live sampler values - a CTS sampler sweep, a
// scene with many filter/wrap/border combinations - the rarely-touched built-in samplers
// are the FIRST victims.
//
// So every Acquire takes a REFERENCE and Release drops one; LRU considers only entries
// whose count is 0; and when every entry is pinned the cache MINTS BEYOND CAPACITY and
// counts it (Counters::OverCapacityMints). Growing is the safe direction - a slot too many
// costs memory, a handle pulled out from under a live record costs correctness - and the
// count is a recorded number rather than a gate, so a workload that pins more than 256
// values is visible instead of being wrong.
//
// WHO HOLDS A REFERENCE: package B's EmitTextureParams for a texture's built-in sampler
// (released when the texture dies or when a parameter change makes it re-acquire), and
// this file's own bind_sampler_states for every CSO a unit is currently bound to. Anything
// that acquires a handle it will still name after it returns must hold one.
//
// THE CAPACITY ITSELF IS STILL A SIZING BOUND worth asserting: a cache that could not hold
// one whole emission pass would over-capacity-mint on every single pass, which is a
// silently unbounded cache rather than a 256-entry one.
//
// THE SLOT IS ALLOCATED WITHOUT A LIFETIME ID, deliberately: a content-addressed CSO
// belongs to a VALUE and not to a frontend object, so ~SamplerObject must not free it -
// another live SamplerObject may hold the same value. MGPipeEmitSamplerCsoDestroyAndFree
// resolves nothing for such an id and correctly frees nothing; the only death path for
// these slots is the LRU eviction below, which is client-side and therefore
// backend-neutral on day one.
//
// PACKAGE D MUST BE TOLD, and it is the one consequence of that choice that reaches the
// server: NotifyStateObjectDestroyed(SamplerCso, lifetimeId) arrives from ~SamplerObject
// with NO HANDLE BEHIND IT, every time. A backend must NOT key a sampler twin on a
// SamplerObject's lifetime id. The twin's life is create_sampler_state -> the LRU's
// delete_sampler_state and nothing else.
// ---- Content-addressed sampler CSO cache ----
// A SamplerObject is a pure value with no per-object driver binding state, so equal samplers
// share one CSO. Lookup is CsoCache.h's: hash, probe, then memcmp before reusing a handle.
// Eviction safety comes from reference counts, not capacity: LRU evicts only unreferenced
// entries and the cache mints past capacity when all are pinned. Slots carry no lifetime id,
// so ~SamplerObject frees nothing and a backend must not key a sampler twin on a
// SamplerObject's lifetime id. Holders and the full rule: ARCHITECTURE.md §2.3.1.
inline constexpr SizeT kMGPipeSamplerCsoCacheCapacity = 256;
static_assert(kMGPipeSamplerCsoCacheCapacity > kMGPipeMaxTextureUnits,
"a cache that cannot hold one whole emission pass would mint over capacity on "
@@ -273,24 +169,13 @@ namespace MobileGL::MG_Pipe {
Uint64 GroupPublishes = 0;
};
// The handle for `params`' value, AND A REFERENCE ON IT. Mints and emits
// create_sampler_state on a miss and emits delete_sampler_state for whatever it evicts
// to make room. `payloadBytes` accumulates what went on the wire.
//
// EVERY Acquire TAKES A REFERENCE and every caller owes exactly one Release for it
// (ID-17). A caller that only wants a transient handle still has to release it, and a
// caller that keeps naming the handle in a published record must hold its reference for
// as long as that record stands - that is what stops the LRU pulling the handle out
// from under it, because an eviction is not a parameter change and nothing re-emits.
//
// THIS IS ALSO PACKAGE B's SEAM. MGPTextureParams::BuiltinSampler names the CSO that
// carries the SamplerParameters of the SamplerObject every ITextureObject owns, and a
// null handle there is Fatal{ProtocolCorruption} rather than "no sampler". TextureEmit.h
// acquires it from here - taking a reference it holds until the texture dies or a
// parameter change makes it re-acquire, at which point it releases the previous handle -
// so the texture's built-in sampler and a glBindSampler'd sampler object with the same
// value share one CSO and one server-side twin, which is exactly the sharing that makes
// content addressing the right answer for this kind.
// The handle for `params`' value, with a reference on it. On a miss it mints and emits
// create_sampler_state, and emits delete_sampler_state for any entry it evicts;
// `payloadBytes` accumulates the wire bytes. Every Acquire owes exactly one Release, and
// a caller that names the handle in a published record holds it for as long as the
// record stands. TextureEmit.h acquires MGPTextureParams::BuiltinSampler here (a null
// handle there is Fatal{ProtocolCorruption}), so a texture's built-in sampler and a
// bound sampler object with the same value share one CSO and one server twin.
MGPipeHandle Acquire(const SamplerParameters& params, Uint64& payloadBytes) {
SamplerParameters canon;
MGPipeCanonicaliseSamplerParameters(params, canon);
@@ -805,22 +690,13 @@ namespace MobileGL::MG_Pipe {
using ITextureObject = MG_State::GLState::ITextureObject;
using SamplerObject = MG_State::GLState::SamplerObject;
// set_sampler_views: the PROGRAM-RESOLVED set only, one entry per unit, no stage
// dimension. Start is 0 and Count is GetMaxTouchedTextureUnit() + 1 clamped to the
// wire bound - the high-water mark is directly the count argument and is not
// re-derived.
//
// WHAT A UNIT'S ENTRY MEANS, and all three cases are legal rather than holes:
// the program resolves no target here -> View and Texture both null
// it resolves one, nothing is bound -> View and Texture both null
// it resolves one and a texture is bound-> Texture is that object's handle and View
// is its sampler-view CSO
// An UNDEFINED DEFAULT texture (name 0 with no image) and a texture that
// SAMPLES AS INCOMPLETE are both dropped to null here, which is the resolution
// DirectGLES performs by leaving the native target unbound: an incomplete texture
// samples (0,0,0,1) and the driver cannot work that out for itself, because the
// backend storage is immutable and never saw the level the application redefined at
// the wrong size.
// set_sampler_views: the program-resolved set, one entry per unit, no stage dimension.
// Start is 0; Count is GetMaxTouchedTextureUnit() + 1, clamped to the wire bound. An
// entry is null when the program samples nothing at the unit, nothing is bound, the
// bound texture is the undefined default texture, or it samples as incomplete;
// otherwise it names the texture and its sampler-view CSO. Nulling an incomplete
// texture matches DirectGLES leaving the native target unbound: it must sample
// (0,0,0,1), and the backend's immutable storage never saw the redefined level.
Uint64 EmitSamplerViews(GLContext& ctx) {
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
const Uint32 count =
@@ -886,38 +762,14 @@ namespace MobileGL::MG_Pipe {
return bytes + sizeof(MGPSamplerViews) + static_cast<Uint64>(count) * sizeof(MGPBoundView);
}
// bind_sampler_states: the unit's sampler CSO, or the null handle when the unit has no
// sampler object - the texture's built-in sampler then applies, exactly as today.
//
// NOT program-resolved, and that asymmetry with the view set above is deliberate: a
// sampler object is bound to a UNIT and applies to whatever the unit holds, so its set
// is the plain per-unit walk BindCurrentUnitSamplers already does. A redundant re-bind
// of the same sampler object emits nothing at all, which is the whole point of the
// suppressor slot: GetTextureBindGeneration() bumps on a redundant re-bind - 26.2
// rebinds the same sampler at every texture-unit switch - so without it this would be
// a several-hundred-byte variable-length record per batch.
// A BOUND SAMPLER STATE HOLDS A REFERENCE ON ITS CSO FOR AS LONG AS IT IS BOUND
// (ID-17). The set the applier holds is "the last set as received" and outlives the
// pass that sent it, so a handle in it must not become the LRU's victim; the reference
// is what makes that true. The reconciliation below walks the unit array once: Acquire
// has already taken a reference for every unit in the new set, so a unit whose handle
// did not move gives that duplicate back, a unit whose handle moved gives back the one
// it used to hold, and a unit that fell outside the window gives back its own.
//
// ITS COST IS NOT "ONE EXTRA ARRAY WALK" and the ID-19 budget entry says so. Each
// Release of a NON-NULL handle is itself a linear scan of the whole cache, sitting
// beside an Acquire probe that is another, so a pass in which K units hold sampler CSOs
// costs 2 * K * O(cacheSize) handle comparisons - up to ~98 000 at K == 192 with a full
// cache, and unbounded above that because the over-capacity path lets the cache grow
// past 256. In Minecraft-shaped workloads K is typically 0: no unit has a SamplerObject
// bound, so Acquire is never called and Release(null) returns immediately. The workload
// that has both a large K and a large cache is a CTS sampler sweep. c0d moved the
// firing rate underneath this as well - bit 13's shutter now mixes
// GetTextureBindGeneration(), so this runs on EVERY texture bind at any unit and not
// only on a parameter change, and the set-hash suppressor below stops the record going
// out but not these two scans, which happen first. The per-unit (sampler lifetime id,
// ctx.GetSamplingResolutionGeneration()) latch recorded under ID-19 as C-m2 removes
// BOTH scans, not one.
// bind_sampler_states: each unit's sampler CSO, or null when the unit has no sampler
// object and the texture's built-in sampler applies. Not program-resolved: a sampler
// object applies to whatever its unit holds, as in BindCurrentUnitSamplers. A bound
// handle keeps a cache reference while bound, because the applier's set outlives this
// pass. Cost: Acquire and each non-null Release scan the whole cache, 2 * K * cache size
// per pass for K units with sampler objects (usually 0 in Minecraft, large in CTS sampler
// sweeps), on every texture bind; the set-hash suppressor skips only the record. A
// per-unit (sampler lifetime id, sampling resolution generation) latch would avoid both.
Uint64 EmitSamplerStates(GLContext& ctx) {
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
const Uint32 count =
@@ -1028,27 +880,12 @@ namespace MobileGL::MG_Pipe {
return handle;
}
// ---- THE TWO CONTRACT ENTRY POINTS THIS FAMILY OWES (contract-v2 §3.4) ----
//
// PipeFill.cpp's birth hooks forward here through an `if constexpr` seam keyed on
// kMGPipeWiredSamplerSubsystem, so while that constant is non-zero these two must
// exist and be spelled exactly like this or the build fails in this file's own commit.
// The hook has already applied BOTH gates (the operator's mask and the wired constant);
// what belongs here is the family's HANDLE RULE and its publication, which the contract
// deliberately does not pick for us because the three families disagree about identity.
//
// MINTING A SAMPLER CSO AT CONSTRUCTION TIME CONTENT-ADDRESSES THE DEFAULT PARAMETERS,
// and that is correct rather than wasteful: the cache dedupes, so a thousand
// freshly-created SamplerObjects share ONE create_sampler_state, and the moment the
// application sets a parameter the next acquire content-addresses the new value and
// this object simply stops naming the default entry. It is one create_sampler_state per
// distinct default-valued sampler, not one per object.
//
// THE REFERENCE THIS TAKES IS DROPPED IMMEDIATELY. A birth is not a standing record: no
// MGPTextureParams and no bind_sampler_states names this handle yet, so pinning the
// entry here would keep the default-parameter value un-evictable for the life of the
// process for no reader's benefit. Whoever later names the handle in a record takes its
// own reference through its own Acquire.
// ---- Birth hooks, forwarded from PipeFill.cpp ----
// ForwardWhenWired calls these after FamilyIsLive passes, so the names must match.
// EmitSamplerCso content-addresses the new object's default parameters, so all
// default-valued samplers share one create_sampler_state. Its reference is released at
// once: a birth names the handle in no record, and pinning it would keep the default
// value unevictable; whoever later names the handle takes its own reference.
void EmitSamplerCso(SamplerObject& sampler) {
Uint64 bytes = 0;
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
+8 -29
View File
@@ -9,34 +9,13 @@
#pragma once
#include <Includes.h>
// Coalescing rule 4 (ARCHITECTURE.md 5.4, P2 brief D11): every kVarTail set_* hashes the
// RESOLVED set on the client and does not emit when the hash has not moved.
//
// This is the carrier for the ~175 lines of debounce that move off the backends in P3b and
// P4b - Espryt's UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and
// Magma's equivalents all answer "is this set the same set as last time", and every one of
// them answers it against a shape the backend rediscovered. P2 lands the MECHANISM and ONE
// real consumer (SetVertexAttribDefaults) so the shape is pinned by a test rather than by a
// plan; the other six slots exist, are unit-tested, and are wired by the phase that moves
// the set they name. P3a wires the second, SetVertexBuffers. P4a wires SetSamplerViews,
// BindSamplerStates and SetShaderImages, and APPENDS an eighth slot, SetFramebufferState -
// which leaves only SetShaderBuffers and SetStreamOutputTargets unwired, both P4b's.
//
// A WIRED SLOT PUTS A REQUIREMENT ON ITS HASH, and SetVertexBuffers is where that first
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
// baseInstance is DRAW state and moves without the buffer set moving, so a hash over the
// entries alone would suppress a record whose one changed field is the fetch shift and the
// server would keep the previous one. MG_Impl/Pipe/VertexInputEmit.h's
// MGPipeVertexBufferSetContentHash mixes Start, Count and BaseInstance in for exactly that
// reason, and VertexInputEmit's base-instance pair is the test that says so.
//
// A hash of 0 is reserved for "never emitted", so the first emission always goes out; a
// computed 0 is remapped to 1, which costs one collision in 2^64 an extra emission and
// never a missed one.
//
// 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.
// Set-hash suppression (ARCHITECTURE.md §5.4): every kVarTail set_* hashes the resolved set on
// the client and is not emitted while the hash is unchanged, in place of the backends' own
// "same set as last time" debounce. A slot's hash must cover every input its record carries,
// not only the set: MGPipeVertexBufferSetContentHash mixes in BaseInstance, which is draw state
// that moves without the buffer set, or a changed fetch shift would be suppressed. A latched 0
// means "never emitted", so the first emission always goes out; a computed 0 is latched as 1,
// costing at worst one extra emission, never a missed one.
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
@@ -61,7 +40,7 @@ namespace MobileGL::MG_Pipe {
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
// P5e (MG_Remote/CONTRACT-P5E.md §1, ruling 11): THREE SLOTS, ONE PER CLASS, not one
// P5e (docs/archive/contracts/CONTRACT-P5E.md §1, ruling 11): THREE SLOTS, ONE PER CLASS, not one
// for the call. set_shader_buffers is emitted per Class (Uniform / ShaderStorage /
// AtomicCounter) because the record's own Class field says which binding-point array
// it describes - so a single slot would make every emission of one class cancel the
+14 -55
View File
@@ -9,45 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of set_shader_buffers - the INDEXED BUFFER BINDING POINTS (P5e package sb,
// MG_Remote/CONTRACT-P5E.md §5.6, rulings 10 and 11). The call has been in the catalogue since
// P4a with no emitter, no route and no applier; this file is the emitter half.
//
// ONE RECORD PER CLASS, THREE CLASSES. MGPShaderBuffers::Class names which binding-point array
// the record describes - Uniform=0, ShaderStorage=1, AtomicCounter=2 - so the three are three
// emissions through one route and one applier, not one record with three tails. That is also
// why the suppressor has THREE SLOTS (ruling 11): a single slot would let a uniform set cancel
// the previous storage set, and the shader-storage bindings would be suppressed as "unchanged"
// by a record that described something else entirely.
//
// XFB IS NOT ONE OF THEM (§5.7). The transform-feedback capture points are span-scoped state
// latched at glBeginTransformFeedback, and set_stream_output_targets carries a Generation this
// payload has no field for - so the catalogue's split between the two rows stays, XFB keeps its
// lockstep escalation, and dirty bit 17 fires for a family that emits nothing this phase.
//
// TWO INVARIANTS THAT MUST SURVIVE INTO THE BODY:
// 1. THE HIGH-WATER-ZERO EARLY-OUT. A class whose touched high-water mark is 0 emits nothing,
// BEFORE any hash and before any walk. Minecraft's touched SSBO and atomic-counter counts
// are both 0, so the whole family costs one integer read per class per validate point on
// the workload this phase exists for.
// 2. A BASE BINDING TRAVELS AS kMGPipeWholeBuffer AND NEVER AS A RESOLVED EXTENT (§5.6,
// table 0). glBindBufferBase does not freeze anything: GL resolves the range against the
// object's size at every USE, and BindingSlotRange1D::GetRange() reproduces that by asking
// the bound object. Resolving it HERE would freeze the size as of the emission, and under
// run-ahead a glBufferData issued between the emission and the apply would then bind the
// OLD extent - silently, with the right handle. So Offset/Size are copied only for an
// EXPLICIT range (glBindBufferRange), and a base binding says "whole buffer" and lets the
// server re-resolve against its own descriptor.
//
// THE WINDOW IS THE TOUCHED HIGH-WATER MARK (ruling 10), the same value the backend's own walk
// has used since P2 and the same value MGPContextValues already carries. A program-derived
// window would be narrower for the uniform class - Minecraft reaches UBOs through the program's
// block bindings, so a program binding block 0 at GL point 83 pays a 84-entry record - but that
// is a NARROWING and this package is a correctness landing; BRIEF-P5E §5 lists it as trailing.
//
// 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.
// Client side of set_shader_buffers, the indexed buffer binding points. One record per class
// (MGPShaderBuffers::Class: Uniform, ShaderStorage, AtomicCounter) through one route and applier,
// with one suppressor slot per class so one class's record cannot mask another's. Transform
// feedback capture points are not in this family; they ride the begin_stream_output record.
// Invariant 1: a class whose touched high-water mark is 0 emits nothing, before any hash or
// walk; that mark is also the record's window. Invariant 2: a base binding travels as
// kMGPipeWholeBuffer, never a resolved extent: GL resolves it at each use, so the server resolves
// it against its own descriptor, and a glBufferData between emission and apply binds the new size.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -63,22 +32,12 @@
namespace MobileGL::MG_Pipe {
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the per-family
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives this
// emitter its body and no file is touched twice.
//
// IT MUST MOVE WITH MG_Backend/Init.cpp's CONSUMER BIT (ID-106). c0e deliberately withheld
// bit 13 from ConsumedSubsystemsFor because no emitter existed and withholding is the safe
// direction; the client's R-8 liveness gate (PipeFill.cpp's P5eFamilyIsLive) asks the server
// for THIS bit in particular, so a build that set the constant here and left the server's
// mask alone would emit nothing at all and look exactly like a build that had not landed.
//
// TURNING IT ON DOES NOT RETIRE A PULL. GetBufferBindingPoint is the family's Coverage.def
// emitted row and PipeFill.cpp's EmittedCallSuppliesTheWholeField answers FALSE for it: the
// field is four raw bases into the frontend's binding-point table and the record carries
// resolved {handle, offset, size} triples, so the residual fill keeps writing the mirror and
// the verify comparator keeps proving it. What retires the pull is the four Espryt consumers
// reading the applier's window instead, which is the other half of this package.
// The subsystem bit this build emits for; PipeFill.cpp ORs it into kMGPipeWiredSubsystems.
// It must change together with MG_Backend/Init.cpp's consumer bit 13: P5eFamilyIsLive asks
// the server for that bit, so setting only this constant emits nothing. Emitting does not
// retire a pull: GetBufferBindingPoint holds raw frontend table bases while the record
// carries resolved {handle, offset, size} ranges, so EmittedCallSuppliesTheWholeField
// answers false and the residual fill keeps writing that mirror.
inline constexpr Uint64 kMGPipeWiredBufferBindingSubsystem = kMGPipeSubsystemBufferBindings;
// The BufferTarget each wire class names. One table, because the class is a wire value and
+21 -56
View File
@@ -11,29 +11,13 @@
#include <MG_Pipe/MGPipeHandles.h>
// The CLIENT's slot allocator: the thing that mints every MGPipeHandle in the system
// (ARCHITECTURE.md 4.2 - no create_* call in the catalogue returns a server-cast handle,
// which is what lets the whole catalogue be remoted with zero creation round trips).
//
// Per kind: a free list plus a high-water mark, so slots stay DENSE and the server's object
// table is an array rather than a hash map. It has nothing to do with MG_State's
// IndexGenerator - that container's LIFO GL-name reuse is the very problem {slot, gen}
// exists to close, and the whole point of the identity is that an ABA on the GL name, on
// the heap address or on the lifetime id cannot reproduce a handle.
//
// Gen increments ONLY when a slot is reused, never on a respecify: a glBufferData on a live
// buffer keeps the same {slot, gen}, because the object is the same object. Two generations
// exist in the design and they are strictly separate - this is the client's answer to "is
// this still the same GL object"; MGGen is the server's epoch for "did I recast my driver
// object", and no MGPipe call may require the client to know it.
//
// The lifetimeId -> slot map is what keeps a GL NAME out of every key (ARCHITECTURE.md 4.2):
// the frontend object's lifetime id is the client's own identity for it, so the backend key
// is the handle and the frontend key is the lifetime id, and neither is a recyclable name.
//
// Lives in MG_Impl (the client side, unrestricted) and is compiled only under
// MOBILEGL_PIPE_PUSH. It is in the P2 CONTRACT commit rather than in a Track H package
// because both Track H slices - Espryt 0b and Magma subsystem 4 - key off it.
// The client's slot allocator: it mints every MGPipeHandle, so no create_* call needs a round
// trip (ARCHITECTURE.md §2.1). A free list plus a high-water mark per kind keeps slots dense,
// so the server's object table is an array. Unrelated to MG_State's IndexGenerator, whose LIFO
// GL-name reuse is the ABA {slot, gen} exists to prevent. Gen increments only when a slot is
// reused, never on a respecify; it is separate from the server's MGGen epoch, which no MGPipe
// call may require the client to know (§2.2). The lifetimeId -> slot map keeps GL names out of
// every key: the frontend key is the lifetime id, the backend key is the handle.
namespace MobileGL::MG_Pipe {
class MGPipeSlotAllocator {
@@ -48,23 +32,14 @@ namespace MobileGL::MG_Pipe {
// Allocate and remember `lifetimeId` as this handle's frontend identity.
MGPipeHandle AllocateFor(MGPipeKind kind, Uint64 lifetimeId);
// P4a, D-H7: THE ONE ENTRY POINT INTO THE ShaderCso COMPOSITE BAND, and the only one
// there will ever be. Allocate() above refuses that band on purpose, so a program
// pipeline's flattened composite - minted client-side from the stage programs bound to
// the pipeline object, and indistinguishable from an ordinary program to the server -
// needs a door of its own rather than a flag on the handle. The kind is implied: only
// ShaderCso has a band.
//
// It behaves exactly like AllocateFor in every other respect (free list first, then
// the band's own high-water mark; Gen moves only on reuse; the lifetimeId -> slot map
// is written) and it carries the band's own exhaustion assert, so exhausting the
// composite space is a NAMED Fatal rather than silent slot theft from ordinary
// programs. Returns kMGPipeNullHandle when the band is full.
//
// Freed through the ordinary Free(MGPipeKind::ShaderCso, handle): a composite's slot
// has two independent release paths - the pipeline cache's LRU eviction and the
// composite ProgramObject's own destructor - and Free refusing a slot that is not live
// at that generation is what makes the second one a proven no-op.
// The only way into the ShaderCso composite band, which Allocate() refuses: a program
// pipeline's flattened composite is minted client-side and looks like an ordinary
// program to the server, so it gets its own entry point rather than a flag on the
// handle. Otherwise it behaves like AllocateFor (free list first, Gen moves only on
// reuse, the lifetimeId map is written). Returns kMGPipeNullHandle, after a named
// assert, when the band is full. Freed through Free(ShaderCso, handle): of its two
// release paths (the pipeline cache's LRU and ~ProgramObject), the second finds the
// slot no longer live at that generation and is a no-op.
MGPipeHandle AllocateComposite(Uint64 lifetimeId);
// The handle a lifetime id was allocated for, or kMGPipeNullHandle. A recycled heap
// address does NOT reproduce a mapping: MG_State hands out a fresh lifetime id per
@@ -89,22 +64,12 @@ namespace MobileGL::MG_Pipe {
// otherwise, live or not.
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
Uint64 LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const;
// One past the highest ORDINARY slot ever handed out of this kind. For every kind but
// ShaderCso that is the whole story; for ShaderCso the composite band is a second,
// separately dense space and CompositeHighWater() below answers it.
//
// THE TWO SPACES ARE REPORTED SEPARATELY, and that is the point rather than a detail.
// Folding the band into this number pins it at ~983k from the first composite mint
// onward, and every later assertion of the "the high-water mark did not move over N
// churn rounds" shape - the one that catches a dense table that never shrinks, which
// is the ~1.3 KB-per-record leak C-1 produced - becomes vacuously true for ordinary
// ShaderCso slots for the rest of the process. A leak case per space is two real
// assertions; one merged number is one real assertion and one that cannot go red.
//
// It is also NOT a table size for kind ShaderCso even now: the band is sparse against
// the ordinary space by design, so a consumer indexing by slot must test
// MGPipeIsCompositeShaderSlot(slot) first and keep the band in a table of its own,
// exactly as this allocator does.
// One past the highest ordinary slot ever handed out of this kind. For ShaderCso the
// composite band is a separate dense space, answered by CompositeHighWater(): folded
// in, this number would sit at ~983k after the first composite, and a leak test
// asserting it stays put over churn could no longer fail for ordinary slots. Nor is it
// a table size for ShaderCso: a consumer indexing by slot tests
// MGPipeIsCompositeShaderSlot(slot) first and keeps the band in a table of its own.
Uint32 HighWater(MGPipeKind kind) const;
// One past the highest COMPOSITE slot ever handed out, i.e.
// kMGPipeShaderCsoCompositeSlotBase + (band slots ever handed out), and exactly the
+61 -232
View File
@@ -9,49 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's texture and renderbuffer family: resource_create from the object's
// Client side of the texture and renderbuffer family: resource_create from the object's
// constructor, resource_respecify from every storage-defining entry point, set_texture_params
// from the parameter mutators, and resource_subdata from the DRAIN LIST at the validate point.
//
// THE THREE OBJECT CALLS ARE NOT EMITTED FROM HERE'S CALLER, they are emitted from MG_State's
// own mutators - a constructor, a storage definition, a glTexParameter - exactly as P3a's
// buffer family is, because that is where the event happens. Only the sub-data drain runs at
// the validate point, which is the explicit exception ARCHITECTURE.md 5.1 makes for texture
// upload: walking every live texture per verb is the cost the drain list exists to avoid.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full: PipeFill.cpp is the contract package's for the whole
// phase, so the emitter package edits this header and the value of
// kMGPipeWiredTextureSubsystem below, and never that file.
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
//
// ---------------------------------------------------------------------------------------
// HOW MG_State REACHES THIS FILE: IT DOES NOT, AND THAT IS THE POINT (c0b, ID-13).
//
// v1 of this package shipped a deviation - six free functions here, called from
// TextureObject.cpp and RenderbufferObject.cpp - because at the contract TAG
// MG_Pipe/PipeMutation.h declared only the six DEATH helpers and this package may not edit
// A's files. c0b landed the BIRTH half, so the deviation is retired rather than carried:
// MG_State now calls MGPipeMintTextureHandle / MGPipeEmitTextureResourceCreate /
// ...ResourceRespecify / MGPipeEmitTextureParams / MGPipeNoteTextureLevelDirty and the two
// renderbuffer twins, all DECLARED in MG_Pipe/PipeMutation.h and DEFINED in
// MG_Impl/Pipe/PipeFill.cpp, which forwards to the entry points below through
// ForwardWhenWired<kMGPipeWiredTextureSubsystem>. No MG_State translation unit includes this
// header any more, which is the property check_include_closure.py's mutation-header probe
// exists to keep.
//
// WHAT THIS FILE OWES THAT SEAM, and a mismatch is a compile error in this package's own
// commit rather than a surprise at the merge (that is what the wired constant buys):
//
// EmitResourceCreate(ITextureObject&) EmitResourceRespecify(ITextureObject&)
// EmitTextureParams(ITextureObject&) NoteLevelDirty(ITextureObject&, Uint32, Uint32)
// EmitRenderbufferCreate(RenderbufferObject&)
// EmitRenderbufferRespecify(RenderbufferObject&)
//
// and the PUBLICATION LATCH is A's too: MGPipeNoteHandlePublished is called where a create
// actually goes out and MGPipeHandleIsPublished is what the death helpers read, so this file
// keeps no Published flag of its own.
// from the parameter mutators, and resource_subdata from the drain list at the validate point.
// Like the buffer resource ops (ARCHITECTURE.md §5.1), the object calls go out from MG_State's
// mutators, where the event happens; texels accumulate and drain at the validate point (§6),
// from a list of dirty levels so no verb walks every live texture. MG_State never includes this
// header: it calls the hooks in MG_Pipe/PipeMutation.h, which PipeFill.cpp forwards here through
// ForwardWhenWired. The publication latch is PipeFill.cpp's; this file keeps no flag of its own.
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -79,30 +44,13 @@
namespace MobileGL::MG_Pipe {
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the four per-family
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives the
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
// silently drop a field on the floor before the call that carries it exists.
//
// IT WAS 0 IN v1, AND THE THING THAT BLOCKED IT IS NOW IN THE TREE. Unlike the other three
// P4a families this one gets no fresh apply entry points - the catalogue is closed and a
// texture rides P3a's OWN resource_create / resource_respecify / resource_subdata /
// resource_destroy rows - so on a base without the wire package's per-kind record vectors
// and its texture branch, two properties made a texture record actively harmful rather than
// merely ignored: MGPipeApplierState::Resources was ONE slot-indexed vector, so a texture
// create at slot 12 overwrote the BUFFER record at slot 12; and SubDataBoxFault validated
// every record as the buffer half of MGPSubData, so a texture sub-data record was
// Fatal{ProtocolCorruption} on `record.Level != 0` alone. Both are closed on this base
// (three per-kind vectors, ApplyTextureUpload, SubDataTextureFault, and C1's level-scoped
// PendingUploads clear), so the constant is its own bit and the family is live.
//
// THE FLIP IS THE WHOLE SWITCH AND NOTHING ELSE MOVES: PipeFill.cpp ORs this constant into
// kMGPipeWiredSubsystems, static_asserts it is 0-or-its-own-bit, gates every birth hook on
// FamilyIsLive(kMGPipeSubsystemTextureResources, this), and gates DrainTextureSubData on
// the same OR. D-K2's rows - including bit 9's dependency on this family - are in
// MG_Pipe/SubsystemDeps.def, once (P3b/P4b R-5), and are not restated here; what this
// comment still has to say is the consequence for INTEGRATION ORDER, which no table can
// carry: this package may never be integrated with only one of the two constants set.
// The subsystem bit this build emits for. PipeFill.cpp ORs it into kMGPipeWiredSubsystems,
// gates every texture birth hook on FamilyIsLive and DrainTextureSubData on the same mask;
// 0 compiles the family out. Textures share the resource_* rows with buffers, which is safe
// because the applier keeps one record vector per kind and checks texture sub-data on its own
// (SubDataTextureFault). The rows of MG_Pipe/SubsystemDeps.def are checked against
// MOBILEGL_PIPE_PUSH, not the wired constants, so this constant and the sampler one are set
// together, and the framebuffer constant is never set without this one.
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = kMGPipeSubsystemTextureResources;
static_assert(kMGPipeWiredTextureSubsystem == 0 ||
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources,
@@ -407,30 +355,14 @@ namespace MobileGL::MG_Pipe {
return region;
}
// ---------------------------------------------------------------------------------
// THE STAGE-CHUNK SLAB SPLIT (fix A2)
// ---------------------------------------------------------------------------------
//
// ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, a linear arena, so a level shadow larger
// than that arena is Fatal{RingOverrun, "SEG_STAGE"} at the encoder rather than a split
// (PipeWireCodec.cpp:856-864) - the same wall PipeFill.cpp's content walks are cut at. It is
// not hypothetical: measured on the CI traces, a 512x128x33 GL_RGBA32F level is 34,603,008
// bytes and a 192-cube GL_RGBA16 level is 56,623,104, both against the default 32 MiB
// segment, so those glTexImage3D calls aborted their replays at the emitter.
//
// THE SPLIT IS INTO WHOLE-WIDTH SLABS. A piece is a set of CONSECUTIVE ROWS OF THE LEVEL
// SHADOW - {x0, y0, z0, w, dy, dz} in level coordinates - because that is the one shape whose
// box names both its texels and its byte run: the run begins at
// z0 * sliceStride + y0 * rowStride + x0 * bpp and is exactly the box's own byte extent. So
// the run's placement is derivable from the strides and box the record carries, and the far
// side never has to know the level's format or pixel size. Whole slices are the coarsest cut
// and the two CI blobs take it; a slice bigger than the cap falls back to whole rows, and a
// ROW bigger than the cap to whole texels inside one row - still one box exactly matching its
// own run, which is the property everything downstream depends on.
//
// THE PIECES ARE CONTIGUOUS AND ASCENDING, and their union is the level exactly once: the
// server assembles one level image out of them (StagedTextureStore::AdoptRun), so a gap would
// lose texels and an overlap would place them twice.
// ---- Stage-chunk slab split (ARCHITECTURE.md §4) ----
// One record's blob is staged whole in SEG_STAGE, so a level larger than the chunk cap goes
// out as several records (a 512x128x33 GL_RGBA32F level is 33 MiB, over the whole default
// segment). A piece is whole slices, else whole rows of one slice, else texels of one row:
// the only shapes whose byte run starts at z0*sliceStride + y0*rowStride + x0*bpp and is
// exactly the box's extent, so the server places it without knowing the format. Pieces
// ascend, touch and cover the level exactly once; StagedTextureStore::AdoptRun assembles one
// level image from them, so a gap loses texels and an overlap places them twice.
struct MGPipeTextureSlab {
MGPBox Box{}; // level coordinates
Uint64 RunOffset = 0; // the run's first byte, as an offset into the level shadow
@@ -690,27 +622,14 @@ namespace MobileGL::MG_Pipe {
entry = Entry{};
}
// ---- the sticky bind mask (D-A4) ----
//
// ORed, never cleared, and emitted on BOTH resource_create and every
// resource_respecify, exactly as P3a's buffer mask is. The four bits nothing set before
// P4a get their producers here and in the framebuffer emitter: RENDER_TARGET and
// DEPTH_STENCIL from an attachment point (FramebufferEmit.h), SAMPLER from a resolved
// sampler view (SamplerEmit.h) and SHADER_IMAGE from glBindImageTexture's state setter
// and the resolved image unit (TextureState.h, ImageEmit.h) - the last two through the
// contract's MGPipeNoteTextureBoundAs door, since neither may include this header
// (final review M-A: before the fix round nothing produced them and the hint was dead).
// A MASK CHANGE AFTER THE ALLOCATION IS A METADATA RESPECIFY (ID-18 M4), and without it
// the sticky half of D-A4 is a no-op for exactly the textures it was written for. The
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
// no further respecify, that being what immutable means - so for the canonical order
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
// ImageBindableHint = 0 for ever and the re-mint PREVENTION never fired. So a mask that actually MOVES re-emits the stored descriptor with
// the new mask: every storage-defining field is byte-identical to what the applier
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
// change arriving between a glTexSubImage2D and the sync that consumes it cannot eat
// the texels.
// ---- Sticky bind mask ----
// ORed, never cleared, and carried on resource_create and every resource_respecify.
// Producers: FramebufferEmit.h (RENDER_TARGET, DEPTH_STENCIL), SamplerEmit.h (SAMPLER),
// ImageEmit.h and glBindImageTexture's setter in TextureState.h (SHADER_IMAGE); the
// last three use the MGPipeNote* hooks in PipeMutation.h, as they cannot include this
// header. An immutable texture is never respecified again, so a mask that moves later
// re-sends the stored descriptor with the new mask (RepublishMask), which the applier
// takes as a metadata update that drops no pending upload (ARCHITECTURE.md §6.1).
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_textures, handle);
@@ -770,34 +689,14 @@ namespace MobileGL::MG_Pipe {
PublishCreate(MGPipeKind::Texture, handle, entry, desc);
}
// resource_respecify, from every storage-defining entry point, WITH THE SCOPE OF THE
// STORAGE IT REPLACES (P4a final review C-1; the scopes are PipeMutation.h's).
//
// THE LEVEL IS PASSED, AND IT IS WIRE'S KEY. The applier keeps a pending-upload set per
// (uploadTarget, level) - the client's dirty flags, inverted - and a respecify drops the
// entries against the storage it REPLACES: with a null MGPRespecifiedLevel every entry,
// with a level exactly that one. v2 passed null at every call, so a level the applier
// had ACCEPTED at one verb (the client flag already clear, D-D5 step 1) and that the
// next verb's glTexImage2D(level 1) or glGenerateMipmap grow defined AROUND was dropped
// with nobody owing its texels: `L0; draw(other); L1; draw(T)` read a black level 0.
// The key is built from the SAME packed MGPSubData::Target the drain puts in that
// level's record (wire-v3 §5 item 5), so what this drops is what that emission made.
//
// THREE SCOPES, one call each for the first two and one call PER REMOVED LEVEL for the
// chain cut: the applier's key is one (uploadTarget, level), so "every level from N"
// is spelled as N.., each after the first landing on an unchanged descriptor - which
// the applier classifies as a metadata update that drops nothing but the level it
// names. That is the refinement wire's W11 clause takes this round.
//
// DEDUPED ON THE DESCRIPTOR ITSELF for the whole-resource form only: the entry points
// that reach it move the SHAPE and several of them do not move the descriptor at all
// (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field this
// record carries), and a byte compare of an 88-byte POD is cheaper than the emission
// it avoids. A PER-LEVEL form is never deduped: the level it redefines is not in the
// descriptor (a non-base level's extent moves no field), so an unchanged descriptor
// cannot say whether the applier still holds a box against the OLD level - and a box
// kept across a shrink is uploaded past the end of the new one. One applier call per
// level definition is the cost, and the sub-data that follows moves the serial anyway.
// resource_respecify, from every storage-defining entry point, with the scope of the
// storage it replaces (MGPipeTextureRespecifyScope). The applier drops pending uploads
// per (uploadTarget, level), only against the replaced storage (ARCHITECTURE.md §6.1);
// the key packs the same MGPSubData::Target the drain writes. A chain cut sends one call
// per removed level. A whole-resource call is deduped on the descriptor, since some
// entry points (glTexParameter BASE_LEVEL) move no descriptor field; a OneLevel call
// never is: a non-base level's extent moves no descriptor field, and a pending box kept
// across a shrink would upload past the end of the new level.
void EmitResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
Uint32 uploadTarget, Uint32 level) {
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
@@ -1131,32 +1030,14 @@ namespace MobileGL::MG_Pipe {
NoteRespecified(entry, desc, accepted);
}
// ---- the drain list (D-D4) ----
//
// Appended ONCE, on the first dirty mark of a level, and cleared at emission. Keyed on
// the STORAGE OWNER from day one and for free: TextureObjectView forwards
// MarkStorageDirty / MarkStorageDirtyRegion to the OWNER's methods after remapping the
// level and the region, so an upload through a view and an upload through the owner
// reach this function with the same object and the same owner-side coordinates.
//
// The per-slot key list is a short linear scan rather than a hash: a level count is
// ~15, the cap on the rect list behind it is 96, and this runs on the glTexSubImage
// path which has just memcpy'd texels.
// THE PARAMETER TYPES ARE THE CONTRACT'S (PipeMutation.h): Uint32 rather than
// MobileGL::TextureUploadTarget and Uint, because that declaration is the one door
// MG_State has into the client and it may not name a frontend enumeration.
//
// THERE IS NO CLEAN ARM, and that is a DECLARED DEVIATION rather than a dropped half.
// v1 carried a second entry point for MarkStorageDirty(..., false); the contract's hook
// has no `dirty` parameter, and asking A to widen it would put a second signature in
// MG_Pipe/PipeMutation.h for something the drain already collects. A level that goes
// clean stays on the list until the NEXT drain walks it, where
// `!mipmap->IsStorageDirty(...)` is the first test EmitOneLevel makes and returns
// "nothing owed", so the entry is dropped from both lists there. The cost is one
// IsStorageDirty call per cleaned level per drain, the list is bounded by the (texture,
// level) pairs dirtied since the last validate point, and a re-dirty before that drain
// is already covered by the entry still standing. What it must NOT be confused with is
// dropping the level's TEXELS: nothing here clears a dirty flag.
// ---- Drain list ----
// A (storage owner, upload target, level) is appended on its first dirty mark and
// removed when emitted. TextureObjectView forwards dirty marks to its owner in owner
// coordinates, so view and owner uploads share one entry. The per-slot key list is a
// linear scan (about 15 levels, on a path that has just copied texels). The hook takes
// Uint32s because PipeMutation.h may not name frontend enums. There is no "went clean"
// hook: a level cleaned before the drain is dropped there when EmitOneLevel finds it
// clean. Nothing here clears a dirty flag.
void NoteLevelDirty(ITextureObject& texture, Uint32 uploadTarget, Uint32 level) {
if (m_draining) return;
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
@@ -1499,29 +1380,14 @@ namespace MobileGL::MG_Pipe {
const MGPBox unionBox = MGPipeBoxOfDirtyRegion(mipmap->GetStorageDirtyRegion(uploadTarget, level));
if (unionBox.W == 0 || unionBox.H == 0 || unionBox.D == 0) return false;
// THE REGION LIST BEHIND THE BOX IS THE LEVEL'S EXACT DIRTY FOOTPRINT (P11 M2, dev
// 9524c688): disjoint boxes holding every texel written since the level was last
// clean and no other. The storage's rect list this used to carry only COVERS the
// writes - it merges touching rects into their bounding box, folds when full and is
// withheld (0) past 3/4 of the union box - and once the far side's image may hold GPU
// writes, whatever it uploads beyond the writes goes up over them. Both servers read
// the list as the texels to upload: Magma always, Espryt once its driver texture may
// hold GPU writes (and otherwise it re-applies the storage's box-or-rects choice
// itself, so the Mali job count is the server's call as before).
//
// 0 IS NOW THE WHOLE-LEVEL SPELLING - a footprint that is the whole level - with one
// exception (below). Every other write carries its boxes, a single one included, so the
// applier's accumulation (boxes union, lists concatenate, "0 regions" makes the entry
// box only) never widens two exact contributions into their union box. THE EXCEPTION:
// a footprint larger than one record may carry (kMGPipeMaxPendingUploadRegions) falls
// back to the storage's rect list - a COVER, stated once in the log - and when that list
// is withheld (0: its rects fill 3/4 of the union box) this 0 is a PARTIAL, INEXACT
// box. A reader that takes "box only" as "the client owns every texel of the box"
// (AdoptDriverLevelIntoShadow's covering-box rule) is exact everywhere but here.
//
// READ AS BOXES, ONCE, because both shapes below want them in that form: the
// whole-level record converts each one straight back through MGPipeBuildSubRegion,
// and a piece clips each one to its own slab.
// The region list is the level's exact dirty footprint: disjoint boxes holding
// every texel written since the level was last clean and no other, because a server
// image that may hold GPU writes must not be uploaded over outside them. 0 regions
// means a whole-level write, except when the footprint exceeds
// kMGPipeMaxPendingUploadRegions: the storage's covering rect list goes out instead,
// and when that list is withheld the box is inexact (ARCHITECTURE.md §6.1.1). Read
// as boxes once: the whole-level record converts each through MGPipeBuildSubRegion,
// and a piece clips each to its slab.
mipmap->GetStorageDirtyFootprint(uploadTarget, level, m_footprint);
m_rectBoxes.clear();
const Bool wholeLevelWrite =
@@ -1656,23 +1522,13 @@ namespace MobileGL::MG_Pipe {
accepted = emitRecord(shadow, static_cast<Uint64>(levelBytes));
}
// THE CLIENT CLEARS ITS OWN FLAG ONLY FOR A LEVEL THE APPLIER ACCEPTED (D-D5 step 1
// read literally; ID-18 M3). v1 cleared on DISPATCH - and, with the wired constant
// still 0, even on a call the `if constexpr` had discarded - so any refusal left the
// server with nothing and the client with a clean flag, and since MG_Impl contains
// no reader of a texture's dirty state the level simply stopped updating for the
// life of the texture. The two refusal paths are invisible from here without this
// answer: a dead or stale handle is a counted no-op and a corrupt record is a Fatal
// that deliberately moves no counter.
//
// A REFUSED LEVEL STAYS DIRTY AND STAYS ON THE DRAIN LIST, which is the safe
// direction and self-heals: the ordinary cause is a record the applier does not
// hold, and the next respecify's self-healing create gives it one. It is LOUD
// because a permanently refused level would otherwise re-emit once per verb for
// ever with nothing to show for it. WITH PIECES IN FLIGHT it is the same rule one
// level up: every piece is still emitted, so a refusal leaves the server with a
// partial image and the WHOLE level dirty, and the next drain re-sends every piece -
// an adoption of the same bytes twice is idempotent.
// The dirty flag is cleared only for a level the applier accepted (ARCHITECTURE.md
// §6): nothing else in MG_Impl reads a texture's dirty state, so clearing it on a
// refused record would stop the level updating for the texture's lifetime. A refused
// level stays dirty and on the drain list; the usual cause is a missing applier
// record, which the next respecify's self-healing create supplies. It is logged
// because a level refused for good re-emits every verb. A refused split level
// re-sends every piece; adopting the same bytes twice is idempotent.
if (dispatched && !accepted) {
++m_refusedSubDatas;
MGLOG_E_ONCE("MGPipe: resource_subdata for texture {slot=%u, gen=%u} level %u was refused; "
@@ -1725,31 +1581,4 @@ namespace MobileGL::MG_Pipe {
(MG_Config::Features.PipePush & kMGPipeSubsystemTextureResources) != 0;
}
// ---------------------------------------------------------------------------------
// WHAT USED TO BE HERE, AND WHY IT IS NOT (c0b, ID-13)
// ---------------------------------------------------------------------------------
//
// v1 carried eight free functions - MGPipeMintAndCreateTexture, MGPipeEmitTextureRespecify,
// MGPipeEmitTextureParams, MGPipeNoteTextureLevelDirty, MGPipeMintAndCreateRenderbuffer,
// MGPipeEmitRenderbufferRespecify and the two ...ResourceDestroy halves - because at the
// contract tag MG_Pipe/PipeMutation.h declared no texture birth hook and
// MG_Impl/Pipe/PipeFill.cpp's death helpers hard-coded `published = false`. Every one of
// them is now A's:
//
// * the four MINTS and the nine EMISSIONS are declared in PipeMutation.h and defined in
// PipeFill.cpp, which gates them on FamilyIsLive(bit, kMGPipeWiredTextureSubsystem) and
// forwards to this class through ForwardWhenWired. MGPipeEmitTextureParams in
// particular was a NAME COLLISION - the contract declares that exact signature - so
// keeping the inline definition here would not have compiled at all;
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
// resource_destroy themselves, so the destructors call ONE helper and not two - and
// since the final review's C-2 the helper then forwards to NoteTextureDied /
// NoteRenderbufferDied above, so no ITextureObject* survives its object in this table
// and no dead level survives on the drain list;
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
// PublishCreate above and read by those helpers.
//
// The self-healing create in EmitResourceRespecify stays this file's: c0b provides no such
// path and it is what repairs a texture born while the subsystem bit was clear.
} // namespace MobileGL::MG_Pipe
+65 -308
View File
@@ -9,54 +9,14 @@
#pragma once
#include <Includes.h>
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
// The frontend state tracker (ARCHITECTURE.md §5.2).
//
// WHERE IT RUNS. Not above MGP_FILL and not in the GL setter: MGPipeValidateForVerb, the
// one statement MGP_FILL already expands to before every gBackendFunctionsTable.GL call
// (PipeFill.h). Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), so a
// setter that pushed would push twice per batch for a state the batch may not even read;
// the validate point coalesces the whole bracket into the two draws that observe it
// (ARCHITECTURE.md 5.1).
//
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
// computed by comparing a shutter against what the tracker last pushed. P2 emitted for bits
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and P4a
// adds SEVEN: 6, 7 and 8 (the program family), 11 (the framebuffer) and 12, 13 and 14 (the
// three unit sets). Only bits 15, 16 and 17 - the const-buffer, shader-buffer and
// stream-output sets - are still computed, latched and counted without an emitter, so the
// per-bit fire rate is a measurement rather than a plan and their fields go through the
// residual fill until P4b.
//
// P4a NARROWS NOTHING AND WIDENS THREE THINGS, and every one of them was an UNDER-FIRE that
// only became reachable once the bit gained an emitter:
// (1) bit 11's shutter gains the READ framebuffer binding slot's version, because
// set_framebuffer_state is emitted per bound TARGET and a glBindFramebuffer(
// GL_READ_FRAMEBUFFER, ...) moved no shutter at all before;
// (2) bit 13's gains the TEXTURE BIND generation, because glBindSampler moves that one and
// not the sampling-resolution one, so bind_sampler_states could not see a sampler bind;
// (3) bits 6/7/8 - and with them bit 14's program half - read the EFFECTIVE program source
// instead of GetCurrentProgram() alone, which is null for the whole life of a bound
// separable program pipeline, so a re-composited pipeline reached no program emitter.
// Over-firing is free; all three of those were the other direction.
//
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
// dangerous direction precisely because the P1 verify comparator cannot see it for
// object-class state (it compares those by identity only). So each shutter below is
// deliberately coarser than the state it guards - five bits share one buffer aggregate,
// the framebuffer bit fires on any attachment write anywhere - and the narrowing is P3's
// work, paid for with the fire rates this file publishes.
//
// NO TIMER LIVES HERE. ROADMAP.md forbids committing hot-path instrumentation; the
// absolute ns/draw comes from DriverBench, which times whole frames from outside the
// library (P2 brief D17). The only counting is the per-bit fire tally, behind
// PipeStats::Enabled() like every other counting site in the tree.
//
// HEADER-ONLY, and that is an ownership decision rather than a design one: the P2 brief
// asks for Tracker.{h,cpp}, but the root CMakeLists.txt that would have to name a new .cpp
// belongs to package A and is frozen behind the p2/contract tag. Everything here is
// included by exactly one translation unit in the library (MG_Impl/Pipe/PipeFill.cpp) plus
// the unit tests, so inline costs nothing. Splitting it back out is one list(APPEND) line.
// Update() runs at the validate point, MGPipeValidateForVerb, which MGP_FILL expands to before
// every backend call (PipeFill.h), not in the GL setters: a glEnable/glDisable bracket around a
// batch then costs one push at the draws that observe it (ARCHITECTURE.md §5.1). It returns one
// MGPipeDirty bit per shutter that moved since the last push. A shutter may over-fire (one extra
// push) but must never under-fire (silently stale state, ARCHITECTURE.md §5.2.1), so each is
// coarser than the state it guards. Bit 17 is computed and counted but has no emitter.
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_State/GLState/Core.h>
@@ -210,7 +170,7 @@ namespace MobileGL::MG_Pipe {
case MGPipeDirty::NewSamplers:
case MGPipeDirty::NewShaderImages:
return kMGPipeSubsystemSamplers;
// P5e's three, one subsystem (MG_Remote/CONTRACT-P5E.md §1): the indexed buffer
// P5e's three, one subsystem (docs/archive/contracts/CONTRACT-P5E.md §1): the indexed buffer
// binding points. Bits 15/16/17 have been computed and counted since P2 and have named
// no subsystem since - "the remaining bits have no call of their own until P4b", which
// the default arm below used to say for them. set_shader_buffers is the call, and
@@ -239,27 +199,13 @@ namespace MobileGL::MG_Pipe {
}
}
// A COMPOSITE shutter, for the bits whose "did anything move" is more than one counter.
// It is a hash, so two different states can in principle collide and cost a MISSED fire.
// The five bits P2 emits for are never composed - they are widened counters and byte
// compares, neither of which can collide.
//
// P3a's three ARE composed, so the risk is now real rather than academic, and it is
// accepted with its size stated: two DIFFERENT states collide with probability ~2^-64 per
// pair, and the inputs are monotone lifetime ids, versions and generations - none of which
// an application can steer. The alternative, comparing the whole 32-attribute configuration
// byte for byte on every verb, is the per-draw cost the shutter exists to avoid. The
// narrowing that removes the composition for bit 10 - its own slot version plus the bound
// object's identity - is what this phase already did to the one shutter that was composed
// over an unrelated aggregate.
//
// That bound needs a mix that avalanches. The hash_combine step this used to be
// (acc ^= value + k + (acc << 6) + (acc >> 2)) does not: small neighbouring counters collide
// outright - (1, 64) and (2, 1) mix to the same value - and bit 12 missed a fire on exactly
// such a pair, so a draw sampled the previous set's texture
// (KHR-GL46.internalformat.copy_tex_image.rgb10_a2ui after a run of earlier cases). Each
// step here is a bijection of either argument with the other fixed, so a change to one
// input alone always moves the result, and the 64-bit finalizer spreads a change to both.
// The mix for composite shutters, built from several counters. It is a hash, so two states
// can collide and miss a fire; over lifetime ids, versions and generations an application
// cannot steer, that is about 2^-64 per pair, accepted instead of comparing the whole state
// on every verb. The mix must avalanche: a hash_combine-style step collides outright on small
// neighbouring counters ((1, 64) and (2, 1)), and a missed bit-12 fire then leaves a draw
// sampling the previous texture set. Each step is a bijection in either argument with the
// other fixed, so a change to one input alone always changes the result.
inline constexpr Uint64 MGPipeShutterAvalanche(Uint64 value) {
value ^= value >> 30;
value *= 0xbf58476d1ce4e5b9ull;
@@ -348,53 +294,14 @@ namespace MobileGL::MG_Pipe {
vao ? MGPipeMixShutter(vaoLifetime, vaoConfig) : 0;
now[Index(MGPipeDirty::NewVertexElements)] = vaoIdentity;
// Deliberately NOT GetProgramForDraw: that joins a pending link, and the tracker
// must not force a compile just to answer "did the shader move". These version
// counters are plain members and are exactly what the backends already read
// without joining (Core.cpp, the glUseProgram half of join site J1).
//
// BUT GetCurrentProgram() ALONE IS NOT THE PROGRAM SOURCE, AND AT P4a THAT IS AN
// UNDER-FIRE. Under GL_ARB_separate_shader_objects an application drives
// `glUseProgram(0); glBindProgramPipeline(P)`, and m_currentProgram is then null
// for the whole life of that pipeline (Core.cpp, GetProgramForDraw's second half):
// all three of these shutters read 0 == 0 forever, so after the first walk on a
// fresh context - the one !m_primed fires unconditionally - bits 6, 7 and 8 never
// fire again however the pipeline is restaged.
//
// WHILE NOTHING WAS EMITTED FOR THEM THAT WAS INVISIBLE, which is how it survived
// to P4a: GetProgramForDraw is emitted-and-still-pulled, the residual fill copies
// it at every verb, and DirtySurface.def rules BindProgramPipelineObject
// kPulledEveryVerb for exactly that reason - the backend still receives the right
// SharedPtr and nothing renders wrong. The moment P4a emits off these bits it
// stops being invisible: glUseProgramStages rebuilds the composite, EmitShaderState
// is never called again, so the new composite gets no ShaderCso handle and no
// create_shader_state while set_draw_program keeps naming the previous one - a
// program the handle protocol never announced, which is exactly the seam-defect
// class P3a spent a phase closing. And bit 8 never firing means
// set_global_constants is never sent for a pipeline draw at all, where the pull
// rescues nothing.
//
// SO THE SHUTTER READS THE EFFECTIVE SOURCE: the program in use when there is one,
// and the bound pipeline when there is not. What it reads OF that pipeline is the
// pair ComputeDrawProgramSignature() is built from - each stage program's lifetime
// id and LINK version - so bit 6 fires exactly when GetProgramForDraw would hand
// back a different composite, which is exactly when a new ShaderCso handle has to
// be minted. Those are the same non-artefact fields the plain-program arm above
// reads, and the ones Core.cpp calls out as not passing through ProgramObject's
// join gate, so the "must not force a compile" rule survives intact: no join, no
// flatten, no Link().
//
// THE PIPELINE NAME IS MIXED IN because two pipelines can carry the same stage set
// and each caches its OWN composite object, so the signature alone would let a
// glBindProgramPipeline between two such pipelines pass without a fire. What that
// does NOT close is a name RECYCLED (glDeleteProgramPipelines +
// glGenProgramPipelines) back onto the same stage programs at the same link
// versions with no other program-family change in between: a ProgramPipelineObject
// has no lifetime id and no wire object at all - DirtySurface.def says so where it
// rules MarkProgramPipelineForDeletion kUnpublishedDestroy - so there is nothing
// else here to mix it with. Recorded rather than quietly left: closing it needs a
// generation counter on the frontend object, which is an MG_State change and not
// this file's to make.
// Reads plain version counters, never GetProgramForDraw(): that joins a pending
// link, and the tracker must not force a compile. The source is the program in use
// or, when there is none (glUseProgram(0) with a separable pipeline bound), the bound
// pipeline's stage programs' {lifetime id, link version}, the pairs
// ComputeDrawProgramSignature() packs, so bit 6 fires exactly when the composite
// changes. The pipeline name is mixed in because two pipelines with the same stages
// cache separate composites. Known gap: a deleted pipeline name regenerated onto the
// same stages is not seen, since ProgramPipelineObject has no lifetime id to mix in.
const auto& program = ctx.GetCurrentProgram();
Uint64 shader = 0;
Uint64 bindings = 0;
@@ -453,23 +360,14 @@ namespace MobileGL::MG_Pipe {
"bits 7 and 8 read the four counters per graphics stage that "
"ComputeUniformMirrorVersions packs");
// Bit 6 is the pipeline's identity plus the composite cache key. Bits 7 and 8
// add the per-program state, which under a pipeline is written to the STAGE
// programs - glUniform* addresses the pipeline's active program,
// glProgramUniform* and the two block-binding calls address a named one - and
// only reaches the composite through RefreshCompositeUniforms. Bit 14's half
// takes the image-unit generation, which is its own counter for the reason
// ProgramObject gives (ES forbids glUniform1i on an image uniform, so Espryt
// BAKES the unit into the ESSL it generates and only a regeneration honours a
// change) and which D-G4 asks this shutter to keep reading as a FRONTEND
// counter rather than any server-side epoch.
//
// STAGELINKS IS MIXED INTO ALL THREE OF THE OTHERS, ON PURPOSE. A composite
// REBUILD hands back a brand-new ProgramObject with an empty default uniform
// block and no backend state at all - SetCachedDrawProgram clears the mirror
// versions with it - so a shutter watching only the per-stage state counters
// would let a rebuilt composite inherit the bindings, the constants and the
// image units of the one it replaced.
// Bit 6 is the pipeline identity plus the composite cache key. Bits 7 and 8
// add the stage programs' own state (glUniform* reaches the active program,
// glProgramUniform* and the block-binding calls a named one), which reaches
// the composite only through RefreshCompositeUniforms. Bit 14 reads the image
// unit version, a separate counter because Espryt bakes image units into the
// ESSL it generates (see ProgramObject). stageLinks is mixed into all of them:
// a rebuilt composite is a new ProgramObject with no uniforms or backend state
// and must not inherit the old one's bindings, constants or image units.
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
Uint64 stageState = 0;
Uint64 stageImages = 0;
@@ -501,76 +399,11 @@ namespace MobileGL::MG_Pipe {
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
now[Index(MGPipeDirty::NewGlobalConstants)] = constants;
// ===========================================================================
// THE RECORD-FIELD -> SETTER -> SHUTTER TABLE FOR THE SEVEN P4a BITS.
//
// THE RULE (P4a fable seam audit, section C.1): every field of every emitted
// record names the frontend setter that changes it, and that setter moves a
// counter the emitting bit's shutter reads - or the emission is unconditional at
// the setter (the resource_* family, set_texture_params). A record field whose
// setter moves no shutter input is a stale record with nothing to refuse: c0d
// (bit 13 without the bind generation), SD-0 (an image re-bind), F-1 (the
// program behind the view set), F-2 (the program behind the image window) and
// F-3 (an attached object's storage) were all this one class. DirtySurface.def
// cannot catch it - it maps MUTATORS to bits and cannot see that a DERIVED field
// depends on a mutator whose row is another family's - so the table lives here,
// beside the shutters, and a row is added whenever a record gains a field.
//
// bit 6 create/bind_shader_state, set_draw/dispatch_program (ProgramEmit.h)
// fields: Cso, StageMask, GlobalUboSize, the artefact blob refs, the two
// bound handles
// setters: glUseProgram (m_currentProgram), glLinkProgram (link version),
// glBindProgramPipeline / glUseProgramStages (pipeline name +
// per-stage {lifetime id, link version})
// shutter: lifetime id x link version, or stageLinks under a pipeline
// bit 7 the program's bindings (image units, block bindings, uniform write set)
// setters: glUniform1i on an opaque uniform (backend state version, image
// unit version), glUniformBlockBinding / glShaderStorageBlockBinding
// (block binding version), any glUniform* (uniform write set)
// shutter: the four per-program counters, x stageLinks under a pipeline
// bit 8 set_global_constants: ShaderCso, Version, the default-block image
// setters: any glUniform* on the default block (UBO content version),
// glUseProgram (lifetime id)
// shutter: lifetime id x UBO content version, or stageState
// bit 11 set_framebuffer_state: Fbo, Color[8]/Depth/Stencil/ReadSurface
// (Res, Kind, InternalFormat, TextureTarget, Layered, Level, Layer,
// UploadTarget), DrawBuffers[8], Width/Height/Layers/Samples/
// FixedSampleLocations, IsDefault, Complete, Target
// setters: glFramebufferTexture*/glFramebufferRenderbuffer, glDrawBuffer(s),
// glReadBuffer, glFramebufferParameteri (the attachment
// aggregate); glBindFramebuffer (the two binding slot versions);
// AND a storage redefinition of an ATTACHED texture or
// renderbuffer - glTexImage*/glTexStorage*/glTexBuffer/
// glTextureView/glRenderbufferStorage* - because InternalFormat,
// TextureTarget, the extent, Samples and Complete are INLINED at
// emission (D-C1): those bump the attachment aggregate from the
// object's PipePublishDescriptor (F-3)
// shutter: attachment aggregate x draw bind version x read bind version
// bit 12 set_sampler_views: per unit {View, Texture}
// setters: glBindTexture / glActiveTexture (bind generation), a texture's
// or a sampler object's parameters (SamplesAsIncompleteTexture -
// the params aggregate), an upload that defines a level (content
// aggregate), the default texture's image appearing (bind
// generation, TextureObject.cpp); AND the program in use -
// glUseProgram, a relink, glUniform1i on a sampler uniform (which
// unit a uniform's TYPE resolves) - F-1
// shutter: content x params x bind generation x opaqueUnits
// bit 13 bind_sampler_states: per unit the sampler CSO handle
// setters: glBindSampler (bind generation, c0d), glSamplerParameter* /
// glTexParameter* (params aggregate + sampling resolution),
// glDeleteSamplers (bind generation)
// shutter: params x sampling resolution x bind generation
// bit 14 set_shader_images: per unit {Res, InternalFormat, Layer, Level,
// Layered, Access} over the program's image-unit window
// setters: glBindImageTexture (bind generation, SD-0), a texture's
// content/params, glUniform1i on an image uniform (image unit
// version); AND the program in use - glUseProgram, a relink -
// F-2
// shutter: content x params x bind generation x programImages
// (lifetime id x link version x image unit version)
// bits 11-14 ALSO fire on the share group's twins of the aggregates they read, for
// a setter called through a sibling context (C8, after the dirty loop)
// ===========================================================================
// Every field of an emitted record must change only through setters that move an
// input of the emitting bit's shutter, or be emitted unconditionally at its setter
// (resource_*, set_texture_params). DirtySurface.def cannot check derived fields,
// so the record-field -> setter -> shutter table for bits 6-8 and 11-14 is kept by
// hand in ARCHITECTURE.md §5.2.2; add a row there when a record gains a field.
// ---- the object-class bits 9..17 ----
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
@@ -614,40 +447,14 @@ namespace MobileGL::MG_Pipe {
indexObject ? indexObject->GetLifetimeId() : 0);
}
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(vaoIdentity, indexShutter);
// Bit 11, WIDENED AT P4a AND THIS IS A REQUIREMENT RATHER THAN AN OPTION. The
// shutter observed the DRAW binding slot only, so glBindFramebuffer(
// GL_READ_FRAMEBUFFER, ...) moved nothing at all - which was harmless while
// nothing was emitted for the bit and is an UNDER-FIRE the moment P4a emits
// set_framebuffer_state per bound target (D-C2): the read record would never be
// sent and the server's ReadSurface would stay the previous framebuffer's. Over-
// firing costs one extra push; under-firing renders stale, and this file's own
// rule is that under-firing is the dangerous direction.
//
// A STORAGE REDEFINITION OF AN ATTACHED OBJECT MOVES THIS SHUTTER (P4a fable seam
// F-3), and the sentence that stood here - "a renderbuffer respecify is still
// invisible here, and deliberately so ... closed by emitting resource_respecify
// straight from the storage entry point" - was true of the RESOURCE record only.
// set_framebuffer_state inlines each attachment's InternalFormat, TextureTarget,
// extent, Samples and Complete (D-C1: "so the four cross-object masks fall out at
// push time with no lookup"), so `glTexImage2D(tex, RGB8); attach; draw;
// glTexImage2D(tex, RGBA8); draw` left the FRAMEBUFFER record saying RGB8 while the
// resource record said RGBA8, and the handle arm answered its alpha-widening,
// snorm-clamp and integer masks from the stale copy where the legacy arm re-read
// the frontend at the same re-sync - a proven arm divergence on a public-GL
// sequence. The fix is at the SETTER, not here: TextureObjectBase::PipePublish
// Descriptor and RenderbufferObject::PipePublishDescriptor - the one funnel every
// storage-defining entry point of either object takes, push-only - bump the
// attachment aggregate this shutter already reads. No counter is added to either
// object (G1), nothing widens this shutter onto the texture-content aggregate (which
// would fire the 304-byte record build on every glTexSubImage2D), and a storage
// definition of an UNATTACHED object over-fires it exactly once at load time.
//
// AND A TRAP THE NEXT NARROWING WOULD WALK INTO, recorded here because it is
// invisible from the shutter: FramebufferObject::SetDrawBuffer versions the VALUE
// being written rather than the index being written TO - it calls
// BumpAttachmentVersion(buffer). The object version and the aggregate still move,
// so THIS shutter is safe; a narrower one built on m_attachmentVersions would not
// be, and P4a must not build one.
// Bit 11 reads the draw and the read binding-slot versions, since the record is
// emitted per bound target. It inlines each attachment's format, target, extent,
// samples and completeness, so a storage redefinition of an attached object must fire
// it too: TextureObjectBase's and RenderbufferObject's PipePublishDescriptor bump the
// attachment aggregate (an unattached object over-fires once). The texture-content
// aggregate stays out; it would rebuild the record on every glTexSubImage*. Do not
// narrow this onto m_attachmentVersions: FramebufferObject::SetDrawBuffer bumps the
// version of the value written, not of the index.
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
MGPipeMixShutter(
ctx.GetAnyFramebufferAttachmentGeneration(),
@@ -665,67 +472,27 @@ namespace MobileGL::MG_Pipe {
now[Index(MGPipeDirty::NewSamplerViews)] = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), ctx.GetTextureBindGeneration()),
opaqueUnits);
// Bit 13, WIDENED AT P4a FOR BIT 11's REASON and found the same way. glBindSampler
// moves NEITHER half of what this used to read: GL_Sampler.cpp's BindSampler_State
// goes through NoteTextureUnitTouched and TextureUnit::SetSamplerObject, and both
// of those bump the TEXTURE BIND generation - bit 12's. The only two writers of
// BumpSamplingResolutionGeneration are PARAMETER changes (SamplerObject.cpp,
// TextureObject.cpp). So `glBindSampler(3, a); draw; glBindSampler(3, b); draw`
// fired bit 12 twice and bit 13 not once, and the server's BoundSamplerStates[3]
// went on naming a's CSO: wrong filtering, with nothing able to see it, because
// bind_sampler_states has no pulled twin to fall back on the way the view set does.
//
// MIXING THE GENERATION IN IS THE FIX RATHER THAN A SECOND GATE ON THE EMITTER,
// because that generation is what the unit SET is derived from: a sampler bind
// changes which sampler state applies at a unit, and a texture bind changes it too
// whenever the unit carries no sampler object and the texture's BUILT-IN sampler is
// what applies. Keeping it one shutter per bit is also what keeps the per-subsystem
// A/B and the per-bit fire tallies meaning what they say - a bit gated on another
// bit's shutter measures neither. The extra fires a plain texture bind now costs
// are swallowed by the emitter's own set-hash suppressor, which MGPipeTypes.h makes
// mandatory for every kVarTail set for this exact traffic.
// Bit 13 mixes in the texture bind generation because glBindSampler moves only that
// one (through NoteTextureUnitTouched and TextureUnit::SetSamplerObject); the
// sampling-resolution generation moves only on parameter changes. A texture bind can
// change the applied sampler too, when the unit has no sampler object. Mixing it in,
// rather than gating the emitter on bit 12, keeps the per-bit fire tallies meaningful;
// the extra fires are absorbed by the emitter's set-hash suppressor. A missed fire
// here means wrong filtering, since bind_sampler_states has no pulled fallback.
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
ctx.GetTextureBindGeneration());
now[Index(MGPipeDirty::NewShaderImages)] = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages),
ctx.GetTextureBindGeneration());
// ---- bits 15/16/17, REWRITTEN AT P5e (sb, MG_Remote/CONTRACT-P5E.md §5.6) ----
//
// ALL THREE USED TO READ `buffers` - the buffer CONTENT aggregate - AND THAT WAS
// WRONG IN BOTH DIRECTIONS AT ONCE. Over: any glBufferSubData anywhere fired all
// three, which is the same width bit 10 was narrowed out of at P3a. Under, and this
// is the half that mattered: glBindBufferBase / glBindBufferRange mutate a binding
// point through a returned reference, which moves the slot's own Uint16 version and
// NOTHING the content aggregate reads - so
// `glBindBufferBase(UNIFORM,1,A); draw; glBindBufferBase(UNIFORM,1,B); draw` fired
// no bit at all. Harmless while nothing was emitted for these three; an
// under-fire the moment set_shader_buffers is, and under-firing renders stale,
// which this file's own rule calls the dangerous direction. Same defect class as
// P4a's glBindSampler hole (c0d), closed the same way: the shutter reads the
// generation the mutator actually moves (BufferState::NoteBindPointChanged).
//
// THE CONTENT AGGREGATE LEAVES ALL THREE and is not replaced by anything: whether
// the BYTES behind a bound buffer moved is the resource family's question, answered
// server-side by the resource record's own Serial, and the record these bits emit
// carries {handle, offset, size} - none of which a glBufferSubData changes. A base
// binding's extent is the one thing that could, and it does not travel resolved:
// Size is kMGPipeWholeBuffer and the server re-resolves it at use (§5.6).
//
// BIT 15 MIXES THE PROGRAM IDENTITY IN, for bit 12's and bit 14's reason one family
// over (fable seams F-1/F-2): the uniform window is resolved FOR THE PROGRAM IN USE
// - the UBO loop indexes it by the program's own block bindings - so a glUseProgram
// alone must re-open it. `shader` is that identity in both arms (lifetime id x link
// version, or stageLinks under a pipeline).
//
// BIT 16 IS TWO TARGETS, one bit: a storage-buffer bind and an atomic-counter bind
// are both "the shader's writable binding points moved", they are emitted together
// at the same validate point, and splitting them would buy one suppressed record on
// a workload that binds one without the other.
//
// BIT 17 KEEPS THE TRANSFORM-FEEDBACK GENERATION beside the new bind-point one:
// the capture points are span-scoped state latched at Begin, so when a span opens
// matters as much as what is bound. Nothing is emitted for it this phase (§5.7).
// Bits 15-17 read the buffer bind-point generations that glBindBufferBase/Range move
// (BufferState::NoteBindPointChanged), not the buffer content aggregate: the records
// carry {handle, offset, size}, a base binding's size travels as kMGPipeWholeBuffer
// and is resolved by the server at use, and content is the resource records' job.
// Bit 15 mixes in the program identity (`shader`) because the uniform window is
// indexed by the program's block bindings. Bit 16 covers storage and atomic-counter
// points, which are emitted together. Bit 17 adds the transform-feedback generation
// (capture points latch at Begin) and has no emitter.
now[Index(MGPipeDirty::NewConstBuffers)] = MGPipeMixShutter(
ctx.GetBufferBindPointGeneration(BufferTarget::Uniform), shader);
now[Index(MGPipeDirty::NewShaderBuffers)] = MGPipeMixShutter(
@@ -916,22 +683,12 @@ namespace MobileGL::MG_Pipe {
AttribDefaults& StagedAttribDefaults() { return m_stagedAttribs; }
const AttribDefaults& StagedAttribDefaults() const { return m_stagedAttribs; }
// ---- P3a D-H2: the draw's vertex-FETCH base instance ----
//
// It lives HERE rather than in a file static because bit 9's shutter has to see it:
// an ambient process global cannot cross a pushed boundary, and the value is now an
// explicit field of set_vertex_buffers and an input to its content hash, so a draw
// whose only change is its base instance has to reach the emitter. Set immediately
// before the fill at the three *BaseInstance draw entry points; CONSUMED and cleared
// by the validate point once it has been emitted, so a plain draw that follows one
// sees 0 again.
//
// THE CLEAR THAT ACTUALLY RUNS IN PRODUCTION IS THE VALIDATE POINT'S. MGPipeLeaveVerb
// clears it too, but no GL entry point calls MGPipeLeaveVerb - only MG_Test's
// ScopedPipeVerb and TrackerTest do - so the production guarantee is entirely
// PipeFill.cpp's, on BOTH of its exits: the end of step 3, and the no-live-context
// early return that skips step 3 altogether. Reset() deliberately does not clear it
// (see there): it is this call's argument, not a latch.
// The draw's vertex-fetch base instance, held here because bit 9's shutter mixes it in:
// it is a field of set_vertex_buffers and an input to its content hash. The three
// *BaseInstance draw entry points set it just before the fill. In production only the
// validate point clears it, on both exits of PipeFill.cpp (after emission and on the
// no-live-context early return); MGPipeLeaveVerb also clears it, but only tests call
// that. Reset() must not clear it: it is the current call's argument, not a latch.
void SetPendingBaseInstance(Uint32 baseInstance) { m_pendingBaseInstance = baseInstance; }
Uint32 PendingBaseInstance() const { return m_pendingBaseInstance; }
void ClearPendingBaseInstance() { m_pendingBaseInstance = 0; }
+5 -22
View File
@@ -78,28 +78,11 @@ namespace MobileGL::MG_Record {
void MarkWritableImageBufferTextures(MG_State::GLState::GLContext* context) {
if (!context) return;
// The backend keeps a bitset of writable image-buffer units
// (DirectGLES.cpp:2350-2352, maintained from its own SyncImageTextureBinding) and
// the client has no equivalent, so it sweeps. The sweep is NOT bounded by a device
// limit read: MaxImageUnits would be a backend read, and a stale or absent backend
// object would silently shorten the walk - which is the one direction this set may
// not fail in.
//
// IT IS BOUNDED BY THE FRONTEND's OWN IMAGE-UNIT HIGH-WATER MARK (P5d round 3,
// package C). The paragraph that stood here said the narrowing "belongs with P8's
// binding-walk migration"; the 2026-09-17 inproc profile moved it forward -
// MarkWritableImageBufferTextures was 2.15% self / 3.59% inclusive of the GL thread
// (GetImageTextureBinding 1.37 of it) at ~852 draws/frame, in a workload that never
// binds an image at all, purely because the walk was MAX_TEXTURE_IMAGE_UNITS (192)
// units wide unconditionally - 192 GetImageTextureBinding reads per draw for a
// context that has never bound one.
//
// AND THE MARK IS SAFE IN THE ONLY DIRECTION THAT MATTERS. It is written by
// glBindImageTexture itself (GL_Texture.cpp, TextureState::NoteImageUnitTouched) and
// it only ever grows - an unbind, a delete-unbind, a context that stops using an
// image unit all leave it where it was - so `unit <= mark` can only be WIDER than
// the set of units that hold a binding, never narrower. -1 means no image unit has
// ever been bound in this context, and then this is one compare.
// The client has no bitset of writable image-buffer units, so it sweeps them, bounded
// by the context's image-unit high-water mark rather than a backend limit, which a
// stale or absent backend object could shorten. glBindImageTexture sets the mark and
// it only grows (unbinding leaves it), so it never excludes a bound unit; -1 means no
// image unit was ever bound and the walk is one compare.
const Int highest = context->GetMaxTouchedImageUnit();
for (Int unit = 0; unit <= highest && unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
++unit) {
+7 -47
View File
@@ -6,53 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// THE CLIENT-SIDE CONSERVATIVE GPU-WRITE SET (ARCHITECTURE.md §16 names this component;
// CONTRACT-P5.md section 3, table 2's first set). Owner: package b1.
//
// WHY IT HAS TO MOVE SIDES. `BufferObject::SyncGpuWrites` (BufferObject.cpp:369) runs
// synchronously, on the application's thread, the moment the application calls glMapBuffer or
// glGetBufferSubData. It has to answer "did the GPU write this buffer since I last read it?"
// - and the six backend sites that rows 0-3 below mirror are all on the server's side of a split
// (DirectGLES.cpp's SyncBufferBindingPointsByRecord, SyncAtomicCounterBuffers and
// MarkWritableImageBufferTexturesGpuWritten; UniformManager.cpp's ResolveStorageTexelBufferDescriptor
// and ResolveStorageBufferDescriptor; WireDraw.inc's BeginWireXfbCaptureForDraw). Asking the server
// is a round trip the design forbids in steady state, so the
// client must build the set itself, from state it already owns: the SSBO and atomic-counter
// binding points, the image units whose Access is not GL_READ_ONLY, and the transform-feedback
// capture targets.
//
// CONSERVATIVE MEANS OVER-APPROXIMATE, AND THAT IS THE WHOLE DESIGN. The reverse channel's
// OnGpuWritten is a NARROWING channel (ResourceTracker.h:577-581): the client marks
// everything a shader COULD have written and the server only ever removes entries. So a row
// here that fires too often costs a readback; a row that fires too rarely reads a stale shadow
// and is silent. Every row below therefore mirrors its backend twin exactly, including the two
// places the twin is deliberately NARROW - a GL_READ_ONLY image binding is left alone, and the
// image walk runs from draw preparation rather than from glBindImageTexture's eager sync.
//
// NO NARROWING IN P5. ResourceTracker.h:587-592's `rangeCount == 1` assertion STAYS. Zero
// ranges will mean "a fully narrowed set - nothing is dirty" at P8/P9, and a package that
// reads ResourceTracker.h:577-581 alone will think it owns that already. It does not.
//
// THE TWO NEW PRODUCERS. Rows 4 and 5 have no backend twin: they are behaviour P5 ADDS
// (ARCHITECTURE.md §13.1), and both are strictly better than what monolith does.
// * glReadPixels into a pack PBO becomes fire-and-forget plus a client-side mark. Monolith
// maps the PBO and copies it into the shadow inside the call (DirectGLES.cpp:10983-10993),
// which is an unconditional stall on every glReadPixels whether or not anyone reads the
// shadow; marking instead defers the cost to the first read that actually wants it.
// * glEndTransformFeedback drops its unbounded ClientWaitSync (GL_Drawing.cpp:1367, timeout
// ~0ull) and marks the capture targets, for the same reason: the wait exists only so that
// a later MapBuffer sees real results, which is precisely what the flag is for.
//
// WHAT NO ROW COVERS, WRITTEN DOWN SO THE NEXT READER DOES NOT HAVE TO ASK. The set is built
// from the APPLICATION's bindings, so it says nothing about a backend's own scratch buffers -
// Espryt's converted-vertex-stream and primitive-restart substitution buffers, Magma's UBO
// ring. None of those has a MarkGpuWritten today either, so the client set is no NARROWER than
// monolith's and this is not a regression; it is a standing hole in both, and it stays one
// until the phase that migrates the backend's own allocations.
//
// GATED ON THE TRANSPORT, like everything else in this package: on the monolith path the six
// backend sites still run and a second marker would be new behaviour (D-J), and rows 4 and 5
// would remove a stall monolith is entitled to keep.
// Client-side conservative GPU-write set (ARCHITECTURE.md §13.1.2). glMapBuffer and
// glGetBufferSubData ask BufferObject::SyncGpuWrites, on the application thread, whether the GPU
// may have written a buffer; the backend's marking sites run on the server, so the client marks
// from its own bindings before each draw or dispatch record goes out. The set over-approximates:
// a missed mark reads a stale shadow silently, while an extra one costs a readback. Rows with a
// backend twin mirror it, including where the twin is deliberately narrow; the atomic-counter
// row is wider, in the safe direction. Backend-owned scratch buffers are not covered.
// P13 W5: moved from MG_Remote/Client/GpuWritePending.h - the record arm owns this set in a
// library without a transport too. The session's waits stay there.
@@ -52,22 +52,13 @@
"Pixel packing exceeds the destination or its addressable range."));
}
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): MAY A READ INTO THIS PACK BUFFER GO WITHOUT A
// REPLY? The answer is the buffer's handle when it may - the read then crosses as
// read_pixels_to_buffer / get_texture_image_to_buffer, lands server-side, and the buffer
// is marked GPU-written - and the null handle when the reply-slot form has to carry it.
// `end` is the last byte past the application's offset the read touches.
//
// Each refusal is a case the mark cannot keep its promise in, not a preference:
// - MOBILEGL_IPC_PBO_READBACK_SYNC=1, the A/B and red-once arm;
// - no readback route (BufferWritebackIsReachable: no size, or the resource family
// is not on the wire), so SyncGpuWrites would clear the mark with nothing fetched;
// - a MAPPED buffer. Only a persistent mapping can be live across a read, and its
// reader takes the bytes straight from the pointer through no call this side sees:
// no SyncGpuWrites would ever run for it, and the pixels would never arrive;
// - a range MGPSubData cannot name (a buffer offset stops at 2^31-1);
// - a buffer that never crossed: it has no handle for the server to write into, and
// the reply form's UploadSubData is what creates it.
// Returns the pack buffer's handle when a read into it may skip the reply: the read
// then crosses as read_pixels_to_buffer / get_texture_image_to_buffer, lands
// server-side, and the buffer is marked GPU-written. Returns null, selecting the
// reply-slot form, when that mark could not be honoured: MOBILEGL_IPC_PBO_READBACK_SYNC=1;
// no readback route (BufferWritebackIsReachable); a mapped buffer, whose persistent
// reader never goes through SyncGpuWrites; an `end` (last byte past the application
// offset) above MGPSubData's 2^31-1 offset limit; or a buffer with no handle yet.
MG_Pipe::MGPipeHandle PackBufferReadbackTarget(const SharedPtr<MG_State::GLState::BufferObject>& pbo,
Uint64 end) {
if (!pbo) return MG_Pipe::kMGPipeNullHandle;
+35 -112
View File
@@ -88,30 +88,12 @@ namespace MobileGL::MG_Record {
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
// ---- WHY THERE ARE TWO OF THESE KNOBS, measured rather than assumed -----------------
//
// MOBILEGL_IPC_E2_DROP_CLEAR came first and it does exactly what it says: every glClear
// stops at the client and no Clear record reaches the ring. It STILL COULD NOT TURN THE
// E2 RETRACE RED (joint-v1.md 3: SSIM 1.000000, mismatchPixels=0 with the knob armed and
// the arming WARN in the library's own log). That is not a broken knob, it is OpenRA:
// `apitrace dump` of openra.trace over the 31249 replayed calls counts 30 glClear, 30
// glXSwapBuffers and 788 glDrawArrays, and the final frame issues its clear at call
// 30197 and then covers the surface four times over with a terrain layer
// (`glDrawArrays(GL_TRIANGLES, first=56064, count=16128)` x4, under a scissor of
// -24,-24,688x528 over a 640x480 surface) before the snapshot at 31249. A frame that
// overdraws every pixel it clears has a picture that does not depend on the clear, so
// "drop the clear" is a control whose observable is invisible to THIS trace - R-16's
// exact defect, a gate that cannot go red for its own reason.
//
// MOBILEGL_IPC_E2_DROP_DRAW is the honest form of the same statement for a trace lane:
// drop every DrawVbo record and the picture can only be the clear colour. It is the
// control that makes "the wire carried the frame" falsifiable, because the thing it
// removes is the thing the golden is made of.
//
// DROP_CLEAR IS KEPT rather than retired: it drops a real record, it now publishes the
// count it dropped (below), and a scenario whose picture DOES depend on its clear -
// ClearThenReadPixelsScenario is the reduced path's target A - is where it is
// observable. What it is no longer allowed to be is E2's retrace control.
// Two negative controls. MOBILEGL_IPC_E2_DROP_DRAW drops every DrawVbo record, so a
// frame that really crosses the wire can show only the clear colour; it is the OpenRA
// retrace's control. MOBILEGL_IPC_E2_DROP_CLEAR cannot be: OpenRA's last frame clears,
// then covers the whole 640x480 surface four times before the snapshot, so dropping the
// clear leaves SSIM at 1.0 (measured). It stays for scenarios whose picture depends on
// the clear, such as ClearThenReadPixelsScenario.
void ArmControlKnobs() {
g_dropClearEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_CLEAR");
g_dropDrawEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_DRAW");
@@ -578,7 +560,7 @@ namespace MobileGL::MG_Record {
info.RestartIndex = 0;
info.DrawIdOffset = 0;
info.IndexResource = MG_Pipe::kMGPipeNullHandle;
info.MinIndex = ~0u; // "unknown", MGPipeTypes.h:1330
info.MinIndex = ~0u; // "unknown", MGPDrawInfo::MinIndex in MGPipeTypes.h
info.MaxIndex = ~0u;
info.XfbCpuCapturedVertices = 0;
info.NumDraws = 1;
@@ -591,29 +573,14 @@ namespace MobileGL::MG_Record {
EmitDrawRecord("DrawArrays", info, &range, 1, nullptr, 0, nullptr);
}
// =============================================================================
// P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots
// (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL
// call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink
// reproduces the backend call the monolith makes.
// =============================================================================
//
// Every entry point below is: read the bindings, plan the head and the ranges (the pure
// functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb
// hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record;
// ID-18), honours the E2 draw-drop control for every draw record and not only the P5
// one, stages a client index array into SEG_STAGE when there is one, and emits.
//
// Client arrays and client indices become owned ordinary buffer resources.
// Remaining invalid/unrepresentable draw shapes are named before encoding:
// +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect`
// would be a host pointer, which rule B forbids on the wire.
// +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has
// already raised INVALID_OPERATION for it; stated so the emitter
// cannot send a null handle where the sink dereferences one).
// +INDEX_OFFSET an element-buffer byte offset that is not a whole number of
// indices (or past 2^32 of them): the record spells Start in
// indices, and rounding would draw from the wrong element.
// ---- Indexed, instanced, multi-draw and indirect draws ----
// All nineteen slots emit one draw_vbo record, the GL call verbatim beside the handles,
// which the server's sink turns into the backend call the monolith makes. Each entry
// point reads the bindings, plans the head and ranges, then calls EmitDrawRecord: the
// pre-verb hooks in EmitDrawArrays' order (push, mark walk, record), the drop-draw
// control, client arrays and indices as owned buffers. Refused by name: +CLIENT_COMMANDS
// (no indirect buffer), +UNBOUND_PARAMETER (no parameter buffer; the sink dereferences
// it) and +INDEX_OFFSET (a byte offset that is not whole indices; Start counts indices).
[[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) {
char name[96];
@@ -974,28 +941,11 @@ namespace MobileGL::MG_Record {
nullptr, 0, nullptr);
}
// How many bytes glReadPixels will pack for this rectangle, from the PACK half of the
// pixel-store state.
//
// ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT.
// The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips,
// alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and
// the real backend honours the live pack state, so a 4x3 RGBA8 read with
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote
// to byte 120. That is the shape of the joint inproc lane's two
// DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends.
//
// So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack
// state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT -
// which is the side that holds the application's pack state, and the only side that
// can - scatters those rows into the application's pointer. "OnReadPixels writes
// exactly DstSize bytes" still holds; DstSize is now a number both sides derive from
// the same three values instead of one side deriving it from state the other cannot
// see.
//
// IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split
// entries touch take the same rule with no special case, because the rule is about the
// LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to.
// Bytes per pixel of a glReadPixels format/type pair, for the tight extent. The pack
// state never crosses for a read: DstSize is the tight w * h * bytesPerPixel run the
// server reads with neutral pack state, and the client scatters it under the
// application's GL_PACK_* state (ARCHITECTURE.md §13.1.3). The rule is about layout,
// so depth and depth-stencil reads need no special case.
Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) {
const TextureInputFormat inputFormat =
MG_Util::ConvertGLEnumToTextureInputFormat(format);
@@ -1284,27 +1234,13 @@ namespace MobileGL::MG_Record {
});
}
// =============================================================================
// CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block
// (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows.
// =============================================================================
//
// RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record
// carries the GL arguments verbatim - the enums as tokens, the GL names the backend
// keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's
// ServerVerbSink reproduces the backend call the monolith makes. The backend keeps
// reading the frontend state it reads today through the BARRIER-PULLED fields of its
// verb class, which the record's own verb stamp is what makes legal. So a migration is
// one emitter here plus one sink body there, and nothing in the backend moves.
//
// THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks
// below dispatch on the GL NAME - that is the whole point of carrying it - so a handle
// is carried for P7's sake only. FindByLifetimeId answers the handle the resource
// subsystem has already published for this object and kMGPipeNullHandle when it has
// published none; Acquire would MINT one here instead, at a call site that emits no
// create record, and the server would then be handed an identity it has never seen.
// A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a
// true statement, rather than a slot nobody allocated.
// ---- Image bind, compute, barriers, copy-image, storage-block binding ----
// Each verb crosses as the call: the record carries the GL arguments verbatim (enum
// tokens, GL names) beside the objects' handles, and the server's verb sink makes the
// backend call the monolith makes. Handles are looked up with FindByLifetimeId, never
// minted: this call site emits no create record, so a minted handle would name an
// object the server has never seen. A null Res/Src/Dst/ShaderCso means none is
// published yet.
MG_Pipe::MGPipeHandle PublishedTextureHandle(
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
@@ -1512,27 +1448,14 @@ namespace MobileGL::MG_Record {
nullptr, 0, nullptr, 0, nullptr);
}
// CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the
// tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2).
// =============================================================================
//
// SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a
// control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets
// the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(),
// which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no
// shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE
// TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span,
// after the record and before GLContext::EndTransformFeedback clears the live bindings,
// which is exactly where b1 already put it (GL_Drawing.cpp's
// MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same
// buffers twice and taking it INSTEAD of there would mark nothing.
//
// P5f fe extends Begin with the immutable capture snapshot: program CSO handle,
// XFB object lifetime id, and four buffer handle/range pairs. Buffer resource records
// precede this verb through BeforeReadOnlyVerb; AcquireShaderCso below publishes the
// program archive before Begin. Deferred driver Begin and End therefore need no
// frontend program or binding-point lookup. set_stream_output_targets stays unused:
// this state changes at the capture-span boundary, so it rides Begin itself.
// ---- Transform-feedback spans, XFB object bind, patch parameter ----
// These six start no shader (they open, close, pause, resume or retarget a capture
// span, or set the patch size), so each takes BeforeReadOnlyVerb(). The capture
// targets' GPU-write mark belongs at the end of the span, where GL_Drawing.cpp calls
// MarkEndTransformFeedbackCaptureTargets before the bindings are cleared; marking here
// too would mark twice. Begin carries the capture snapshot (program CSO, XFB lifetime
// id, four buffer ranges), so the deferred driver Begin/End need no frontend lookup;
// the state changes only at span boundaries, so set_stream_output_targets is unused.
void EmitBeginTransformFeedback(GLenum primitiveMode) {
VerbChannel session = ChannelFor("BeginTransformFeedback");
+14 -41
View File
@@ -81,23 +81,12 @@ namespace MobileGL::MG_Record {
// Returns the previous routing.
Bool SetMonolithVerbPortRoutingForTesting(Bool viaPort);
// THE E2 NEGATIVE CONTROLS (t1's debt against c1, BRIEF §7). When set, the named emitter
// SKIPS its record - it still runs the pre-verb hooks and still returns - so a replay that
// is really going through the wire loses that verb while a replay that fell through to the
// driver is unaffected. They are functions rather than knobs in Config.h for two reasons:
// a control has to be settable from a test process that has already started, and a knob
// would be a MOBILEGL_IPC_-shaped name for something no operator may ever set.
//
// Emissions actually skipped, so a control can assert that it DID something rather than
// that a picture changed - a control that silently never fired is the third shape of R-16's
// "a gate that cannot go red for its own reason".
//
// WHICH ONE E2'S RETRACE USES, and it is not the clear. Measured on the joint head: with
// MOBILEGL_IPC_E2_DROP_CLEAR=1 armed and its WARN in the library's own log, the OpenRA
// retrace under inproc still scored ssim=1.000000 / mismatchPixels=0 (joint-v1.md §3),
// because OpenRA covers every pixel it clears before the snapshot. Dropping the DRAWS is
// the control whose observable the golden is actually made of. See VerbPort.cpp's
// ArmControlKnobs for the trace census that settled it.
// Negative controls for the retrace lanes. When set, the named emitter skips its record but
// still runs the pre-verb hooks, so a replay that really goes through the wire loses the
// verb while one that fell through to the driver does not. Functions rather than Config.h
// knobs: a running test process sets them, and no operator may. The counts let a control
// assert that it fired. The OpenRA retrace uses the draw drop: OpenRA overdraws every pixel
// it clears, so dropping the clear leaves SSIM at 1.0 (see ArmControlKnobs in VerbPort.cpp).
void SetDropClearEmissionForNegativeControl(Bool drop);
Uint64 DroppedClearEmissions();
void SetDropDrawEmissionForNegativeControl(Bool drop);
@@ -126,29 +115,13 @@ namespace MobileGL::MG_Record {
GLsizei height, Uint64 bytesPerPixel,
const PixelStoreParameters& pack);
// P7 gate 5 (g5-readback). A glReadPixels whose tight answer is larger than one reply slot
// is NOT one record: the client splits the w*h rectangle into BANDS, emits each band as an
// ordinary read_pixels record with its own box and its own tight DstSize (so every answer
// still fits one slot, ID-47, and the server's PH-3 bound - its tight answer against its own
// LinkTerms.maxReplyBytes - holds unchanged), and scatters each band into the application's destination under its
// own GL_PACK_* state. No reply is chunked; the READ is.
//
// THE SHAPE. Whole-width row bands of floor(maxReplyBytes / rowBytes) rows when a row fits
// a reply; otherwise every row is cut into column pieces of floor(maxReplyBytes / bpp)
// pixels. Both shapes are contiguous in the tight layout, which is what lets the neutral-
// pack fast path keep reading straight into the application's pointer. Bands are in
// row-major order and cover the rectangle exactly once.
//
// NO PLAN exactly when not even ONE pixel fits a reply (maxReplyBytes < bytesPerPixel,
// including a link with no reply pool at all); the emitter then refuses BY NAME through
// ID-47's unchanged helper rather than emitting anything. That is the only ReadPixels shape
// left that ends in Fatal{ReplyTooLarge}: CONTRACT-P5 row 23 makes an answer that cannot fit
// a slot a named Fatal, not a GL error, and no pixel can be returned in pieces smaller than
// itself.
//
// THE PLAN IS TWO NUMBERS AND THE BANDS ARE WALKED, NOT LISTED: a band list would be one
// allocation per read that grows with the read, and the walk below is the only place the
// order and the coverage are decided - the emitter and the unit control both call it.
// A glReadPixels whose tight answer exceeds one reply slot is split into bands, each an
// ordinary read_pixels record with its own box and tight DstSize, scattered under the
// application's pack state (ARCHITECTURE.md §13.1.2). Bands are whole-width row runs when a
// row fits a reply, else column pieces of one row; they go in row-major order and cover the
// rectangle once. No plan means not even one pixel fits, and the emitter refuses by name
// (Fatal{ReplyTooLarge}). The plan is two numbers and bands are walked, not listed, so a
// read allocates nothing and the emitter and its unit test share one walk.
struct ReadbackBand {
Uint64 FirstRow = 0;
Uint64 Rows = 0;
@@ -200,7 +173,7 @@ namespace MobileGL::MG_Record {
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected);
// =============================================================================
// P5b d1 - the draw family's record plan (MG_Remote/CONTRACT-P5B.md §2 d1)
// P5b d1 - the draw family's record plan (docs/archive/contracts/CONTRACT-P5B.md §2 d1)
// =============================================================================
//
// The nineteen indexed / instanced / multi-draw / indirect entry points all ride draw_vbo
+21 -62
View File
@@ -10,33 +10,13 @@
#include <Includes.h>
#include <Config.h>
// The CLIENT side of P3a's vertex-input family (brief D-G, D-H, D-I): the bound VAO's
// format as create/bind_vertex_elements, its buffers as set_vertex_buffers with an explicit
// baseInstance, and its element binding as set_index_buffer.
//
// UNLIKE THE RESOURCE FAMILY, these three emit at the VALIDATE POINT, from
// MGPipeValidateForVerb's step 3 in the fixed order elements -> buffers -> index. That is
// the ordinary rule (ARCHITECTURE.md 5.1); the resource family is the one exception to it.
//
// THE CSO IS IDENTITY-ADDRESSED, NOT CONTENT-ADDRESSED (D-G1, a recorded deviation from
// ARCHITECTURE.md's 1024-entry content-addressed scheme). One handle per frontend
// VertexArrayObject, minted off its lifetime id, and create_vertex_elements is RE-ISSUED on
// the same handle whenever the configuration moves - legal, because MGPipeHandle::Gen
// increments only on slot reuse and never on a respecify. Espryt has no vertex-elements CSO
// to share: its twin owns one driver VAO name plus 64 scratch buffer ids, which two frontend
// VAOs cannot share, so content addressing would be strictly slower on the only backend this
// phase touches. P7 adds the hash-probe-memcmp layer above these same three calls when
// Magma's VertexInputStateFactory takes the CSO over.
//
// WHAT THE UNIT GATE READS. G6 is "the emitted blob + set + index record reproduce exactly
// what the backend's VAO twin reads from the frontend today, field by field, for all 32
// slots", and G7 is a scripted control that stops the conversion copying ONE field and
// expects the suite to go red NAMING it. So the conversion is a pure function per field
// (MGPipeBuildVertexAttribWire / MGPipeBuildVertexBindingPointWire) and the staging buffers
// the emitter builds into are readable afterwards - the emitter passes m_blob and m_entries
// straight to the applier, so "what was emitted" costs no copy at all.
//
// HEADER-ONLY, for the ownership reason Tracker.h states in full.
// Client side of the vertex-input family: the bound VAO's format as create/bind_vertex_elements,
// its buffers as set_vertex_buffers (with an explicit baseInstance) and its element binding as
// set_index_buffer, emitted at the validate point in that order. The vertex-elements CSO is
// addressed by VAO identity, not content (ARCHITECTURE.md §2.3): one handle per VAO lifetime id,
// with create_vertex_elements re-issued on it when the configuration moves, because Espryt's
// twin owns a driver VAO that two VAOs cannot share. The wire conversion is one pure function
// per view, and the emitter builds into members tests can read back without a copy.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -57,25 +37,14 @@ namespace MobileGL::MG_Pipe {
// D-G2: the wire conversion, one pure function per view
// ---------------------------------------------------------------------------------
// EVERY FIELD OF VertexAttribute THE WIRE FORM CARRIES, and nothing else:
//
// Divisor is deliberately absent - it is resolved per binding point and travels in
// MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads
// it. Carrying it twice would let a malformed record disagree with itself.
// LegacyStride / LegacyPointer are deliberately absent - they are the
// glGetVertexAttrib* query answers and nothing but the query path reads them, so
// they stay client-side.
// Buffer is deliberately absent - identity travels in set_vertex_buffers, which is
// what keeps this record stable while the buffers under it change.
// Stride is the RESOLVED distance and a surviving 0 is MEANINGFUL: a pointer call's 0
// was already resolved to the element size by the frontend, so a 0 here can only
// have come from the binding model, where it means every vertex reads the SAME
// element. Collapsing it back into the element size is what made
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
// IsLong travels SEPARATELY from Type == Float64: VertexAttribFormat(GL_DOUBLE) reads
// doubles and asks for them converted to float, VertexAttribLFormat keeps all 64
// bits, and the backend's fp64 narrowing and its Adreno disabled-attribute
// workaround both key on telling the two apart.
// The wire form carries every VertexAttribute field except Divisor (sent once, per binding
// point, in MGPVertexBuffer::Divisor), LegacyStride/LegacyPointer (query answers, client-only)
// and Buffer (identity travels in set_vertex_buffers, so this record stays stable while the
// buffers change). Stride is the resolved distance: a 0 here comes only from the binding
// model and means every vertex reads the same element, so it must not become the element
// size. IsLong is separate from Type == Float64: VertexAttribFormat(GL_DOUBLE) converts to
// float, VertexAttribLFormat keeps 64 bits, and the backend's fp64 narrowing and Adreno
// disabled-attribute workaround tell the two apart.
inline MGPVertexAttribWire MGPipeBuildVertexAttribWire(const MG_State::GLState::VertexAttribute& attrib,
Uint32 bindingIndex) {
// ASSERT RATHER THAN ASSUME, in both directions, because the three narrowing casts
@@ -425,22 +394,12 @@ namespace MobileGL::MG_Pipe {
Uint64 VertexBufferSetCount() const { return m_bufferSets; }
Uint64 IndexBufferSetCount() const { return m_indexSets; }
// ---- C-1: "does the applier hold a record for exactly this handle?" ----
//
// The CSO's death path (MGPipeEmitVertexElementsDestroyAndFree) needs that answer and
// MUST NOT GUESS IT FROM THE SLOT. A VertexElementsCso slot can exist with no record
// behind it, because a backend that keys its twins on the handle mints the slot itself
// (DirectGLES' BackendSlotTable::GetOrCreate -> MGPipeSlots().Acquire) whether or not
// bit 8 ever asked this client to emit anything - which is exactly what a
// MOBILEGL_PIPE_PUSH=0x7f lane runs. delete_vertex_elements on such a handle is a
// REFUSED call, and the applier's resolver asserts on a refusal
// (PipeApply.cpp's ResolveVertexElements), i.e. a stop in a verify build.
//
// Kept OUT of Reset(), unlike the create/bind latch beside it, and for the mirror
// image of Reset()'s own reason: "a fresh context is a fresh server" is true of the
// per-context half of this table, and object RECORDS are precisely what
// MGPipeApplierReset does not clear (PipeApply.h's two halves). This half tracks those
// records, so it lives exactly as long as they do.
// Whether the applier holds a vertex-elements record for exactly `handle`. The death
// path (MGPipeEmitVertexElementsDestroyAndFree) must ask this instead of inferring it
// from the slot: a backend that keys its twins on handles (DirectGLES'
// BackendSlotTable) can mint a VertexElementsCso slot with no record behind it, and
// delete_vertex_elements on such a handle is refused, which the applier asserts on.
// This half of the latch survives Reset(), because MGPipeApplierReset keeps records.
Bool RecordIsPublished(MGPipeHandle handle) const {
if (MGPipeHandleIsNull(handle)) return false;
const SizeT slot = handle.Slot;
+1 -1
View File
@@ -6,7 +6,7 @@
* SPDX-License-Identifier: LGPL-3.0-only
* End of Source File Header */
/* SHARED IMAGES, AS A C ABI (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
/* SHARED IMAGES, AS A C ABI (docs/archive/notes/anland/plan-ahb-dmabuf.md).
*
* libMobileGL.so exports these on Linux (not Android) in split builds, so a component that is not
* a GL entry point - the GBM backend (MG_Gbm), above all - can ask the server for an image and
File diff suppressed because it is too large Load Diff
@@ -56,7 +56,7 @@ namespace MGITest {
SamplerCso,
SamplerViewCso,
// ShaderCso covers BOTH the ordinary program slots and the program-pipeline COMPOSITES
// minted out of the reserved high band (MGPipeHandles.h:86-107, D-H7). One kind, because
// minted out of the reserved high band (kMGPipeShaderCsoCompositeSlotBase). One kind, because
// that is what the allocator has: the band is a second dense table inside the same kind
// and LiveCount counts both.
//
@@ -33,7 +33,7 @@
// MOBILEGL_TRANSPORT=inproc` (review finding M-5).
// 2. `ClientSession::Active() != nullptr`. c0 made this one deliberately return null rather
// than Fatal, because "does a session exist" has a legitimate "no" - it is the monolith
// answer (ClientSession.cpp:31). So it is exactly "a client session exists in this process",
// answer (ClientSession::Active's definition). So it is exactly "a client session exists in this process",
// and no amount of editing stub MESSAGES makes a null pointer non-null.
// 3. `ImplementedVerbCount() > 0`. c0's stub returns 0; the contract gives this function the
// job of making "a table that silently lost an emitter" distinguishable from "a table that
@@ -31,7 +31,7 @@
# are the monolith-arm and the no-pulled-read scenarios, 4 skips are the two PersistentMapArm-
# and two StrictArming-only cases skipping outside their own lanes, all by design).
# Entry counts are first-blocker counts: an entry aborts on its FIRST pulled read, so retiring
# a row advances those entries to their next one. See docs/Disaggregated/notes/p5f/f1-report.md.
# a row advances those entries to their next one. See docs/archive/notes/p5f/f1-report.md.
# ---------------------------------------------------------------------------------------------
#
# P5f wave 2: the expected set is empty after fc -> fe -> fm integration.
+2 -2
View File
@@ -19,7 +19,7 @@
# monolith DirectGLES. / DirectVulkan. case is on a gated split arm or is named, with a reason
# class, in scripts/data/split_coverage_exemptions.txt. Deleting a line below reds it BY NAME.
# The per-case census this list was cut from (inproc / spawn / tcp, junit per arm) is
# docs/Disaggregated/notes/p8/A.md.
# docs/archive/notes/p8/A.md.
# ---- (a) THE AMBIENT CASES -----------------------------------------------------------------
#
@@ -190,7 +190,7 @@ set(MGL_P8A_KNOB_FILTER_WidenedPacked16 "CopyImagePacked16Scenario.*")
set(MGL_P8A_KNOB_ENV_WidenedPacked16 "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1")
# ForcedDs keeps the spelling the existing Split/Spawn DepthStencilReadbackMatrix block uses. The
# default-framebuffer case was left out here while it was red on spawn and tcp; P8-SE fixed it
# (notes/p8/SE.md) and it runs under the knob on every arm like the rest.
# (docs/archive/notes/p8/SE.md) and it runs under the knob on every arm like the rest.
set(MGL_P8A_KNOB_FILTER_ForcedDs
"DepthStencilReadbackScenario.*:DepthStencilReadbackAttachmentShapeScenario.*")
set(MGL_P8A_KNOB_ENV_ForcedDs "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1")
@@ -7,7 +7,7 @@
// End of Source File Header
//
// Scenario - glCopyImageSubData INTO A LEVEL THE DRIVER WILL NOT READ BACK (P8-E,
// docs/Disaggregated/notes/p8/E.md).
// docs/archive/notes/p8/E.md).
//
// The formats are the ones an ES driver keeps as texture-only storage: GL_RGB16 (EXT_texture_norm16
// makes the three-channel spelling sampleable, never colour-renderable) and the 16-bit SNORM family
@@ -28,13 +28,13 @@
// (WireTextureReadback.inc, ReadTextureImageWire): a colour level of exactly that extent is
// treated as a level the driver refused to attach - what a driver without EXT_texture_norm16 /
// EXT_render_snorm does for these formats (an Adreno 750, which has both, reads them all back
// through the GPU; measured in P13), and what llvmpipe can be made to do no other way. The `DirectGLES.<arm>.StoreRead.`
// entries set it, and this scenario then builds its textures at THAT extent instead of the default
// one - so the knob is scoped by the extent, and the tcp lane's shared supervisor can carry it
// (the TcpServer.Start fixture's ENVIRONMENT) without touching any other entry, this scenario's
// own knob-free tcp entries included. Under the knob every read must still be right, and the
// server log must name the store route - otherwise the entry would be pinning the GPU route a
// second time under a name that claims otherwise.
// through the GPU; measured in P13), and what llvmpipe can be made to do no other way. The
// `DirectGLES.<arm>.StoreRead.` entries set it, and this scenario then builds its textures at
// THAT extent instead of the default one - so the knob is scoped by the extent, and the tcp
// lane's shared supervisor can carry it (the TcpServer.Start fixture's ENVIRONMENT) without
// touching any other entry, this scenario's own knob-free tcp entries included. Under the knob
// every read must still be right, and the server log must name the store route - otherwise the
// entry would be pinning the GPU route a second time under a name that claims otherwise.
#include <cstdint>
#include <cstdio>
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, end to end over inproc.
// P5c ct (docs/archive/contracts/CONTRACT-P5C.md §5): the two control records, end to end over inproc.
//
// applier_reset (§5.1): this process's first validate primes the pipe tracker, and the
// FreshlyPrimed edge is the record's producer (MG_Impl/Pipe/PipeFill.cpp). What the scenario
@@ -155,7 +155,7 @@ namespace {
// (DirectVulkan.cpp, CONTRACT-P7 §5.5) and emits the same `object_death`. Package L
// measured that the SPAWN arm already passed once the WireTables install condition
// went backend-agnostic and that the INPROC arm failed here with `deaths` 0 vs 0
// (notes/p7/magma-two-process-first-run.md §6); the table is what closes the inproc
// (docs/archive/notes/p7/magma-two-process-first-run.md §6); the table is what closes the inproc
// half, and this case is the gate on it. It is also the RED-ONCE for that table: with
// the install site short-circuited, both cases red on the two EXPECT_GT below.
const GLubyte red[4] = {255, 0, 0, 255};
@@ -259,7 +259,7 @@ namespace {
// "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION, ALWAYS"; T0/T1 are
// unimplemented, refused by name at the handshake since P11 A1). So the capability is wired
// AHEAD of its consumer on purpose, and `rsd=` on the PipeStats line is the counter that
// will show the records the day P11 lands a tier that adopts. See notes/p7/magma-c.md.
// will show the records the day P11 lands a tier that adopts. See docs/archive/notes/p7/magma-c.md.
TEST_F(CtWireScenario, TheServerPublishesTheResidentSubDataCapabilityFromItsOwnTable) {
if (!Ready()) return;
// A REAL WORKLOAD FIRST, and it is not decoration: ScenarioFixture's arming rule
@@ -534,7 +534,7 @@ namespace MGITest {
// THE CASE IS ARMED ON THE MAGMA WIRE ARM ONLY, and the two gates below say which of the
// other arms that discover it (the suite is registered whole on the monolith DirectVulkan
// arm and on every DirectGLES arm) owe this picture and do not yet produce it. Both are
// recorded debts (notes/p7/magma-b2.md §6), not agreements with the wrong answer:
// recorded debts (docs/archive/notes/p7/magma-b2.md §6), not agreements with the wrong answer:
// - Espryt (DirectGLES, monolith and split alike): the flipped resolve writes nothing and
// raises no error, and the scaled one raises no error either. P3b/P4b.
// - the monolith DirectVulkan arm: `VulkanRenderer.cpp` refuses a depth blit with a
@@ -560,17 +560,19 @@ namespace MGITest {
TEST_F(DepthStencilReadbackMatrixScenario, AFlippedMultisampleResolveMirrorsTheBandsAndAScaleDeclines) {
if (!Ready()) return;
if (Gl().BackendName() != "DirectVulkan") {
GTEST_SKIP() << "Espryt (" << Gl().BackendName() << ") does not produce this picture yet: a "
GTEST_SKIP() << "Espryt (" << Gl().BackendName()
<< ") does not produce this picture yet: a "
"flipped multisample depth resolve writes nothing with no error and a scaled "
"one raises no INVALID_OPERATION - a P3b/P4b debt (notes/p7/magma-b2.md §6)";
"one raises no INVALID_OPERATION - a P3b/P4b debt (docs/archive/notes/p7/magma-b2.md §6)";
}
if (!PeekSplitRuntime().transportResolved) {
GTEST_SKIP() << "the monolith DirectVulkan transport (this process resolved no split "
"transport, so BlitFramebuffer takes the monolith arm) refuses a flipped "
"depth blit (\"depth blits with flipped rectangles are not supported yet\", "
"destination untouched, no error) and scales a multisample depth blit GL "
"calls INVALID_OPERATION - a wire-vs-monolith divergence where the wire arm "
"is the correct one; the monolith fix is P13/G1-bound (notes/p7/magma-b2.md §6)";
GTEST_SKIP()
<< "the monolith DirectVulkan transport (this process resolved no split "
"transport, so BlitFramebuffer takes the monolith arm) refuses a flipped "
"depth blit (\"depth blits with flipped rectangles are not supported yet\", "
"destination untouched, no error) and scales a multisample depth blit GL "
"calls INVALID_OPERATION - a wire-vs-monolith divergence where the wire arm "
"is the correct one; the monolith fix is P13/G1-bound (docs/archive/notes/p7/magma-b2.md §6)";
}
DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4);
if (!SourceIsUsable()) {
@@ -1175,7 +1175,7 @@ TEST_F(F1WireScenario, SubWordUniformBufferRangePadsMissingBytes) {
// The sibling of ShortUniformBufferRangePadsMissingBytes whose bound range does NOT
// end on a four-byte boundary. That window is what `uniform-buffer-byte-tail`
// used to refuse (a named Fatal that took the whole session with it), and it is the
// live boundary the Redmi Minecraft run reported (notes/p5f/magma-inproc-fix.md #5).
// live boundary the Redmi Minecraft run reported (docs/archive/notes/p5f/magma-inproc-fix.md #5).
//
// Only the SIZE can land off a word here. glBindBufferRange constrains the offset to
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and records GL_INVALID_VALUE otherwise
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// P10 (A, MG_Remote/CONTRACT-P10.md §1): FENCE POLLS ANSWERED FROM THE REVERSE CHANNEL.
// P10 (A, docs/archive/contracts/CONTRACT-P10.md §1): FENCE POLLS ANSWERED FROM THE REVERSE CHANNEL.
//
// A glClientWaitSync / glGetSynciv(GL_SYNC_STATUS) used to be a kWaitReply record: its answer
// came back only after the server had applied every record before it, so a poll was a full
@@ -7,7 +7,7 @@
// End of Source File Header
//
// Scenario - glGenerateMipmap ON THE FORMATS ESPRYT DOES NOT HAND STRAIGHT TO THE DRIVER
// (P8-B, docs/Disaggregated/notes/p8/B.md).
// (P8-B, docs/archive/notes/p8/B.md).
//
// Espryt generates R11F_G11F_B10F and depth chains with per-level blits, and RGB16F / RGB32F
// chains outside glGenerateMipmap because ES refuses a three-channel float format there. Until
@@ -819,12 +819,11 @@ void main() { oColor = texture(uTex, vUv); }
EXPECT_EQ(liveAfter, liveBefore)
<< kChurn << " " << kindName << SpaceSuffix(space)
<< " objects were created, drawn with and destroyed and "
<< (liveAfter - liveBefore)
<< " objects were created, drawn with and destroyed and " << (liveAfter - liveBefore)
<< " slots never came back. Each one holds a SlotState, a lifetime-id map node "
"and the applier's record for the life of the process, and past the kind's "
"slot bound every create trips Fatal{ProtocolCorruption} for good "
"(PipeApply.h:97-113). This is P3a's C-1 defect, which is why every P4a kind "
"(PipeApply.h's slot bounds). This is P3a's C-1 defect, which is why every P4a kind "
"frees its slot from the frontend destructor through one client-side helper "
"(D-I1) rather than from a backend death table. Backend "
<< Gl().BackendName();
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - THE DRAW FAMILY P5b's d1 PUTS ON THE WIRE (MG_Remote/CONTRACT-P5B.md §2 d1): one
// Scenario - THE DRAW FAMILY P5b's d1 PUTS ON THE WIRE (docs/archive/contracts/CONTRACT-P5B.md §2 d1): one
// VBO, one EBO, one program, one VAO, and one picture per draw entry point whose correctness
// DEPENDS on the fields that entry point adds to draw_vbo. Two quads live in the vertex buffer,
// a left one and a right one, and every case draws exactly one of them or both through a
@@ -611,7 +611,7 @@ void main() { word = 0xC0FFEEu; }
<< window.line;
}
// P11 A2 (MG_Remote/CONTRACT-P11.md §2): WHICH TIER THE ARENA REALLY LANDED IN. The cases above
// P11 A2 (docs/archive/contracts/CONTRACT-P11.md §2): WHICH TIER THE ARENA REALLY LANDED IN. The cases above
// are green whether the arena adopted or stayed emulated, so the lane declares the arm
// (MGITEST_PERSISTENT_MAP_ARM) and this case asserts it, the way
// PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares does for a sub-16-MiB map.
@@ -665,7 +665,7 @@ void main() { word = 0xC0FFEEu; }
ExpectDeclaredArm(declared, "after a frame of draws, an SSBO binding and a dispatch");
}
// P11 A1 (MG_Remote/CONTRACT-P11.md §1): THE TCP LANE WITH MOBILEGL_IPC_ADOPT_TIER=0. A stream
// P11 A1 (docs/archive/contracts/CONTRACT-P11.md §1): THE TCP LANE WITH MOBILEGL_IPC_ADOPT_TIER=0. A stream
// data plane has no memory the two sides share, so the client refuses the knob by name at its
// handshake - exactly one `Refuse{AdoptTierOnStream, "T0"}` line in its log - and the session
// runs T2: the other cases in the same `.AdoptTier0.` lane are green and the arena is emulated.
@@ -699,7 +699,7 @@ void main() { word = 0xC0FFEEu; }
EXPECT_EQ(log.find("Fatal{UnimplementedAdoptTier"), std::string::npos) << log;
}
// P11 B2 (MG_Remote/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT.
// P11 B2 (docs/archive/contracts/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT.
// MOBILEGL_IPC_ADOPT_TIER=0 over shared segments asks T0 in the Hello (A1 made it die at the
// handshake; ruling ID-P11-14 made it this). The lane's server cannot grant it - a host has no
// AHardwareBuffer, or `.AdoptT0Disallowed.` turned its allow switch off - so it refuses by name
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): READS INTO A PIXEL PACK BUFFER ARE ANSWERED BY NOTHING.
// P9 (W1, docs/archive/contracts/CONTRACT-P9.md §1): READS INTO A PIXEL PACK BUFFER ARE ANSWERED BY NOTHING.
//
// Under a transport a glReadPixels / glGetTexImage whose destination is a bound
// GL_PIXEL_PACK_BUFFER no longer waits for its pixels: it crosses as read_pixels_to_buffer /
@@ -418,7 +418,7 @@ namespace MGITest {
// under a transport a pack-buffer read posts NO reply-slot record and marks the buffer
// GPU-written, once per read. MOBILEGL_IPC_PBO_READBACK_SYNC=1 puts the reply form back,
// and this case must then fail on both counters - that is its red-once, recorded in
// docs/Disaggregated/notes/p9/.
// docs/archive/notes/p9/.
TEST_F(PackBufferReadbackScenario, APackBufferReadPostsNoReplyAndMarksTheBuffer) {
if (!Ready()) return;
const std::string split = SplitRuntimeSkipReason();
@@ -290,7 +290,7 @@ void main() { oColor = vec4(vColor, 1.0); }
// undefined and this function returned at the line above before asserting anything;
// the split build's monolith lane, where the tracker IS compiled, adopts
// (MGPipeApplyMapPersistent's R-6 decline is ANDed with `Transport != Monolith`,
// PipeApply.cpp:2005), and seven entries went red the first time the assertion ran at
// PipeApply.cpp), and seven entries went red the first time the assertion ran at
// all.
//
// So the expectation is `live == (arm == emulated)`, taken from the SAME peek
@@ -11,8 +11,8 @@
// P3a migrates two subsystems: kMGPipeSubsystemResources (bit 7, the resource_* family) and
// kMGPipeSubsystemVertexInput (bit 8, vertex elements / buffers / index). The push build's default
// mask becomes kMGPipeSubsystemsMigratedAtP3a = 0x1ff, and P2's 0x7f survives as the control that
// clears exactly those two bits - MGPipe.h:79's rule that every phase's constant keeps meaning what
// it meant, so an operator's recorded mask is still readable a phase later.
// clears exactly those two bits - the rule in MGPipe.h's MOBILEGL_PIPE_PUSH bitmask section that every
// phase's constant keeps meaning what it meant, so an operator's recorded mask is still readable a phase later.
//
// That A/B is what every "push vs pull" number in MEASUREMENTS.md is taken against, and it has one
// characteristic failure mode: the bits stop steering anything, both arms run the same code, and
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT (P9 W2, notes/p9/W2-REMINT.md).
// Scenario - A TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT (P9 W2, docs/archive/notes/p9/W2-REMINT.md).
//
// The name is the ROADMAP's: P4a named `texture-remint-pull` the one new stall class of the
// split - a texture first bound to an image unit AFTER the backend allocated it has to be
@@ -33,7 +33,7 @@
// table below.
//
// THE CLIENT EMITTER LANDED TOO, and three places in the tree said otherwise until this commit:
// this header, the CMake probe's comment, and notes/p4a/p4a-results/gates-v1.md's re-run table.
// this header, the CMake probe's comment, and docs/archive/notes/p4a/p4a-results/gates-v1.md's re-run table.
// MG_Impl/Pipe/TextureEmit.h's EmitOneLevel increments CallClass::ClientTextureUploadEmissions on
// every piece it emits, so `ctu=` is a live reading and `ctu == emit` is a live assertion. The
// CMake probe's `else()` arm is kept - it is what makes the arming decision falsifiable - but it
@@ -30,7 +30,7 @@
// That is exactly the shape SSIM and every ordinary pixel case go green over.
//
// SO THE CLAIM IS ASSERTED ON BOTH CHANNELS AT ONCE, and neither alone is the gate
// (notes/recovered/wf2/final/part3.md 7.3, "门:新增场景,通过 view 上传、经属主采样(以及反
// (docs/archive/notes/recovered/wf2/final/part3.md 7.3, "门:新增场景,通过 view 上传、经属主采样(以及反
// 向),跨 draw 边界各一次"):
//
// PIXELS an upload through one name must be visible through the other name, in the SAME
@@ -51,7 +51,7 @@
// MarkStorageDirtyRegion call PipeNoteLevelDirty on ITSELF instead of forwarding to the owner -
// i.e. build the two-cursor shape the design rejects. Case (a) then goes red on stale OWNER
// texels AND on a SubDataCount that did not move, which is the pair this file exists to make
// simultaneous. Recorded in docs/Disaggregated/notes/p34b/espryt-d2.md.
// simultaneous. Recorded in docs/archive/notes/p34b/espryt-d2.md.
//
// VIEWS ARE OPT-IN ON ESPRYT. DirectGLES withholds GL_ARB_texture_view unless
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 (ConfigLoader.cpp:213, Config.h:72), so every lane that
@@ -13,7 +13,7 @@
// in every lane; the DirectGLES.Split. entries run the same two cases with
// MOBILEGL_TRANSPORT=inproc, where the same body is the smallest workload that crosses a real
// ring. Four things about its shape are decisions rather than defaults, and all four come from
// the measured verb census (~/w7/notes/p5/verb-census.md):
// the measured verb census (docs/archive/notes/p5/verb-census.md):
//
// 1. THE DRAW IS VBO-BACKED AND HAS NO CLIENT-ARRAY INDICES. glDrawArrays against a buffer
// bound to GL_ARRAY_BUFFER, never glDrawElements with a client pointer. That keeps
@@ -7,7 +7,7 @@
// End of Source File Header
//
// P8-SV: THE MAGMA WIRE ARM ISSUES glDispatchComputeIndirect FROM THE WIRE STORE, AS THE MONOLITH
// ARM DOES (docs/Disaggregated/notes/p8/SV.md).
// ARM DOES (docs/archive/notes/p8/SV.md).
//
// Before P8-SV the wire arm read the three group counts through ReadWireBuffer and dispatched them
// from the CPU, so a dispatch buffer a shader had written cost a whole-GPU wait per call - the
@@ -6,7 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// P8-SV: TWO glGenerateMipmap SHAPES THE MAGMA WIRE ARM GOT WRONG (docs/Disaggregated/notes/p8/SV.md).
// P8-SV: TWO glGenerateMipmap SHAPES THE MAGMA WIRE ARM GOT WRONG (docs/archive/notes/p8/SV.md).
//
// * A 3D chain through the NATIVE blit. The wire arm passed the level's depth as the blit's
// subresource layerCount on an image with one array layer (VUID-vkCmdBlitImage-srcImage-00240,
+23 -146
View File
@@ -6,23 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call
// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57
// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py).
//
// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and
// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc
// with the per-accessor table and prints the coverage summary. Rows matching neither are
// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes
// "regenerate and git diff --exit-code with 0 UNMAPPED".
//
// Three pseudo-calls stand for read points that do NOT become a forward call:
// kClientResolved - the frontend answers it itself; the server is never asked
// (section 4.4.6: "the server answers nothing the client can answer").
// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1).
// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr<MG_State...>, which
// becomes an MGPipeHandle parameter; there is no single call to name.
//
// Coverage table (ARCHITECTURE.md §3.1): which MGPipe call answers each backend read point in
// scripts/data/backend_read_inventory.md. gen_pipe.py joins the inventory's `member` column
// with MGP_COVERAGE_ACCESSOR_LIST and its `delta` column with MGP_COVERAGE_DELTA_LIST into
// generated/PipeCoverage.inc; a row matching neither is UNMAPPED, and the gate requires zero.
// Pseudo-calls: kClientResolved (the frontend answers; the server is never asked),
// kReverseChannel (an MGPipeCallbacks entry, §8.1), kStructuralHandle (a SharedPtr<MG_State...>
// parameter that becomes an MGPipeHandle).
// clang-format off
// X(Accessor, PipeCall)
@@ -34,39 +24,14 @@
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \
/* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \
/* target argument), but by supplying the target from the EMISSION SITE, which */ \
/* knows it exactly. The split, target by target, in BufferTarget's OWN */ \
/* spelling (BufferObject.h:15-33) so the list cross-checks mechanically: */ \
/* Vertex, and the per-attribute buffer of a VAO -> set_vertex_buffers */ \
/* Index -> set_index_buffer */ \
/* DrawIndirect, Parameter -> set_indirect_buffers */ \
/* Uniform, ShaderStorage, AtomicCounter, */ \
/* TransformFeedback -> set_shader_buffers */ \
/* CopyRead, CopyWrite, PixelPack, PixelUnpack, */ \
/* Texture -> still pulled: the */ \
/* transfer and pixel-store targets have no call of their own yet; */ \
/* DispatchIndirect -> still pulled, and */ \
/* it is the one a reader most plausibly assumes set_indirect_buffers */ \
/* covers: BOTH backends read it (SyncBoundBuffer at every */ \
/* glDispatchComputeIndirect) and NO call carries it - MGPIndirectBuffers */ \
/* is the DrawIndirect + Parameter pair only - so the field stays the */ \
/* fill loop's for this target; */ \
/* Query -> still pulled: no */ \
/* backend reads it at all and no call names it. */ \
/* That is 15 of 15; a target missing from this list is a target a later */ \
/* phase would retire the pull for on the strength of a split that never */ \
/* covered it. */ \
/* THE ROW STAYS ONE ROW, and that is structural rather than a shortcut: this */ \
/* list IS the MGPipeInputField enum and the PipeInputs field set, and the */ \
/* field is ONE array (m_bufferBindingSlot[kBufferTargetCount]) that a second */ \
/* row of the same name could only duplicate. So the split lives here and in */ \
/* the emitters, and the row keeps naming set_indirect_buffers for the plan's */ \
/* explicit replacement of the DrawIndirect/Parameter pair. It is deliberately */ \
/* NOT in the EMITTED list below: seven targets above are still pulled, and a */ \
/* row there says "the whole field is supplied", which for this field would be */ \
/* the same half-truth GetPixelStoreParameters is kept out for. */ \
/* Polymorphic over BufferTarget; the emitters supply the target, so the calls */ \
/* split it: Vertex (and a VAO's attribute buffers) -> set_vertex_buffers, */ \
/* Index -> set_index_buffer, DrawIndirect and Parameter -> set_indirect_buffers, */ \
/* Uniform, ShaderStorage, AtomicCounter, TransformFeedback -> set_shader_buffers. */ \
/* The other seven targets (copy, pixel, Texture, DispatchIndirect, Query) have no */ \
/* set_* carrier; on the server, verbs carry the handles they need. The field is */ \
/* one array, so it stays one row, and it has no emitted row: no call supplies all */ \
/* of it (FieldOwnership.def). */ \
X(GetBufferBindingSlot, SetIndirectBuffers) \
X(GetBufferBindingPoint, SetShaderBuffers) \
X(GetBufferBindingPointCount, SetShaderBuffers) \
@@ -157,102 +122,14 @@
X(handle-ify (wire handle), kStructuralHandle) \
X(Buffer ops delta, ResourceRespecify)
// X(Accessor, PipeCall) - the EMITTED list (P2 brief D5): which P2 call now SUPPLIES this
// PipeInputs field, so the per-verb residual fill loop no longer has to pull it out of
// GLContext. gen_pipe.py turns it into kMGPipeFieldEmittedBy[] (generated/PipeFilled.inc);
// a field with no row here keeps going through the fill loop, which is what makes the
// MOBILEGL_PIPE_PUSH bitmask a true per-subsystem A/B rather than an all-or-nothing switch.
//
// Every name must be an accessor in MGP_COVERAGE_ACCESSOR_LIST and every call must be a
// real call in PipeCalls.def; gen_pipe.py refuses anything else.
//
// The one row whose call differs from the accessor list's is GetPrimitiveRestartIndex:
// coverage maps it onto draw_vbo because that is where a backend reads it, but the VALUE
// travels in dynamic chunk D6, so set_dynamic_state is what supplies it.
//
// GetPixelStoreParameters is DELIBERATELY ABSENT, and the reason is the shape of the field
// rather than of the call. The field is PipeInputs::m_pixelStore[2] - pack AND unpack - and
// set_pixel_pack_state carries the PACK half only, deliberately and permanently (D10,
// ARCHITECTURE.md 4.6 D5: nothing on the far side of the boundary reads unpack state). A row
// here says "this field is supplied, the fill loop may skip it", which would be a half-truth:
// the moment the render-state bitmask has its bit set, the unpack half would be written by
// nothing while its poison stamp said it was published, so neither the poison nor the verify
// comparator could see it. Until the field is split, the whole of it keeps going through the
// fill loop and the pack half is simply written twice.
//
// P3a ADDS ONE ROW, GetBoundVertexArray -> BindVertexElements, and it is the vertex-input
// family's only candidate: GetBufferBindingSlot is polymorphic over a target set P3a covers
// only part of (see its comment above) and GetCurrentVertexAttribute has been here since P2.
// The row is INERT until the vertex-input subsystem is wired - MG_Impl/Pipe/PipeFill.cpp's
// kMGPipeWiredSubsystems does not carry that bit at the contract commit, because the emitters
// beside it are still stubs - which is exactly the guard that lets a row land before the call
// that carries it exists.
//
// A NOTE FOR THE COMMIT THAT WIRES IT, because it is not visible from this file: the field is
// a shared pointer to the frontend VAO, and the vertex-input calls supply the CONFIGURATION
// (the applier's MGPipeVertexElementsRecord), not the object. So the row is shape-only in the
// same sense GetCurrentVertexAttribute's is, and it stays so until the backend's twin
// resolution reads the applier's BoundVertexElements instead of the object - at which point
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
// rather than silently by this row's presence.
//
// P4a ADDS SIX ROWS, and the same note applies to every one of them: each is SHAPE-ONLY, each
// lands in PipeFill.cpp's EmittedCallSuppliesTheWholeField FALSE arm, and the decision is
// taken THERE rather than inherited from a row's presence here. The rows and their calls:
//
// GetFramebufferBindingSlot -> SetFramebufferState GetProgramForDraw -> SetDrawProgram
// GetImageTextureBinding -> SetShaderImages GetProgramForDispatch -> SetDispatchProgram
// GetTextureUnitObject -> SetSamplerViews GetMaxTouchedTextureUnit -> SetSamplerViews
//
// Five of the six are the pointer-storage case GetBoundVertexArray already documents: the
// field is a BindingSlot<FramebufferObject>, an ImageTextureBinding, a TextureUnit or a
// SharedPtr<ProgramObject> - frontend heap references - and the calls carry eight-byte
// {slot, gen} handles and resolved descriptors. The applier has no way to produce a pointer
// and P4a deliberately does not give it one; skipping the pull would leave every one of those
// mirrors null on every draw of every push build. What retires those pulls is not a better
// applier, it is the phase where the backend stops reading a frontend object at all.
//
// THE SIXTH left that arm at P5c rv: GetMaxTouchedTextureUnit is a plain Int whose problem was
// never shape, it was the CARRIER - set_sampler_views' Count is the set's window, and the set
// is suppressed on an unchanged content hash while the high-water mark still moves on a
// redundant re-bind. set_context_values (CONTRACT-P5C.md §5.3) carries the mark as a VALUE of
// its own, whole-record suppressed, so the mark rides an unsuppressed field and the row is
// RECORD_SUPPLIED outright.
//
// P5c rv ADDS EIGHT ROWS, and they are the whole of the value-class debt that was left
// (CONTRACT-P5C.md §5.3): seven residual values no set_* call carried - the two texture-unit
// counters, the 15-target touched-count array and the five transform-feedback values - plus
// GetMaxTouchedTextureUnit re-homed from SetSamplerViews. All eight name SetContextValues, one
// POD emitted at validate when any covered value moved, hash-suppressed whole. The OBJECT rows
// (GetTextureUnitObject and its four P4a siblings above) stay BARRIER_PULLED with their
// retiring phases; the three texture shutters (below) take no row here either - they carry no
// wire value at all, the accessors answer the applier's own Serial.
//
// GetTextureContextId / GetTextureBindGeneration / GetSamplingResolutionGeneration stay out
// DELIBERATELY, and the reason is ownership rather than coverage: they are frontend SHUTTERS,
// and what replaces them server-side is the applier's own Serial - a different value with a
// different owner. A row here would claim the sets supply the generations, and the fill loop
// would skip two counters no record carries.
// And GetTextureObject / GetProgramObject are STICKY (see MGP_COVERAGE_STICKY_LIST): they are
// keyed by GL name, they are object lookups rather than verb state, and a forwarded field has
// no storage for an emitted call to supply.
//
// P5e ADDS ONE ROW, GetBufferBindingPoint -> SetShaderBuffers (MG_Remote/CONTRACT-P5E.md §5.6),
// and it is SHAPE-ONLY like P3a's and P4a's six: the field's storage is four raw bases into the
// frontend's Array<Array<BindingSlotRange1D, 84>, 4>, and set_shader_buffers carries RESOLVED
// {handle, offset, size} ranges per class. The applier has no way to produce a pointer and P5e
// deliberately does not give it one, so the row lands in PipeFill.cpp's
// EmittedCallSuppliesTheWholeField FALSE arm and the decision is taken THERE. What retires the
// pull is not a better applier, it is the four Espryt consumers reading the applier's
// BoundShaderBuffers instead of the frontend's binding points - at which point the
// FieldOwnership.def row flips to RECORD_SUPPLIED.
//
// THE ROW IS INERT AT THIS COMMIT for GetBoundVertexArray's reason one phase on: the emission
// and the fill skip both test kMGPipeWiredSubsystems, and the buffer-binding family's wired
// constant is 0 until the package that gives MG_Impl/Pipe/ShaderBufferEmit.h its body sets it.
// It lands here, with its two PipeFill.cpp switch arms, because this file and that one belong
// to the contract package for the whole phase - a family package that had to add an enumerator
// AND the arm that dispatches on it would be editing the one file the merge trap lives in.
// X(Accessor, PipeCall) - the emitted list: the call that supplies each PipeInputs field, so
// the residual fill loop may skip it. gen_pipe.py turns it into kMGPipeFieldEmittedBy[]
// (generated/PipeFilled.inc) and refuses an accessor or call not listed above or in
// PipeCalls.def. A row names the call that carries the value, which may differ from the read
// point above (GetPrimitiveRestartIndex rides dynamic chunk D6). A row alone does not skip the
// fill: PipeFill.cpp's EmittedCallSuppliesTheWholeField is false where the call carries handles
// but the field holds a frontend pointer. Deliberately absent: GetPixelStoreParameters (pack
// half only), the texture shutters (answered from applier serials) and the sticky lookups.
#define MGP_COVERAGE_EMITTED_LIST(X) \
X(GetActiveTextureUnit, SetContextValues) \
X(GetBlendColor, SetDynamicState) \
+28 -253
View File
@@ -6,169 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The dirty-surface mapping (ARCHITECTURE.md 5.2 corollary 4, P2 brief D16).
//
// MGPipe replaces "the backend rediscovers what changed" with "the frontend says what
// changed", which only works if EVERY frontend mutation a backend can observe has an answer
// to "what publishes this". The failure mode is silent and one-directional: a mutation that
// forgets to publish renders stale, and no purity gate can see it.
//
// So the surface is enumerated MECHANICALLY. scripts/gen_pipe_dirty_surface.py scans
// MG_Impl/GLImpl AND MG_State/GLState for every pGLContext-> mutator call and every
// MGP_NOTE_MUTATION site, and, with --check, fails if a scanned mutator has no row here or a
// row here names a mutator the scan no longer finds. Both directions, so a deleted mutator
// cannot leave a stale row behind either.
//
// THE SECOND ROOT AND THE SECOND MECHANISM ARE P3a's, and they were not cosmetic: reading
// MG_State/GLState found BumpSamplingResolutionGeneration, a mutator with no row at all
// whose every caller lives in that directory, and reading MGP_NOTE_MUTATION brought
// NoteUnitTouched - which no `pGLContext->` pattern can see, because every texture and
// sampler bind routes through it rather than calling it through the context - inside the
// gate. A notice site carries a FIELD name, so the mutator a row names is the ENCLOSING
// function.
//
// ANSWERS. A row lists EVERY publisher that fires on EVERY path through that mutator,
// and only those; several are joined with '|'. A publisher that fires on some paths but
// not all must not appear, because a shutter built from this file would then UNDER-fire,
// and ARCHITECTURE.md 13.2 names under-firing as the dangerous direction. The one row that
// carries a bit which fires on only some paths says so in its answer - kPulledPartialShutter
// joined with that bit - because the alternative, dropping the bit, tells a reader of this
// file that a bit P2 already emits a call for has no shutter at all.
//
// "EVERY PATH" MEANS EVERY PATH THAT MUTATES. A setter that returns early because the value
// did not change publishes nothing and needs to publish nothing - there is no mutation to
// carry - so a redundant-write guard (SetColorMask's `if (changed) BumpVersions();`, the
// BitwiseEqual guards on the patch levels) does not make its publisher conditional in the
// sense this rule cares about. A publisher reached on only SOME of the paths that DO mutate
// - SetCapability's ClipDistance arms, SetStencilFunc's reference-only call - is the thing
// that must not be named.
//
// EVERY BIT ANSWER IN THIS FILE IS DERIVED AND CHECKED, in two families and one
// direction. The RenderState family (45 rows) is checked both ways against RenderState.cpp,
// below. Every other NEW_* answer is checked against the shutter Tracker.h builds for that
// bit: gen_pipe_dirty_surface.py resolves what the shutter READS and what each mutator
// transitively WRITES (through MGP_NOTE_AGGREGATE too, whose hop it reads out of
// MGPipeNoteAggregate's own switch, and through the function-like macros of MG_State, which
// it EXPANDS - sixteen of RenderState.cpp's writes exist only after the preprocessor has
// pasted them together) to the SAME two-level token, MEM:<member> plus FIELD:<member>.<leaf>,
// and fails a row that names a bit whose shutter its mutator moves on no path at all. That
// half is one-directional on purpose - "it does write something the shutter reads" cannot
// prove it does so on EVERY path - so it catches under-firing and not over-claiming.
//
// AN ABSENCE CLAIM IS ONLY WORTH THE READING BEHIND IT, and this gate learned that twice:
// its write analysis first recorded a write through a member's field as the field alone
// and never the member, so --check printed, as a fact about RenderState.cpp, that
// SetPixelStoreParam "writes nothing NEW_PIXEL_PACK's shutter reads" about a setter whose
// whole body is sixteen writes to exactly that member; then, once it read the member, it
// still could not see a write through a REFERENCE (SetBlendEquation's `for (auto&
// blendState : m_parameters.BlendStates)`) and said the same false thing about seven more
// setters - while its reader side resolved `render.PatchVertices` to the WHOLE of
// m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and
// a row saying glClearColor publishes the patch state was green. So now: a reference or
// pointer bound to a member-rooted lvalue is followed, and its writes are credited to the
// member and the field it was bound to; a write whose root the analysis cannot place (a
// reference parameter, a call result, a member without the m_ prefix, a token it could not
// expand) TAINTS the function, and every answer that depends on a tainted function is
// UNDECIDED - printed with its reason, never a verdict; a writer supports a bit only when
// the two sides share a member AND, both resolved to fields, their field sets intersect (a
// whole-member write or read is every field); a member in common with no field information
// on one side is COARSE, reported and never counted. --check counts only the supported
// answers as derived, prints the COARSE and UNDECIDED tallies, and FAILS on an UNDECIDED
// row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST at the bottom of this file marks it - a
// mark that outlives its reason is a red gate too. What it still cannot claim: a shutter
// member written outside MG_State/GLState + MG_Impl/Pipe is undecided in the absence
// direction, a call is resolved by NAME to every body of that name, and a FIELD token is
// not scoped to a type - all three only widen what a mutator is credited with, and the
// second is also how a taint spreads.
//
// The prose answers (kImmediate, kExplicitDestroy, kUnpublishedDestroy, kNoBackendRead,
// kPulledEveryVerb, kPulledPartialShutter, kReverseChannel) are statements no derivation
// checks - except the bits a kPulledPartialShutter row names, which are checked like any
// other bit answer. --check prints how many rows carry a prose answer, so "all mapped" can
// never be read as "all verified".
//
// For the RenderState family that answer is not a matter of taste and it is CHECKED
// rather than asserted: scripts/gen_pipe_dirty_surface.py reads RenderState.cpp and
// derives, per setter, which of NEW_RENDER_STATE / NEW_PIPELINE_STATE moves on every
// path - BumpVersions() moves both, a bare ++m_version moves only NEW_RENDER_STATE, and
// a setter with both kinds of path therefore always-fires only NEW_RENDER_STATE - and
// --check fails when a row disagrees, in either direction. That check exists because
// this file got exactly two rows wrong: SetCapability, whose ClipDistance0..7 arms move
// only m_version, and SetStencilFunc, whose pipeline bump is conditional on Func moving.
// Both named NEW_PIPELINE_STATE, which does not fire for glEnable(GL_CLIP_DISTANCE0) or
// for a reference-only glStencilFunc.
//
// NEW_* a MGPipeDirty bit (MG_Impl/Pipe/Tracker.h). The tracker's shutter for
// that bit moves when this mutator runs, so the next verb publishes it.
// kImmediate the mutating function also reaches the backend in the same body, so the
// mutation is published inline and needs no shutter at all.
// kReverseChannel not state: a write INTO the frontend from the backend's side.
// kNoBackendRead no backend read point observes this state at all.
// kExplicitDestroy published by the delete_* / resource_destroy call the Track H slice
// emits when the object's last reference drops - an object's DEATH,
// which no generation shutters because there is no longer an object
// to carry one. Only for a kind that HAS an identity on the wire to
// destroy: the resources and CSOs of PipeCalls.def, which is what P2
// brief D13 scopes Espryt 0b's explicit destroy to.
// kUnpublishedDestroy
// the same event for a kind NOTHING publishes: a program, a program
// pipeline and a shader have no per-object handle on the wire at all
// in P2 - resource_destroy and the delete_* family name resources and
// CSOs - so their DirectGLES twins are still reclaimed by the
// backend's own registry teardown and no frontend call says they
// died. Recorded as a hole rather than dressed up as a mechanism that
// exists; naming kExplicitDestroy here would be the same defect the
// RenderState derivation above exists to stop, one class down in
// stakes. (D13's prose says 'six kinds' while the Core.cpp ranges it
// cites also cover MarkProgram/MarkShaderForDeletion; the tree
// decides, and the tree has no wire object for those three.)
// kPulledEveryVerb no shutter exists at all - no MGPipeDirty bit moves on any path through
// this mutator - and none is needed yet: the PipeInputs field it writes is
// in its verb class's may-read mask, so the residual fill copies it at
// EVERY verb of that class. A shutter here is a P3/P4 optimisation, not a
// correctness gap.
// kPulledPartialShutter
// the same pull, but a bit DOES move - on some of the paths that mutate,
// not all of them - so this row must never be read as "no shutter exists".
// The bits that move are named after the '|', which is the one place this
// file joins a prose answer with a bit, and the reason is exactly that a
// P3a shutter builder has to be able to tell "no bit covers this" from "a
// bit covers half of it". The named bits are checked the same way every
// other bit answer is - a dead one is a red gate - but they are NOT a
// licence to narrow: what holds on every mutating path is the pull.
// Which rows need this answer is a human judgement and stays one: the
// derivation's "it does move that shutter" direction over-approximates
// (a call name resolves to every body of that name, a write inside an
// `if` counts), so it can refute a named bit but cannot find the rows
// that should have named one.
//
// KNOWN BLIND SPOTS OF THE SCANNER, recorded here rather than left implicit
// (gen_pipe_dirty_surface.py's own notes plus its scan roots):
// 1. it matches braced function bodies textually, so a mutator inside a LAMBDA is
// attributed to the enclosing function;
// 2. a mutation published through a HELPER the entry point calls reads as deferred here.
// 3. CLOSED AT P3a. The scan root was MG_Impl/GLImpl only, so the four MGP_NOTE_MUTATION
// sites in MG_State/GLState/TextureState/TextureState.h were outside it entirely. The
// root is now both directories and the notice is a recognised publish mechanism; what
// that found is the two rows marked "P3a" below.
// The gate is therefore a COMPLETENESS gate over what the scanner does see. The semantic
// proof stays the MOBILEGL_PIPE_VERIFY lane, which is blind to none of them.
//
// THE MUTATOR PREFIX SET WIDENS AT P4a, and what it does NOT gain is the more interesting
// half. `pGLContext->` + Add|Set|Mark|Bump|Allocate|Truncate|Record|Notify|Begin|End could
// not see `UseProgram`, `BindVertexArray`, `BindProgramPipelineObject` or
// `BindTransformFeedbackObject` - four mutators that each move a field P3a or P4a pushes -
// because none of them starts with one of those words. `Use` and `Bind` are added, and the
// complete set the widening surfaces was enumerated by grep at the phase's base ref so it
// cannot surprise anybody: exactly those four names, on seven call sites.
//
// `Create*` and `Pop*` are DELIBERATELY NOT ADDED. They create or destroy objects rather than
// move a pushed field, and each object class's creation and destruction is already answered
// twice over - by its own Mark*ForDeletion row below and by the constructor-time
// resource_create - so adding them would produce rows that restate an answer this file already
// gives, and every one of them would have to be maintained against a mechanism that is not
// theirs. A gate whose rows do not each carry their own question is a gate nobody reads.
//
// Dirty-surface map: what publishes each frontend mutation a backend can observe, checked by
// scripts/gen_pipe_dirty_surface.py --check (ARCHITECTURE.md §5.2.1). An answer joins with '|'
// the publishers that fire on every path that mutates. NEW_*: a Tracker.h bit the next verb
// publishes. kImmediate: the same body reaches the backend. kReverseChannel: a backend write into
// the frontend. kNoBackendRead: no backend reads it. kExplicitDestroy / kUnpublishedDestroy: a
// death with / without a wire handle. kPulledEveryVerb: no bit; the residual fill copies the field
// at every verb. kPulledPartialShutter|NEW_*: the same pull; the bits move on some paths only.
// clang-format off
// X(Mutator, Answer)
@@ -300,22 +144,13 @@
/* the paths that mutate and not all, which is exactly what this answer is for; what */ \
/* holds on every mutating path is still the pull through GetBufferBindingPoint. */ \
X(SetNamedTransformFeedbackBinding, kPulledPartialShutter|NEW_SO_TARGETS) \
/* P5e (sb), THE ONE NAME THE `Touch` PREFIX SURFACES, on two call sites - GL_Buffer.cpp's*/ \
/* BindBufferBase_State and BindBufferRange_State. It moves the per-target high-water */ \
/* mark that bounds every indexed-binding-point walk there is: the backend's, the */ \
/* client's GPU-write sweep's, and set_shader_buffers' emitted window. */ \
/* */ \
/* kPulledEveryVerb, in GetMaxTouchedTextureUnit's exact words one family over: the */ \
/* field it writes (GetTouchedBufferBindingPointCount) has no shutter of its own and */ \
/* needs none - it is in kDraw's, kDispatch's and kXfbSpan's may-read masks, so the */ \
/* residual fill copies it at every verb of those classes, and since P5c rv */ \
/* set_context_values carries it as a value whose gate is the whole-record hash. */ \
/* */ \
/* IT NAMES NO BIT, AND THAT IS NOT THE SAME AS THE MUTATION GOING UNPUBLISHED. Both */ \
/* call sites bump the bind-point generation on the line below this call, so bits 15/16/ */ \
/* 17 fire for the bind - but they fire for the BINDING, through a different member, and */ \
/* a row here that named them would claim this mutator moves a shutter it does not touch */ \
/* (the derivation refutes exactly that, which is how the claim was tested). */ \
/* Called by BindBufferBase_State / BindBufferRange_State (GL_Buffer.cpp), this mutator */ \
/* moves the per-target high-water mark that bounds every indexed binding-point walk. */ \
/* No shutter reads it: GetTouchedBufferBindingPointCount is in the kDraw, kDispatch */ \
/* and kXfbSpan may-read masks, and set_context_values carries it. Both call sites bump */ \
/* the target's bind-point generation, which fires that target's NEW_CONST_BUFFERS, */ \
/* NEW_SHADER_BUFFERS or NEW_SO_TARGETS through a different member, so this row must */ \
/* not name them. */ \
X(TouchBufferBindingPoint, kPulledEveryVerb) \
/* ---- P4a, THE FOUR THE WIDENED PREFIX SET SURFACES. Every one of them moves a field */ \
/* P3a or P4a pushes and none of them was visible to the scan before, because none */ \
@@ -350,38 +185,13 @@
/* the kDraw and kXfbSpan may-read masks, so the residual fill copies them at every */ \
/* verb of those classes. Narrowing it is P4b's, with set_stream_output_targets. */ \
X(BindTransformFeedbackObject, kPulledEveryVerb) \
/* ---- an object's death: no generation, because there is no longer an object */ \
/* to carry one. Espryt 0b's delete_* / resource_destroy publishes the kinds */ \
/* that have a handle on the wire; programs, program pipelines and shaders have */ \
/* none in P2, so nothing publishes theirs - kUnpublishedDestroy, a known hole. */ \
/* TWO OF THESE ROWS STOPPED BEING ASPIRATIONAL AT P3a, and the call that makes */ \
/* each true is named rather than implied: */ \
/* MarkBufferObjectForDeletion -> resource_destroy (PipeCalls.def), emitted */ \
/* from ~BufferObject the moment the last reference drops - which is the */ \
/* glDelete* that only marks the name, followed by whatever unbind actually */ \
/* releases it - and followed IN THAT ORDER by the client freeing the slot. */ \
/* MarkVertexArrayForDeletion -> delete_vertex_elements, published through the */ \
/* death notice ~VertexArrayObject already raises for the VertexElementsCso */ \
/* kind; the CSO handle is minted per frontend VAO off its lifetime id. */ \
/* THE PUBLISHER IS THE BACKEND'S (Managers.cpp's OnFrontendStateObject- */ \
/* Destroyed consumer, package espryt), not the client's: the client mints */ \
/* the CSO handle and emits create/bind, and the free rides with that */ \
/* consumer. Until it lands the row states the design, not the tree. */ \
/* P4a CLOSES ONE OF THE THREE HOLES ABOVE AND STATES WHY THE OTHER TWO ARE NOT HOLES. */ \
/* MarkProgramForDeletion -> kExplicitDestroy. delete_shader_state exists now and */ \
/* ~ProgramObject emits it through the client-side death helper, in the fixed */ \
/* order: the wire delete first, the backend notice second, the slot free last. A */ \
/* program pipeline COMPOSITE takes the same call on the same helper - the server */ \
/* never learns it is a composite. */ \
/* MarkProgramPipelineForDeletion stays kUnpublishedDestroy, and it is NOT waiting */ \
/* for a later phase: a ProgramPipelineObject has no lifetime id and no wire object */ \
/* at all (its only identity is m_everBound). It never gets a handle, so there is */ \
/* nothing for a delete to name. What its cache's eviction DOES publish is the */ \
/* composite's delete_shader_state, which is the row above. */ \
/* MarkShaderForDeletion stays kUnpublishedDestroy for the same kind of reason: a */ \
/* ShaderObject has no lifetime id and never crosses the boundary - the payload is */ \
/* per-stage SPIR-V plus the reflection archive, not source, and glslang lives */ \
/* entirely on the client. */ \
/* ---- an object's death: no generation, since no object is left to carry one. */ \
/* kExplicitDestroy: the destructor (last reference, not glDelete*) runs the kind's */ \
/* MGPipeEmit*DestroyAndFree (PipeMutation.h): the wire delete if the kind has one */ \
/* (framebuffers do not), then the backend notice, then the slot free. Buffers send */ \
/* resource_destroy, VAOs delete_vertex_elements, programs and pipeline composites */ \
/* delete_shader_state. kUnpublishedDestroy: program pipelines and shaders have no */ \
/* lifetime id and never cross the boundary, so no record names their death. */ \
X(MarkBufferObjectForDeletion, kExplicitDestroy) \
X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
X(MarkProgramForDeletion, kExplicitDestroy) \
@@ -408,48 +218,13 @@
X(AddTransformFeedbackPausedPrimitives, kPulledEveryVerb) \
X(AddTransformFeedbackPrimitives, kPulledEveryVerb)
// X(Mutator, Bit) - the (row, bit) pairs above whose derivation is KNOWN to come out
// UNDECIDED, each with the reason --check prints for it. Every bit answer NOT listed here
// is marked derived: --check fails when the derivation cannot decide it, and fails again
// when a mark here names a pair the derivation now decides, so this list can neither hide a
// row nor outlive its reason.
//
// IT WAS EMPTY UNTIL P4a, and it stops being empty for a reason that is a property of the
// SCANNER rather than of the rows. Every entry below is a bit answer that is plainly true -
// glUseProgram is what moves the object bits 6, 12 and 14's shutters read through (the
// program in use; bits 12 and 14 since the fable seam round, F-1 / F-2), and glBindVertexArray
// is what moves the object bit 5's shutter reads through - and the write analysis cannot say
// so, because each of the two mutators reaches, BY NAME, a body that writes a member with no
// m_ prefix:
//
// UseProgram -> DestroyProgramSlot() writes `attachedShaders`
// BindVertexArray -> a call spelled `Bind(` resolves to every body of that name, one of
// which (ImageTextureBinding::Bind) writes `Access`
//
// A call resolved by name to every body of that name is one of the three over-approximations
// this analysis documents about itself, and an unplaceable write TAINTS the body it is in -
// which is the right default, because "it does not write anything the shutter reads" must
// never be claimed about code the script could not read. Widening the taint rule to ignore
// non-m_ writes would weaken the one mechanism that catches a genuine under-fire, so the rows
// are MARKED, with the tool's own reason, rather than the tool being made more permissive.
// Control 21 is what proves every marked row still needs its mark (it reads this list, so a
// row that gains a bit here is counted rather than assumed), and control 18 is what fails the
// moment any of them becomes decidable and the mark outlives its reason.
//
// The ten mutators that reach a tainted body (--check prints the count) all carry a prose
// answer, which no derivation checks; these two mutators are the first that carry a bit answer.
//
// P5e (sb) ADDS A THIRD, for the same property of the scanner and a third unplaceable write:
//
// SetNamedTransformFeedbackBinding
// -> MGPipeSlots().Acquire() calls GetLifetimeId() through its reference
// parameter `buffer`, which the analysis cannot root in a member
//
// The claim it marks is plain: the bound arm of this function bumps the transform-feedback
// target's bind-point generation (BufferState::NoteBindPointChanged), which is exactly what
// bit 17's shutter reads since P5e. The mark is on the BIT the kPulledPartialShutter row
// names, not on the pull, so what this list suspends is the narrowing claim and never the
// answer that holds on every path.
// X(Mutator, Bit): bit answers above that gen_pipe_dirty_surface.py cannot decide. --check
// fails on an undecided pair missing here and on a listed pair it can now decide. Each answer
// is true, but its mutator reaches by name a body with a write the analysis cannot root in a
// member: UseProgram via DestroyProgramSlot() (`attachedShaders`), BindVertexArray via
// ImageTextureBinding::Bind (`Access`), SetNamedTransformFeedbackBinding via
// MGPipeSlots().Acquire() (reference parameter `buffer`). Marking keeps the taint rule strict;
// for SetNamedTransformFeedbackBinding only the bit after kPulledPartialShutter is suspended.
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X) \
X(UseProgram, NEW_SHADER) \
X(UseProgram, NEW_SAMPLER_VIEWS) \
+6 -60
View File
@@ -6,66 +6,12 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P6 `dl` (CONTRACT-P6 5.2, D2 ruling). THE INTERNAL FATAL VOCABULARY, and its projection onto
// the SEVEN-value wire FatalCode.
//
// a6 censused 30 distinct family words against a 7-value wire enum, so Session::Fail(FatalCode,
// detail) cannot carry the vocabulary in its first argument - the word would collapse to one of
// seven and the log would say "ProtocolCorruption" where the code said "UnmigratedVerb". So the
// family is an INTERNAL enum, generated from this file, and the wire keeps FatalCode. Each row
// carries the coarse code the PEER is told when this family kills the session; the rich word
// stays in the local log and in the SessionFault frame's `family` string.
//
// THE PROJECTION IS A TOTAL TABLE, NOT A SWITCH WITH A DEFAULT. This build has no -Werror
// anywhere (measured: CMakeLists, every *.cmake, both workflows), so a gate premised on -Wswitch
// catching a missing arm is fiction. A row here IS the arm; a family with no row does not
// compile a call that names it.
//
// WHY EACH CODE. The wire FatalCode answers one question for the peer: what KIND of end was
// this. The buckets:
// ProtocolCorruption - the peer sent, or the session reached, something that cannot be
// honoured correctly: a malformed record, an unimplemented verb/surface,
// an invariant the roles must hold, a reply that does not fit its
// contract. The peer's takeaway is "we disagree about the protocol".
// RingOverrun - a shared ring or event channel overran. A transport-capacity fact.
// SegmentMismatch - a shared SEGMENT's geometry could not hold what a role needed to put
// in it. Distinct from a ring overrun: the segment is the wrong SIZE,
// not merely full this instant.
// AbiMismatch - the two builds do not agree on layout or protocol version.
// ServerCrashed - a liveness end the WAITING side attributes to the other: a drain that
// never completed, an apply thread that is gone or never started, a native
// resource the server had to have and could not obtain. The device-lost
// latch is armed from the doorbell, never from these, but the peer that
// was waiting is told the server is gone.
// DeviceLost - reserved: set by the latch (5.3), never raised as a Fatal family, so no
// row projects onto it.
//
// P7 WAVE 0 ADDED ELEVEN ROWS, for the eleven words the widened census gate found in the server
// image outside MG_Remote/ (scripts/ci/fatal_census.py rule 3). Ten are ProtocolCorruption for
// the reason that bucket already states - a record, a mask or a reply that cannot be honoured
// correctly. The eleventh, ResourceUnavailable (Managers.cpp's three image-promotion sites and
// VkBufferManager.cpp's eight), is ServerCrashed rather than SegmentMismatch: SegmentMismatch
// means a SHARED SEGMENT was the wrong size, and telling a peer that when a VkBuffer allocation
// or a native readback failed would be a lie about whose geometry was wrong. The honest thing to
// tell a waiting peer is the one ServerCrashed already says. The wire enum has no "the server
// could not obtain a resource" value and adding one would move `wireFingerprint`, which is a
// protocol change no wave-0 instrument is entitled to make (recorded for the integrator).
//
// P12 (on-screen server window) ADDED ONE ROW, ServerWindowLost: the display server's window was
// destroyed (surfaceDestroyed) under a live on-screen session. The session latches by name after
// its surface is released, and the WAITING client is told what ServerCrashed already says - the
// server-side resource its session had to have is gone - so it reads a clean device loss. Not
// DeviceLost: that code is the latch's own, never a family's projection (see its bucket above).
//
// BackendDeviceLost: the GPU device (Magma's VkDevice, Espryt's EGL context) a session renders
// with was lost to a GPU fault or hang. It is only ever LATCHED (MGPipeSessionLatch), so the one
// session ends and the display server's other sessions keep running; the waiting client is told
// what ServerCrashed says and reads a lost context. Not DeviceLost, for ServerWindowLost's reason.
//
// Two words were NOT given rows, they were collapsed instead: `InitialBytesNotCarried`
// (MG_Impl/Pipe/PipeFill.cpp) said the same thing as `UncarriedInitialBytes` (Client/
// WireTables.cpp) about the same record - a respecify that crossed with no initial bytes - and
// two words for one family is exactly the drift a6 censused. PipeFill now uses the older word.
// The internal fatal families and the coarse wire FatalCode each one reports to the peer. The
// family word stays in the local log and in the SessionFault frame's `family` string. The table
// is total: a family without a row does not compile (the build has no -Werror, so -Wswitch
// cannot catch a missing arm). What each code means and how to choose one: ARCHITECTURE.md
// §15.3.1. DeviceLost is set only by the latch, so no row projects onto it. Reuse an existing
// family when it already describes the failure.
//
// clang-format off
+24 -77
View File
@@ -6,21 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5f exit classification. This table describes SERVER reads, not monolith storage.
// RECORD_SUPPLIED is still derived from Coverage.def minus the whole-field refusal set.
// A handle record does not supply a legacy pointer-valued getter: those getters are FATAL,
// while their migrated consumers read the server record directly. Never relabel a pointer
// RECORD_SUPPLIED merely to make the debt count zero.
//
// APPLIER_DERIVED accessors answer server state. FATAL accessors must abort on any server
// read, including barriered/escalated records and non-strict/shared-block configurations.
// BARRIER_PULLED remains a schema class so generator controls can test debt admission,
// but no production row may use it after P5f. Monolith getter/fill behavior is unchanged.
//
// EmittedCallSuppliesTheWholeField retains its eight refusals: seven legacy pointer
// representations plus the two-sided pixel-store field. Deleting those refusals would
// falsely derive RECORD_SUPPLIED for the retired getters; server consumers bypass them.
//
// Server-read class of each PipeInputs field and sticky forward; the generator requires one.
// RECORD_SUPPLIED not listed: Coverage.def's emitted fields minus the refusals in PipeFill.cpp's
// EmittedCallSuppliesTheWholeField (legacy pointer getters, two-sided pixel
// store). Never relabel a pointer getter to zero the debt.
// APPLIER_DERIVED answered from server state.
// FATAL every server read aborts, whatever the record or configuration.
// BARRIER_PULLED schema only, for the generator's controls; no production row uses it.
// clang-format off
// X(Field, Class, RetiringPhase, Why)
@@ -58,69 +50,24 @@
X(InvalidateCompileEnv, FATAL, "-", "transport caps republication replaces direct frontend invalidation") \
X(RecordError, FATAL, "-", "transport OnGlError posts an event before the legacy frontend-error-queue arm")
// X(Field, Arg0, Class, Why) - ARGUMENT-KEYED EXCEPTIONS.
//
// A row here narrows ONE argument value of ONE field to a different class. The field keeps
// its single row above; this is the same shape Coverage.def:62-69 already rules for
// GetBufferBindingSlot - "THE ROW STAYS ONE ROW, and that is structural rather than a
// shortcut: this list IS the MGPipeInputField enum and the PipeInputs field set, and the
// field is ONE array that a second row of the same name could only duplicate".
//
// GetPixelStoreParameters is m_pixelStore[2] indexed by its own `isUnpack` argument, exactly
// as GetBufferBindingSlot is m_bufferBindingSlot[15] indexed by its BufferTarget. Only [0]
// (pack) has a carrier, and the applier writes it.
//
// AND THE UNPACK HALF HAS NO BACKEND READER AT ALL. Every gPipeInputs->GetPixelStoreParameters
// call site in the tree passes `false`: DirectGLES.cpp's PackStateFromContext, StoreReadbackRowsToClient
// and ReadPixels, Utils.cpp's StoreClientRows, VulkanRenderer::ReadDepthStencilImageToClient() and
// RecordVerbSink::ReadBoundFramebufferTight() - six, not the five the scout named, and the
// scout did not open them. PipeFill.cpp's own EmitPixelPackState says the same thing from
// the other side: "PACK only, deliberately: nothing on the far side of the boundary reads
// unpack state". So the honest class for the unpack half is FATAL, not BARRIER_PULLED: a
// future backend read of it would otherwise be served a stale struct in silence, and this
// way it is a named abort on the first read.
// X(Field, Arg0, Class, Why) - argument-keyed exceptions. A row narrows one argument value of
// one field to another class; the field keeps its single row above, because it is one array
// (as GetBufferBindingSlot is in Coverage.def).
// GetPixelStoreParameters(isUnpack): only the pack half has a carrier, written by the applier,
// and every backend call site passes false. The unpack half is FATAL so a future backend read
// aborts by name instead of reading a stale struct.
#define MGP_FIELD_OWNERSHIP_ARG_LIST(X) \
X(GetPixelStoreParameters, 1, FATAL, \
"the unpack half has no carrier AND no backend reader; all six gPipeInputs sites pass false")
// X(WireOp, Verb) - WHERE THE SERVER STAMPS.
//
// The stamp rule needs one thing the wire does not carry: which MGPipeVerb a record belongs
// to. The two name spaces are not the same and do not line up by name - the DRAW verb is
// `DrawArrays` and its record is `draw_vbo`, the BLIT verb is `BlitFramebuffer` and its record
// is `blit` - so the map is written here and checked against both sources (PipeCalls.def for
// the op, FillPoints.def for the verb) rather than believed.
//
// EXACTLY THE OPS THAT ARE VERB BOUNDARIES. CONTRACT-P5.md section 7 puts five slots in class
// B (emitted in P5): Clear, DrawArrays, ReadPixels, BlitFramebuffer and Present. FOUR OF THEM
// ARE HERE AND PRESENT IS NOT, and that is a ruling rather than an omission: FillPoints.def:21
// says in so many words that "Present and SetSwapInterval go through BackendObject virtuals
// and read no frontend state, so they are not verbs here". There is no MGPipeVerb::Present to
// stamp for, MGPipeValidateForVerb is never called for it, and stamping at Present would
// retire the previous verb's answers with nothing to put in their place.
//
// An op that is not in this list is NOT a verb boundary and the applier must not stamp on it:
// a set_dynamic_state between two draws is part of the draw's verb, not a new one.
//
// THE GENERATOR REFUSES AN OMISSION, NOT ONLY A TYPO, and that is the half the first version of
// this file did not have. Every call that is VERB-SHAPED must have a row here or an exemption
// row below with a reason. "Verb-shaped" is the union of two mechanical tests, both derived:
// (a) PipeCalls.def gives the call kind kCtxVerb - the catalogue's own word for it;
// (b) the call's name is also an MGPipeVerb name in FillPoints.def.
// Eighteen rows and three exemptions cover all twenty-one (P5: twelve and three; P5b appended
// the five kCtxVerb rows 72..76 and settled ResourceCopyRegion). Only four of the rows could
// arrive in P5 (CONTRACT §7 class B minus Present); the rest are Fatal{UnmigratedVerb} on the
// client until a P5b migration package flips them and are mapped ANYWAY, because the failure
// mode of an absent row is silent: the applier would run the record under the PREVIOUS verb's
// serial, mask and name, and a field inside that mask would read FRESH while holding the
// previous verb's value.
//
// NOT MECHANICALLY DETECTABLE, and so not claimed: a call that is a verb boundary, is not
// kCtxVerb, and whose name differs from its verb's. `ResourceCopyRegion` WAS the one in the
// tree (its verbs could have been CopyImageSubData / CopyTexSubImage2D / CopyTexImage2D). P5b
// decided: resource_copy_region is glCopyImageSubData only, and the two framebuffer-sourced
// copies have their own row (copy_framebuffer_to_texture), so the choice is a row here rather
// than a rule in a reader's head.
// X(WireOp, Verb) - where the server stamps a verb boundary (ARCHITECTURE.md §9.2.1). Op and
// verb names differ (draw_vbo stamps DrawArrays, blit BlitFramebuffer), so the map is checked
// against PipeCalls.def and FillPoints.def. An unlisted op is not a boundary: a
// set_dynamic_state between two draws belongs to the draw's verb. The generator requires every
// verb-shaped call (kind kCtxVerb in PipeCalls.def, or named like an MGPipeVerb in
// FillPoints.def) to have a row here or in the exemption list, since a missing row would apply
// the record under the previous verb's stamp. A boundary that is neither goes undetected and
// needs a hand-written row.
#define MGP_VERB_OP_LIST(X) \
/* CONTRACT §7 class B - the four that can actually arrive in P5. */ \
X(Clear, Clear) \
@@ -138,7 +85,7 @@
X(ResumeStreamOutput, ResumeTransformFeedback) \
X(GenerateMipmap, GenerateMipmap) \
X(GetTextureImage, GetTextureImage) \
/* P5b (MG_Remote/CONTRACT-P5B.md). The ONE renamed, non-kCtxVerb boundary the file above */ \
/* P5b (docs/archive/contracts/CONTRACT-P5B.md). The ONE renamed, non-kCtxVerb boundary the file above */ \
/* said "the phase that emits it adds its row and decides which verb it is": P5b rules */ \
/* resource_copy_region = glCopyImageSubData ONLY; the two framebuffer-sourced copies */ \
/* (CopyTexImage2D / CopyTexSubImage2D) are copy_framebuffer_to_texture below, stamped as */ \
@@ -175,12 +122,12 @@
// reviewer's head and so that the generator's completeness check has something to accept.
#define MGP_VERB_OP_EXEMPT_LIST(X) \
X(Present, \
"FillPoints.def:21 - Present and SetSwapInterval go through BackendObject virtuals and " \
"Present and SetSwapInterval go through BackendObject virtuals and " \
"read no frontend state, so they are not verbs here. There is no MGPipeVerb::Present, " \
"MGPipeValidateForVerb is never called for it, and stamping would retire the previous " \
"verb's answers with nothing to put in their place. It is class B all the same.") \
X(SetSwapInterval, \
"the same sentence of FillPoints.def:21, and it is class C besides") \
"not a verb for the same reason as Present, and it is class C besides") \
X(Flush, \
"not a verb at all: the verb census (BRIEF §12 C-3) found Flush is not a GLFunctionsTable " \
"slot and glFlush/glFinish are empty function bodies (Definitions.cpp:111-112), so there " \
+6 -65
View File
@@ -6,72 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the
// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into
// generated/PipeFillPoints.inc:
//
// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class
// MGP_FILL_CLASS_LIST the verb classes
// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read
//
// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable
// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and
// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum
// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every
// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go
// through BackendObject virtuals and read no frontend state, so they are not verbs here.
//
// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every
// class and are not listed. The verify lane is the oracle for this table: a
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
// row, never by marking the field sticky.
//
// ---------------------------------------------------------------------------------------
// THE VERDICT ON THE EIGHT STATICALLY OVER-APPROXIMATED ROWS (P2 brief C.1, MEASUREMENTS.md
// section 4). Every one of them is KEPT, and the reason is the same in all three groups: the
// row is not a guess, it names a concrete backend path, and the only evidence that could
// retire it is DYNAMIC - a corpus that never reaches the path proves nothing, because a row
// removed on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build.
//
// kReadback + IsTransformFeedbackActive / IsTransformFeedbackPaused
// KEPT. The depth/stencil read emulation draws (ScopedEmulationDrawState, DirectGLES.cpp)
// and pauses an active capture around its own draw, so a glReadPixels of a depth or
// stencil attachment reads the transform-feedback state exactly as a draw does. Reached
// only when the emulation is armed, which is a driver-shaped decision, so no desktop
// corpus can decide it.
//
// kTextureOp + IsCapabilityEnabled, kDispatch + IsCapabilityEnabled
// KEPT. Magma's GenerateMipmap materialises a texture's queued clear before it blits and
// PrepareStorageImageTextures does the same for every storage image a dispatch writes;
// both go through VkClearManager::PreCompensateSrgbClearColor, which reads
// GL_FRAMEBUFFER_SRGB. The P2 contract gave that capability real storage for the first
// time, so this row went from reading a compile-time constant to reading real state -
// which is the opposite of a row that could be dropped.
//
// kBlitOrCopy / kTextureOp + the shader blit's viewport and vertex/buffer bindings
// (GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode, GetBufferBindingPoint)
// KEPT. TryBlitToDefaultFramebufferWithShader is a real draw of a backend-owned helper
// program: ApplyGLViewportState -> ComputeGLViewport reads viewport 0 and its depth range,
// GetOrCreateBlitPipeline -> SelectProvokingVertexMode reads the provoking vertex, and
// BindProgramUniformBuffers' block resolvers read the frontend binding points. It is taken
// when a blit's destination is the default framebuffer and the driver cannot do it
// natively - again a driver-shaped decision.
//
// What WOULD retire a row: the poison build already answers "was this field read at this
// verb" exactly (MOBILEGL_PIPE_POISON_OMIT withholds one field's stamp for one verb and a
// read of it aborts naming the pair). Turning that into a retirement gate means running the
// omission across the full CTS caselist on both devices, not the desktop corpus.
//
// P3a STATUS: still not done, and deliberately not done here. P3a is one of the five
// architecture boundaries that owe a full gl44to46 caselist run on both devices, so the
// omission sweep rides that run rather than duplicating it - the verdict lands with the
// caselist result at the phase exit, off the critical path, and every row above stays in
// place until it does. A row retired on desktop evidence would be retired on evidence that
// cannot support it, which is the reason this item exists rather than a scheduling excuse.
// ---------------------------------------------------------------------------------------
//
// Per-verb fill points of PipeInputs (ARCHITECTURE.md §9.2), read by scripts/gen_pipe.py.
// MGP_FILL_VERB_LIST: every GLFunctionsTable function pointer in declaration order, with its
// class (gen_pipe.py refuses any other set). MGP_FILL_FIELD_LIST: the fields a class may read;
// sticky fields (MGP_COVERAGE_STICKY_LIST) are implicit. Fix Fatal{UnmigratedPipeInput} by
// adding a row, never by making it sticky. A row is removed only on a MOBILEGL_PIPE_POISON_OMIT
// run of the full CTS caselist on both devices; a corpus that never reaches a path proves nothing.
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
//
// clang-format off
// X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h),
+11 -46
View File
@@ -54,22 +54,13 @@ namespace MobileGL::MG_Pipe {
kOptional = 1u << 5,
};
// P5e (MG_Remote/CONTRACT-P5E.md §2.2). THE FIFTH COLUMN OF PipeCalls.def: what the client
// does after publishing a record of this call, on a server that publishes
// kCapRunAheadApply. Unscoped for MGPipeCallClass's reason - PipeCalls.def spells these as
// bare tokens so the same file can be read by the C++ preprocessor and by
// scripts/gen_pipe.py, which generates MGPipeWaitClassFor(op) from it.
//
// IT IS A VALUE, NOT AN OPCODE AND NOT A FLAG. The wire carries the opcode and both roles
// recover the class from it, exactly as they recover the flags - which is the whole reason
// MGPipeBarriered(op, payload, applierState) can be one function computed identically by
// the emit table and by the sink instead of two copies that agree until they do not.
//
// kWaitNone is 0 so that an uninitialised read is the SAFE direction in exactly one sense
// and the dangerous one in the other, which is why nothing reads this table without an
// opcode that came out of the catalogue: MGPipeWaitClassFor answers kWaitClassCount for an
// opcode it does not know, and a caller holding bytes off a stream must reach its own
// Fatal{ProtocolCorruption} rather than a wait decision.
// The fifth column of PipeCalls.def: what the client does after publishing a record of this
// call to a server that publishes kCapRunAheadApply (ARCHITECTURE.md §11.6). Unscoped so the
// .def can use bare tokens for both the preprocessor and scripts/gen_pipe.py, which generates
// MGPipeWaitClassFor(op). Both roles recover the class from the opcode, so
// MGPipeBarriered(op, payload, applierState) is one function for the emit table and the sink.
// MGPipeWaitClassFor answers kWaitClassCount for an unknown opcode; a caller holding stream
// bytes must raise its own Fatal{ProtocolCorruption} rather than make a wait decision.
enum MGPipeWaitClass : Uint8 {
kWaitNone = 0,
kWaitApplied,
@@ -107,35 +98,9 @@ namespace MobileGL::MG_Pipe {
// the other.
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7;
inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8;
// P4a's four. FOUR AND NOT ONE, for P3a's reason one level out: a framebuffer path that
// regressed, a texture path that regressed, a sampler path that regressed and a program
// path that regressed are four different findings, and clearing one must not disarm the
// other three.
//
// FOUR OF THE SIX FAMILIES CARRY A DEPENDENCY and it is diagnosed at the first use, never
// half-run - one Resolve<Family>SubsystemArm per family beside the backend's existing
// ResolveResourceSubsystemArm, and lazy rather than at bring-up because a pre-flight child
// dying on a signal makes a whole lane SKIP green.
//
// THE ROWS ARE IN SubsystemDeps.def, ONCE (P3b/P4b R-5), and this comment no longer
// restates them. It used to, and it was WRONG in two ways that nothing could fail: it said
// "THREE OF THEM" when there are six rows, and it said "the mirror pairs (10 without 11,
// 10 without 9, 7 without 10) are all fine" when 10-without-11 is precisely D-K2's FOURTH
// row (ID-14/ID-15) - refused by the server and withheld by the client, with PipeFill.cpp
// saying so out loud: "The brief's original 'bit 10 without 11 is fine' is WITHDRAWN for
// P4a as built." The audit found the same pair of defects repeated in
// MG_Backend/DirectGLES/Managers.h's header, i.e. in the header of the file that
// implements the refusal.
//
// THE MIRROR PAIRS THAT REALLY ARE FINE, said out loud rather than left as an absence,
// because a table is only trustworthy if what it does NOT contain was decided:
// - bit 10 set, bit 9 clear: FINE. The legacy FBO sync reaches the texture twin through
// SyncTextureObjectToBackend, which dispatches to the handle arm by itself.
// - bit 11 set, bit 9 clear: FINE, for the same reason - a sampler view names a texture,
// never a framebuffer.
// - bit 7 set, bit 10 clear: FINE, and it is P3a's shipped configuration.
// - bit 12 set with any or none of 9/10/11: FINE, per its own row.
// - bit 10 set, bit 11 clear and its mirror are NOT fine; that is the fourth row.
// One bit each for the framebuffer, texture-resource, sampler and program families. Bits
// 0-13 are fixed on (ConfigLoader.cpp refuses a cleared one at startup); the dependencies
// between bits are in SubsystemDeps.def.
inline constexpr Uint64 kMGPipeSubsystemFramebuffer = 1ull << 9; // set_framebuffer_state
inline constexpr Uint64 kMGPipeSubsystemTextureResources = 1ull << 10; // texture + renderbuffer
// resource_*, set_texture_params
@@ -143,7 +108,7 @@ namespace MobileGL::MG_Pipe {
// the three unit sets
inline constexpr Uint64 kMGPipeSubsystemPrograms = 1ull << 12; // shader CSO, draw/dispatch
// program, global constants
// P5e's one (MG_Remote/CONTRACT-P5E.md §1). The INDEXED BUFFER BINDING POINTS: the three
// P5e's one (docs/archive/contracts/CONTRACT-P5E.md §1). The INDEXED BUFFER BINDING POINTS: the three
// dirty bits NewConstBuffers / NewShaderBuffers / NewSoTargets, the set_shader_buffers
// emitter, and the backend's four binding-point walks. It is its own bit and not part of
// bit 7's resource family for the reason every other split here has: a UBO/SSBO binding
+12 -53
View File
@@ -12,43 +12,12 @@
#include "MGPipeHandles.h"
#include "MGPipeTypes.h"
// The backend -> frontend reverse channel, named (plan B section 7.1).
//
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
// objects. An in-process driver interface has no vocabulary for shadow writeback, GPU-write notification or
// default-framebuffer geometry, because in such a design the state tracker and the driver share an
// address space. Naming them as callbacks is the deliberate deviation (D8).
//
// SEVEN, NOT THE TEN PLAN B WROTE. Three went, each because nothing produces it.
//
// OnTexturePullRequest, the server asking the client to re-send a texture's levels after
// re-minting it (with op 50 ResourceSubDataComplete as the forward terminator). P9 W2 measured
// that no re-mint needs the client: Espryt's server reads the GPU level back and replays its own
// staged store, Magma copies GPU to GPU, on every arm (notes/p9/W2-REMINT.md). So the callback
// went; op 50 stays in the opcode table as a retired row (opcodes only ever grow) and its decoder
// still declines it.
//
// OnXfbScatterReady went with the
// design it belonged to: plan B put the XFB scatter on the CLIENT (the server would hand
// back the packed scratch and the client would run the patch loop over its own shadow), so
// there had to be a callback that told the client the layout. P5c/P5f went the other way -
// the server reads its OWN staged shadow, scatters there and returns the reconciled range
// through OnBufferWriteback, which needs no second callback - and the declaration was left
// behind with zero producers, zero consumers and no EventKind of its own. Deleted here
// (P3b/P4b espryt D1 slice 3) rather than carried: an entry that cannot fire is one every
// reader of this file has to rule out by hand, and the size assertion below made it look
// load-bearing.
//
// And OnLog, deleted in P9 W3 for the same reason: zero producers, zero consumers, no
// EventKind. Plan B pictured the server's log lines as reverse-channel records; they never
// were. They travel the CONTROL plane as LogLine frames (protocol.fbs), written by the server's
// logger through its log forward (MG_Remote/Transport/LogForward.h) - which is also where the
// severity policy this entry's comment promised ("<= WARN lossy, >= ERROR lossless and rate
// limited") now lives, on the level LogLine has always carried.
//
// Installed at context creation. In a monolith these are direct calls; under split they are
// records on the reverse channel, and their ORDER is a correctness requirement rather than
// an optimization (section 7.4).
// The backend -> frontend reverse channel as named callbacks (ARCHITECTURE.md §8.1): shadow
// writeback, GPU-write notification, surface geometry and the like, which an in-process driver
// interface has no names for. Installed at context creation; direct calls in a monolith,
// records on the reverse channel under split, where their order relative to the command stream
// is a correctness requirement (§8.2). Server log lines travel the control plane as LogLine
// frames (MG_Remote/Transport/LogForward.h), not this channel.
namespace MobileGL::MG_Pipe {
struct MGPipeCallbacks {
// A driver-detected GL error that only the server could have seen. The text is
@@ -76,22 +45,12 @@ namespace MobileGL::MG_Pipe {
// Null-initialized: a backend that installs nothing sends nothing.
inline MGPipeCallbacks gMGPipeCallbacks{};
// HOW MUCH OF ITSELF ONE REVERSE-CHANNEL RECORD MAY BE, and it is not a tuning knob: a
// producer that ignores it kills the server process.
//
// OnBufferWriteback's bytes travel INLINE in one SEG_EVENT record, and a ring producer
// refuses any record above Capacity()/2 OUTRIGHT (Transport/Ring.h) - not "when full", but
// always, because above half a capacity a record is placeable at some head offsets and not
// at others, so waiting for room would be a hang. The server's answer to a refusal is
// Fatal{EventRingOverflow}, and SEG_EVENT defaults to 256 KiB, so ANY producer that posts a
// whole range wider than ~128 KiB is a server that dies on a large enough buffer.
//
// THE PRODUCERS ARE IN MG_Backend, which cannot reach MG_Remote::Client's
// BufferWritebackSliceBytes (that one is the CLIENT-linkage answer to the same question,
// for requests going the other way) and must not reach ServerSession either. So the
// transport publishes the one number through this hook, exactly as it publishes segment
// resolution through gMGPipeSegmentResolver: the SERVER role installs it at Accept and
// releases it at Close, and a monolith build leaves it null.
// Size of the SEG_EVENT ring, which bounds one reverse-channel record. OnBufferWriteback
// carries its bytes inline, and the ring producer always refuses a record above half its
// capacity (Transport/Ring.h) with Fatal{EventRingOverflow}, so producers slice wide ranges
// (MGPipeBufferWritebackSliceBytes). The producers live in MG_Backend and cannot reach the
// transport, so the server role installs this query at Accept and clears it at Close; a
// monolith build leaves it null.
using MGPipeEventRingCapacityQuery = Uint64 (*)();
inline MGPipeEventRingCapacityQuery gMGPipeEventRingCapacityBytes = nullptr;
+8 -16
View File
@@ -80,22 +80,14 @@ namespace MobileGL::MG_Pipe {
// Framebuffer), so a real allocation starts at 1.
inline constexpr Uint32 kMGPipeFirstAllocatableSlot = 1;
// ShaderCso slot space. The top 1/16 of it is reserved for PROGRAM PIPELINE COMPOSITES
// (section 5.6.3): a composite is minted client-side out of the stage programs bound to
// a pipeline object, and the server never learns it is a composite - it is just another
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
// lifetime bookkeeping out of the ordinary program slot allocator.
//
// THE ONE ENTRY POINT INTO THE BAND is MGPipeSlotAllocator::AllocateComposite(lifetimeId)
// (MG_Impl/Pipe/SlotAllocator.h, P4a D-H7). MGPipeSlotAllocator::Allocate REFUSES the band
// for kind ShaderCso, which is what makes "an ordinary program can never be handed a
// composite slot" a property of the allocator rather than of its callers; the band carries
// its own exhaustion assert, so exhausting it is a named Fatal rather than silent slot
// theft from ordinary programs. A composite's slot has TWO independent release paths - the
// pipeline cache's LRU eviction and the composite ProgramObject's own destructor - and
// both go through one client-side death helper (MG_Pipe/PipeMutation.h's
// MGPipeEmitShaderCsoDestroyAndFree), whose second call is a proven no-op because Free
// refuses a slot that is not live at that generation.
// ShaderCso slot space. The top 1/16 is reserved for program pipeline composites
// (ARCHITECTURE.md §5.6): a composite is minted client-side from a pipeline object's stage
// programs and is just another ShaderCso to the server. A band rather than a flag keeps the
// composite resolver's lifetime bookkeeping out of the ordinary program slot allocator. The
// only way in is MGPipeSlotAllocator::AllocateComposite (MG_Impl/Pipe/SlotAllocator.h);
// Allocate refuses the band for ShaderCso, and the band has its own exhaustion check. A
// composite slot has two release paths (pipeline cache LRU eviction, the composite
// ProgramObject's destructor), both through MGPipeEmitShaderCsoDestroyAndFree (idempotent).
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
+7 -43
View File
@@ -6,49 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The definitions behind MGPipeRenderStateSpans.h and behind the two arrays
// generated/PipeSpanTable.inc has declared since P0. Compiled ONLY under
// MOBILEGL_PIPE_PUSH (CMakeLists.txt appends it to SOURCE_FILES there), which is how the
// pull build gains no symbol from the split - a declaration emits nothing.
//
// PROVENANCE OF THE PIPELINE HALF. It began as the enumeration
// VulkanRenderer::ComputePipelineStateHash carried above itself, which was the contract
// that function had without being able to say so; it moves here because this file is now
// that contract. Verbatim, from VulkanRenderer.cpp at feat/disaggregated@48268068:
//
// Value hash over every fixed-function GL state the pipeline payload reads that
// the memo key's other fields (mode, program hash, vertex-input hash, render-pass
// hash, transform flags) do not already pin down. Enumerated against the payload
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op,
// min sample shading, the glSampleMaski word
// - front/back stencil ops + compare funcs (ref/mask are dynamic state)
// - per draw buffer up to the render pass's colour span: indexed blend enable,
// blend factors/equations, indexed colour write mask (broadcast from index 0
// when the device lacks independentBlend - the same read the payload does)
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
//
// P2's pipeline half is a strict SUPERSET of that list. It adds SampleCoverageValue,
// SampleCoverageInvert, FrontFaceModeSetting, ProvokingVertexModeSetting,
// ScissorTestEnabledMask, PolygonModeBack, the eleven capability bools the hash never read
// (DebugOutput, DebugOutputSynchronous, Dither, LineSmooth, PolygonOffsetLine,
// PolygonOffsetPoint, PolygonSmooth, SampleAlphaToCoverage, SampleAlphaToOne, SampleCoverage,
// ProgramPointSize) and the three capabilities P2 gave storage to (FramebufferSrgb,
// DepthClamp, TextureCubeMapSeamless). All of them are written by a setter that calls
// BumpVersions(), so under the header's rule they are pipeline. The alternative - demoting
// those setters to ++m_version - would change MG_State semantics in the PULL build for the
// sake of the push path. Growing the subset costs nothing measurable: the hash runs only
// when m_pipelineStateVersion moves, which is exactly when Magma recomputed
// ComputePipelineStateHash before.
//
// The render-pass facts are deliberately NOT here. ComputePipelineStateHash's signature is
// (colorAttachmentCount, rasterizationSamples) and it folds ResolveEffectiveSampleMask, so
// it was never a pure function of RenderStateParameters; a CSO handle cannot replace it on
// its own and Magma keeps renderPassHash as a separate memo-key component.
// Definitions behind MGPipeRenderStateSpans.h and the two chunk arrays that
// generated/PipeSpanTable.inc declares (ARCHITECTURE.md §5.3). The pipeline half holds every
// byte a BumpVersions() setter writes, which covers all fixed-function state Magma's pipeline
// build reads and more; the extra bytes cost nothing because the subset is hashed only when
// m_pipelineStateVersion moves. Render-pass facts (colour attachment count, rasterization
// samples and the effective sample mask derived from them) are not in RenderStateParameters,
// so Magma keeps renderPassHash beside the CSO handle in its pipeline memo key.
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
+8 -24
View File
@@ -12,30 +12,14 @@
#include "MGPipeTypes.h"
#include "MGPipeValueTypes.h"
// G7: the pipeline/dynamic split of RenderStateParameters, written in EXACTLY ONE PLACE
// (ARCHITECTURE.md 5.3, D-B1).
//
// The rule that decides the split, and it is the only rule:
//
// A byte of RenderStateParameters is in the PIPELINE half if and only if some public
// RenderState setter that calls BumpVersions() writes it. Every other byte is in the
// DYNAMIC half. There is no third set.
//
// That makes the G7 invariant - the pipeline-subset hash moves IF AND ONLY IF
// m_pipelineStateVersion moves - true by CONSTRUCTION rather than by inspection, and it is
// what MG_Test/Pipe/RenderStateSpansTest.cpp walks every setter to confirm.
//
// The chunks alternate: chunk 0 is dynamic, chunk 1 is pipeline, and so on, so the whole
// table is 16 BOUNDARIES rather than 15 hand-written ranges. Every boundary is an offsetof
// or a sizeof - never a literal - because a python guess at a layout it cannot see is
// exactly the drift the setter-consistency test exists to catch. 8 dynamic chunks + 7
// pipeline chunks = 15, and both counts fit the Uint32 ChunkMask of MGPRenderStateDesc and
// MGPDynamicState with room to spare.
//
// Note the two splits are ORTHOGONAL and coexist (ARCHITECTURE.md 5.3): DirectGLES'
// head [0, 312) / blend [312, 536) / tail [536, 1168) spans cut ACROSS this table, and
// nothing about them changes. StencilFaceState is deliberately NOT reordered - reordering
// would move Espryt's shadow bytes for no gain.
// The pipeline/dynamic split of RenderStateParameters, written only here (ARCHITECTURE.md §5.3).
// Rule: a byte is in the pipeline half if and only if some public RenderState setter that calls
// BumpVersions() writes it; every other byte is dynamic. So the pipeline-subset hash moves
// exactly when m_pipelineStateVersion moves; MG_Test/Pipe/RenderStateSpansTest.cpp checks every
// setter. Chunks alternate dynamic/pipeline from chunk 0, so 16 boundaries describe 15 chunks
// (8 dynamic, 7 pipeline; each count fits the Uint32 ChunkMask), every one an offsetof or
// sizeof, never a literal. DirectGLES' head/blend/tail spans cut across this table
// independently; StencilFaceState is not reordered, as that would move Espryt's shadow bytes.
namespace MobileGL::MG_Pipe {
namespace MGPipeRenderStateChunkDetail {
+148 -476
View File
@@ -12,24 +12,14 @@
#include "MGPipeHandles.h"
#include "MGPipeHostSpan.h"
// Every MGPipe payload (plan B section 4.5). Each one is a flat POD with explicit padding,
// carries a static_assert on trivial copyability and one on its exact size, and never
// contains a pointer: MGHostSpan, the one shape that changes with the transport, only ever
// rides in a variable tail (draw_vbo's user indices, set_shader_buffers' named-UBO bytes),
// never inline in a fixed payload.
//
// Sizes are asserted rather than merely documented because the wire records generated from
// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a
// protocol break that no test would otherwise see.
//
// P0.5 DEBT, half repaid. The MG_State half is gone: the (since deleted) ResidualValueBlock's
// RenderStateParameters and PixelStoreParameters now come from MGPipeValueTypes.h, so
// this header no longer reaches RenderState.h (its closure still touches TextureEnum.h,
// through BackendObject.h, for the reason in the next sentence). What remains is MGPCaps embedding
// MG_Backend's DynamicBackendParameters - deliberate, the caps block IS that struct
// (section 4.4.1) - and that one include is what still keeps purity gate A (section
// 10.3) off this header; the gate asserts MGPipeValueTypes.h instead. The caps block
// needs fixed-width members before it can move (a type change, not a move): P1/P7.
// Every MGPipe payload (ARCHITECTURE.md §4). Each is a flat POD with explicit padding, a
// static_assert on trivial copyability and one on its exact size, and never contains a pointer:
// MGHostSpan, the one shape that changes with the transport, rides only in a variable tail.
// Sizes are asserted because the generated wire records (generated/PipeWire.inc) are memcpy'd,
// so a field changing width is a protocol break no other test would see.
// BackendObject.h supplies DynamicBackendParameters (embedded in MGPCaps), BackendType and,
// through TextureEnum.h, the texture enums; that reach into MG_Backend is why the include-purity
// gate (scripts/check_include_closure.py) checks MGPipeValueTypes.h rather than this header.
#include <MG_Backend/BackendObject.h>
#include "MGPipeValueTypes.h"
@@ -133,24 +123,15 @@ namespace MobileGL::MG_Pipe {
// buffer. So the answer is the SERVER's table, published as a bit and read through
// MGL_BACKEND_SLOT_CAP. The first of the "XFB span family" bits SlotCaps.h predicted.
kCapBackendOwnsXfbCapture = 1ull << 9,
// P5e (MG_Remote/CONTRACT-P5E.md §1, §6). THE SERVER APPLIES AN UNBARRIERED RECORD
// WITHOUT READING CLIENT MEMORY: every object it needs is resolved from a handle the
// record or the applier state carries, so the client may publish and move on instead
// of waiting for appliedSeq at every verb (rule F).
//
// It is set in the DirectGLES arm of the server's CallMask and NEVER in the
// DirectVulkan one - Magma keeps the lockstep for the whole of P5e (§6), its
// apply-thread allocator debt is real, and a bit published there would turn that debt
// into torn reads rather than into the accounted pulls it is today. On Espryt the arm
// is gated on kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp), false until the P5e
// integration commit, so every package lands with the wait rule inert.
//
// The client latches RunAheadArmed() = m_barrierArmed && Ipc.RunAhead &&
// Caps().HasCap(kCapRunAheadApply) at the first caps adoption after Start; a later
// snapshot may only turn it OFF, because a caps re-run must not be able to make a
// lockstep server run-ahead in the middle of a frame.
// The server applies an unbarriered record without reading client memory: every object
// it needs is resolved from a handle in the record or the applier state, so the client
// may publish and move on instead of waiting for appliedSeq (ARCHITECTURE.md §11.6).
// Published through MGPipeRunAheadCapBitsFor, gated per backend by a readiness constant
// in MG_Backend/Init.cpp. ClientSession::LatchRunAheadFromCaps arms run-ahead at the first
// caps adoption after Start; a later snapshot may only disarm it, because records already
// published without a wait cannot be taken back.
kCapRunAheadApply = 1ull << 10,
// P11 B2 (MG_Remote/CONTRACT-P11.md B2). THIS SESSION MAY RUN T0: the client asked for
// P11 B2 (docs/archive/contracts/CONTRACT-P11.md B2). THIS SESSION MAY RUN T0: the client asked for
// it in its Hello (LinkTerms.adoptTier = 0) on a shared-segment plane, the server's
// allow switch is on, this platform has AHardwareBuffer, and the backend's POST
// self-test of the sustained-lock pattern passed on its live context. Never set in the
@@ -358,31 +339,14 @@ namespace MobileGL::MG_Pipe {
Uint32 StorageFlags; // glBufferStorage flags
Uint8 HasDefinedContent; // false after a NULL-data respecify - STORAGE-DEFINING
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
// THE SCOPE OF A RESPECIFY (P5 R-13.3b, CONTRACT-P5.md table 1 row 19b). Zero means
// "this respecify redefines the WHOLE resource"; non-zero means it redefines exactly
// the (RespecifiedUploadTarget, RespecifiedLevel) pair below and nothing else.
//
// It exists because the applier ALREADY takes that scope - as a trailing
// const MGPRespecifiedLevel* (PipeApply.h:792-795) - and MGPResourceDesc could not
// express it, so it was the second of resource_respecify's two companions with no wire
// carrier. PH-4 extends that carrier with exact W/H/D for the named level, which the
// server validates before staging bytes. The difference is not cosmetic: a null scope drops EVERY pending upload,
// because every level's coordinate system has just been replaced, while a per-level
// one drops only that key. Crossing without the scope would make every mutable
// per-level glTexImage*D on the far side look like a whole-resource redefinition and
// silently eat the texels of every other level - exactly the loss the server-side
// pending-upload set exists to prevent.
//
// READ IT THROUGH THE HELPERS BELOW, never by touching the fields: the presence byte,
// target/level pair and exact per-level extent form one value. For a named image level,
// BufOffset/BufSize temporarily carry W/H/D; the target makes this safe because a
// texture-buffer range cannot legally name a mip level. The applier clears that carrier
// before storing the descriptor.
//
// NOT STORAGE-DEFINING, and not metadata either: it does not describe the resource at
// all, it describes what this CALL replaces. MGPipeResourceRespecifyNeedsAck and the
// storage-defining field set below are unaffected by it, which is why it is not named
// in either.
// The scope of this respecify. Zero: it redefines the whole resource and the applier
// drops every pending upload. Non-zero: it redefines only the (RespecifiedUploadTarget,
// RespecifiedLevel) pair below and drops only that level's uploads, so a per-level
// glTexImage*D keeps the other levels' texels. Use the helpers below: the presence byte,
// the pair and the level's exact extent are one value, and for a named level
// BufOffset/BufSize carry W/H/D (a texture buffer cannot name a level) until the applier
// clears them. It describes what the call replaces, not the resource, so it is neither
// storage-defining nor metadata (see MGPipeResourceRespecifyNeedsAck below).
Uint8 HasRespecifiedLevel;
Uint8 Pad0;
// ImageBindableHint and BindMask above are the two METADATA fields the rule exists
@@ -396,9 +360,9 @@ namespace MobileGL::MG_Pipe {
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
// cannot hold one.
Uint32 GlNameForDiag;
// The pair HasRespecifiedLevel above gates. Byte for byte the two members of
// MGPRespecifiedLevel (PipeApply.h:792-795), in that order, so the applier's existing
// struct and this carrier are one layout - but spelled as two Uint16s rather than as
// The pair HasRespecifiedLevel above gates. Byte for byte the first two members of
// MGPRespecifiedLevel (PipeApply.h), in that order, so the applier's existing
// struct and this carrier share that pair's layout - but spelled as two Uint16s rather than as
// that type, because PipeApply.h includes THIS header and not the other way round.
Uint16 RespecifiedUploadTarget;
Uint16 RespecifiedLevel;
@@ -558,23 +522,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPDynamicState, 32);
// Both views travel, and neither is derivable from the other: the resolved
// VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means
// "element size" while a binding-model stride of 0 means "every vertex reads the same
// element" (section 4.5.3). IsLong and Type == Float64 are carried separately.
//
// THE BLOB RULE - ONE RULE FOR EVERY RECORD IN THIS HEADER THAT CARRIES AN MGPBlobRef,
// and MGPSubData below is the other one. `Blob.Size` is the record's own statement of how
// many bytes its blob holds, and the applier holds the record to that statement WHENEVER
// THE RECORD MAKES IT: a non-zero Blob.Size that disagrees with the byte length the
// record's other fields describe is Fatal{ProtocolCorruption} and the call is refused. A
// ZERO Blob.Size means "this record does not declare its blob", which is what a monolith
// emission is - the bytes travel beside the record through the apply entry point's
// companion `const void*` and no MGPBlobRef is filled - and it is not a fault. Either way
// the bytes read are bounded by the record's OTHER fields (the two counts here, the
// destination range there), so the length is a cross-check and never the safety property.
// A transport that fills these in gets a real gate on the first record it truncates; a
// client that leaves them zero gets no verify-build abort for a field it never used.
// Both views travel and neither derives from the other: the resolved VertexAttribute[32]
// and the binding points, because a pointer-call stride of 0 means "element size" while a
// binding-model stride of 0 means "every vertex reads the same element". IsLong and
// Type == Float64 are carried separately.
// Blob rule for every record here with an MGPBlobRef: a non-zero Blob.Size must equal the
// length the record's other fields describe, or the applier refuses the call with
// Fatal{ProtocolCorruption}; zero means "not declared" (monolith passes the bytes beside the
// record). The other fields always bound the read; Blob.Size is only a cross-check.
struct MGPVertexElements {
MGPipeHandle Cso;
Uint32 AttributeCount;
@@ -730,66 +685,24 @@ namespace MobileGL::MG_Pipe {
Uint16 Level;
Uint32 Layer;
Uint16 UploadTarget; // static_cast<Uint16>(MobileGL::TextureUploadTarget)
// P4a, ID-12 / esprytobj DV-5: WAS Pad0, and the size did not move - the two bytes
// were already here. static_cast<Uint16>(MobileGL::TextureTarget), and
// kMGPipeSurfaceNoTextureTarget on every point that is not a texture.
//
// THE FOUR CROSS-OBJECT MASKS ARE WHY IT EXISTS. IsSnormFallbackAttachment,
// IsUnormFallbackAttachment and IsAlphaWidenedColorAttachment all reduce to
// (format, TEXTURE TARGET) - ShouldUseCaveatTextureFormat(format, target) and
// BackendTextureFormatAddsAlpha(format, target) - and no TextureUploadTarget ->
// TextureTarget inverse exists anywhere in the tree, so UploadTarget cannot answer
// them. Without this field D-C1's promise that the inline InternalFormat makes the
// masks "fall out at push time with no lookup" is unkeepable and the backend keeps
// reading the frontend attachment objects.
//
// CONSULTED ONLY WHEN Kind == kMGPipeSurfaceKindTexture. A zero-initialised record
// carries 0, which is TextureTarget::Texture1D and not the sentinel; that is not a
// defect, because such a record is Kind == None and names no texture at all. Gating
// on Kind is the reader's contract.
// static_cast<Uint16>(MobileGL::TextureTarget), or kMGPipeSurfaceNoTextureTarget on a
// point that is not a texture. The snorm/unorm-fallback and alpha-widened attachment
// masks depend on (format, texture target), so the backend answers them from this record
// instead of reading the frontend attachment objects.
// Read it only when Kind == kMGPipeSurfaceKindTexture: a zeroed record carries 0
// (Texture1D), which is harmless only because its Kind is None.
Uint16 TextureTarget;
};
MGP_ASSERT_POD(MGPSurface, 24);
// P4a, D-C2/D-C3 and ID-19: THE RECORD DESCRIBES A FRAMEBUFFER OBJECT, and Target says
// whether it ALSO moves a binding.
//
// A `Named` record describes the framebuffer object it names (Fbo) and changes NO
// binding. `Draw` / `Read` / `Both` records describe the same object AND set the bound
// handle(s) of the target(s) they name.
//
// The applier therefore keeps records PER FRAMEBUFFER OBJECT, keyed by the handle's slot
// (the generation is checked on lookup and a stale one refuses; a framebuffer has no wire
// lifetime - D-I2, the catalogue has no framebuffer delete - so a successor's record
// simply OVERWRITES the slot), plus the two bound handles. And every DSA entry point that
// hands a framebuffer to the server BY NAME - BlitNamedFramebuffer, the four
// ClearNamedFramebuffer*, and the DSA attachment / draw-buffer / read-buffer setters at
// their validate point - is PRECEDED BY A Named RECORD, so that any framebuffer the
// server is about to receive by name already has one.
//
// That last rule is the phase's main correction, not a nicety. With only the two
// bound-target records, glClearNamedFramebufferfv(fbo) on an unbound fbo made the backend
// mint a fresh driver framebuffer with NO ATTACHMENTS, find no record for it, decline,
// and then issue the clear against it anyway - GL_INVALID_FRAMEBUFFER_OPERATION and
// nothing cleared, where the legacy arm cleared correctly. Writing such an object into
// the bound-target record instead would have been worse: the applier would then claim it
// is bound.
//
// GL has two independent framebuffer bindings and one record carries one Fbo, so a
// Draw/Read pair is two records and one object bound to both targets is one record with
// Both. The draw-buffer array belongs to the OBJECT the record names; it reaches the
// driver's bound draw framebuffer only for a record whose Target is Draw or Both -
// Espryt's own comment records the Minecraft 26.x OIT bug where a READ-only sync landed
// glDrawBuffers on the wrong framebuffer - and a Named record's draw buffers are applied
// when that object is next configured, never to whatever happens to be bound. ReadSurface
// is resolved from THAT framebuffer's own read buffer in EVERY record, Named included,
// which is what makes the read-buffer-shared-FBO defect class unrepresentable rather than
// merely fixed.
//
// Target IS A ContentHash INPUT (the hash covers the whole record), and the emitter's
// suppressor must be keyed by the framebuffer the record names, not by one global slot:
// two different objects' Named records in a row must both go out, and a Named record must
// never be suppressed against the same object's bound record or the reverse.
// Every framebuffer record describes the object named by Fbo; Target says whether it also
// moves a binding. Draw/Read/Both set the named binding(s), Named sets none. GL has two
// independent bindings, so a draw/read pair is two records and one object bound to both is
// one Both record. The applier stores records per framebuffer object (Fbo's slot, generation
// checked; a successor overwrites the slot) plus the two bound handles. Every DSA entry point
// that hands a framebuffer over by name is preceded by a record for it (Named when the object
// is bound to neither binding); without one the backend would meet an object with no record
// and act on an empty driver framebuffer.
enum class MGPipeFramebufferTarget : Uint8 {
Draw = 0,
Read = 1,
@@ -813,37 +726,13 @@ namespace MobileGL::MG_Pipe {
static_assert(kMGPipeMaxColorAttachments == MobileGL::kMGMaxDrawBuffers,
"MGPFramebufferState::Color[] and DrawBuffers[] are one array width");
// ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by Fbo - and Target says
// whether it also moves a binding (P4a, D-C2 as corrected by ID-19; see
// MGPipeFramebufferTarget above for the failure that forced it).
//
// A Named record describes that object and changes NO binding. Draw / Read / Both records
// describe that object AND set the bound handle(s) of the target(s) they name. The applier
// keeps these records PER FRAMEBUFFER OBJECT, keyed by Fbo's slot (generation checked on
// lookup; a framebuffer has no wire lifetime - D-I2 - so a successor's record simply
// overwrites the slot), plus the two bound handles; every DSA entry point that hands a
// framebuffer to the server by name is preceded by a Named record.
//
// WHAT Target CHANGES, FIELD BY FIELD. NO FIELD IN THIS RECORD REFERS TO "the currently
// bound framebuffer" - every one of them describes the object named by Fbo - and that is
// the invariant a reader depends on:
//
// Fbo, Color[], Depth, Stencil, ReadSurface, Width/Height/Layers/Samples,
// FixedSampleLocations, IsDefault, Complete
// properties of the object named by Fbo, identical in meaning under every Target.
// In particular ReadSurface is resolved from THAT framebuffer's own read buffer -
// on a Named record too - never from whichever framebuffer is bound to GL_READ.
// DrawBuffers[]
// a property of the named object; it reaches the driver's bound draw framebuffer
// only when Target is Draw or Both. Under Named it is stored with the object and
// applied when that object is next configured.
// Target
// the only binding-specific field: Draw/Read/Both name the binding(s) this record
// also sets, Named names none. It is a ContentHash input.
// ContentHash
// per RECORD, not per object, and the emitter's suppressor is keyed by the
// framebuffer named: a Named record must never be suppressed against the same
// object's bound record, nor one object's Named record against another's.
// No field here refers to "the currently bound framebuffer"; all describe the object named
// by Fbo (see MGPipeFramebufferTarget). ReadSurface is that object's own read buffer under
// every Target. DrawBuffers[] reaches the driver's bound draw framebuffer only for Draw or
// Both; under Named it is stored and applied when the object is next configured. Target is
// the only binding-specific field and a ContentHash input. The emitter's suppressor is keyed
// per named framebuffer, so a Named record is never suppressed against the same object's
// bound record or against another object's Named record.
struct MGPFramebufferState {
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
MGPSurface Color[8];
@@ -1022,7 +911,7 @@ namespace MobileGL::MG_Pipe {
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (RESERVED, P8-F: 0 today;
// a range with nothing to ship carries an empty span, so the two arrays stay aligned).
// MGPShaderBuffers::Class's three values, and the ONLY spelling of them (P5e,
// MG_Remote/CONTRACT-P5E.md §1). The payload's comment has named them since P4a and
// docs/archive/contracts/CONTRACT-P5E.md §1). The payload's comment has named them since P4a and
// nothing numbered them, so the emitter and the applier were one literal each away from
// disagreeing. Uniform is 0 so a zeroed record describes the uniform binding points, which
// is the class every workload has.
@@ -1096,25 +985,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPStreamOutputTargets, 24);
// ---- set_program_bindings (P5e, opcode 80; CONTRACT-P5E.md §1 and §5.5) --------------
//
// THE THREE POST-LINK MUTABLE REFLECTION FIELDS. create_shader_state carries the archive
// as LINKED; glUniformBlockBinding, glUniform1i on a sampler and glShaderStorageBlockBinding
// all move a binding AFTER the link, inside the frontend's LinkArtifacts, so an archive
// snapshot alone is wrong and the server would answer a draw with the bindings the program
// was linked with rather than the ones it is bound with.
//
// Three tails, in this order, because the three live in three different index spaces:
// Int32 BlockBindings[BlockBindingCount] dense, GL uniform-block index
// MGPProgramSamplerUnit SamplerUnits[SamplerUnitCount] sparse, ascending Location
// MGPProgramStorageOverride StorageOverrides[StorageOverrideCount]
// The storage-override map is NAME-keyed BY DESIGN (ProgramObject.h: three index spaces,
// one coordinate), so its key travels as a host span staged whole, exactly as
// set_storage_block_binding's single name does.
//
// Emitted BEFORE create_shader_state at the same validate point: a rebuild inside the verb
// must already see the bindings. A re-issued create CLEARS all three tails, for the reason
// GlobalConstants is cleared - a relink replaces the archive the tails index into.
// ---- set_program_bindings ------------------------------------------------------------
// glUniformBlockBinding, glUniform1i on a sampler and glShaderStorageBlockBinding change
// bindings after link, and create_shader_state carries only the linked archive. Three tails,
// in this order, one per index space: Int32 BlockBindings[] (dense block index),
// MGPProgramSamplerUnit SamplerUnits[] (ascending Location), MGPProgramStorageOverride
// StorageOverrides[] (keyed by block name, staged whole). Emitted before create_shader_state
// at the same validate point so a rebuild inside the verb sees them; a re-issued create
// clears all three, because a relink replaces the archive they index into.
struct MGPProgramSamplerUnit {
Uint32 Location; // the frontend uniform location
Int32 Unit; // the texture image unit glUniform1i put there
@@ -1278,50 +1156,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPSubRegion, 40);
// Carries the union box AND the region list so the SERVER picks the upload shape - the
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
// JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame.
//
// `Target` IS TWO FACTS IN ONE Uint16 (P4a, D-D3 / ID-12), and MGPipePackSubDataTarget
// under the struct is the only spelling of the encoding - nothing may open-code a half:
//
// low byte = MGPipeResourceTarget - WHICH KIND of storage the destination is.
// The applier branches on it: a buffer
// target dispatches into MGPipeResourceOps,
// every other target accumulates a pending
// upload for the texture sync to consume.
// high byte = MobileGL::TextureUploadTarget - WHICH cube face / upload target the level
// belongs to. It is NOT derivable from the
// resource target - six faces share TexCube
// - and 26 enumerators leave a byte ample.
//
// WHY THAT WAY ROUND, AND WHY THE ENCODING LIVES HERE RATHER THAN IN EACH EMITTER. The
// applier's SubDataNamesABuffer tests the WHOLE field == 0, and
// TextureUploadTarget::Texture1D is 0 - so a texture record carrying the bare upload
// enumerator is indistinguishable from a buffer record exactly when its owner is a 1D
// texture, and that texture's upload is dispatched into the buffer path. With the
// resource target in the LOW byte a buffer record's Target stays EXACTLY
// kMGPipeResourceTargetBuffer - P3a's buffer records are unchanged on the wire, their
// upload byte being zero too - while a texture record can never be zero, because no
// texture's MGPipeResourceTarget is. The static_assert under the struct holds that
// invariant, and the applier's whole-field test stays right either way.
//
// THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination
// byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0.
//
// THE BLOB RULE, THE SAME ONE MGPVertexElements ABOVE CARRIES: `Blob.Size` is the record's
// own statement of how many bytes its blob holds and the applier holds the record to that
// statement whenever the record makes it - a non-zero Blob.Size that is not exactly the
// record's own byte size (MGPipeSubDataBufferSize below) is Fatal{ProtocolCorruption} and
// the write is refused. A zero Blob.Size means "this record does not declare its blob",
// which is what a monolith emission is: the bytes travel beside the record through
// MGPipeApplyResourceSubData's companion `const void*`. The destination range is what
// bounds the write in both cases. That caps ONE record at a 2^31-1 offset and a
// 2^32-1 size; a range beyond either is split by the emitter - the same rule, and at
// SEG_STAGE's 32 MiB the far tighter one, that the ring's half-capacity bound already
// imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are
// the only spelling of this convention; nothing else reads the box for a buffer.
// Carries the union box and the region list so the server picks the upload shape
// (ARCHITECTURE.md §6). Target packs MGPipeResourceTarget in the low byte and the
// TextureUploadTarget (the cube face) in the high byte; use MGPipePackSubDataTarget and the
// accessors below. That order keeps a buffer record's Target exactly
// kMGPipeResourceTargetBuffer (SubDataNamesABuffer tests the whole field) and no texture
// record 0. For a buffer, UnionBox.X/W are the byte offset/size (MGPipeSetSubDataBufferRange);
// one record holds at most a 2^31-1 offset and a 2^32-1 size, and the emitter splits beyond.
// Blob.Size follows the blob rule at MGPVertexElements: zero or exactly that byte size.
struct MGPSubData {
MGPipeHandle Res;
// Target is PACKED - see the block above, and read it only through
@@ -1330,25 +1172,12 @@ namespace MobileGL::MG_Pipe {
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
// bytes an untransformed level shadow?
Uint8 SourceIsVerbatimLevelShadow;
// P5 (b1): DOES THIS RESOURCE HAVE A LIVE HOST WRITER RIGHT NOW? One byte out of the
// original pad; the complete mip extent below is a separate payload extension.
//
// It is here rather than on MGPResourceDesc, and that is a ruling with a reason. The
// fact is CONTENT-shaped - "someone may be writing these bytes without telling you" -
// and MGPResourceDesc's only carrier is resource_respecify, which for a BUFFER is
// never classified as metadata-only (PipeApply.cpp's RespecifyRedefinesNoStorage
// refuses the buffer target outright, so that ID-18 M4's "no reallocation ack" is true
// by construction). Announcing a map on a descriptor would therefore have cost a
// spurious reallocation per map - precisely the failure the brief warned about - or a
// change to that rule, which is a P4a contract the buffer family should not be
// re-opening for a flag.
//
// WHO SETS IT: MGPipeEmitResourceSubData, from BufferObject::HasLiveHostWritesForWire.
// WHO READS IT: ApplyBufferWrite, into MGPipeResourceRecord::HasLiveHostWrites, which
// is what IsBufferDrawCleanByHandle asks instead of the frontend object's IsMapped().
// ZERO IS THE ANSWER, NOT THE ABSENCE OF ONE: a record that does not set it says "no
// live writer", which is what every monolith emission means and why the monolith path
// needs no edit at all.
// Whether the buffer has a live host writer (a persistent write map) right now. It rides
// the content record, not MGPResourceDesc: a buffer resource_respecify is never
// metadata-only, so announcing a map there would reallocate. Every buffer content emitter
// in PipeFill.cpp sets it from BufferObject::HasLiveHostWritesForWire, and ApplyBufferWrite
// assigns it to MGPipeResourceRecord::HasLiveHostWrites, so zero means "no live writer".
// IsBufferDrawCleanByHandle reads that record instead of the frontend's IsMapped().
Uint8 HasLiveHostWrites;
Uint8 Pad0[2];
MGPBox UnionBox;
@@ -1411,75 +1240,24 @@ namespace MobileGL::MG_Pipe {
}
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
// P3a, D-A5: the per-record half of resource_respecify's kNeedsAck.
//
// Flags are a PER-CALL static property and resource_respecify serves BOTH glBufferData
// and glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData
// in a world upload; only glBufferStorage is a real synchronous allocation and only it is
// allowed a synchronous ack. So kNeedsAck on the call means "records of this call MAY
// require an acknowledgement" and THIS predicate decides per record. In monolith the ack
// is ((void)0) - the applier is one function call away - and the transport wires the
// doorbell to this predicate when it lands.
//
// P4a, D-A2: THE PREDICATE IS NARROWED TO NAME THE BUFFER TARGET, and that is a
// requirement rather than a tidy-up. glTexStorage* also sets Immutable - it is a real
// descriptor fact the backend reads, and the client must set it - but texture allocation
// is already deferred to sync time in monolith (glTexImage*/glTexStorage* only
// MarkStorageDirty; even glRenderbufferStorage* allocates lazily inside SyncToBackend), so
// splitting changes no observable behaviour and this batch must NOT ack. glBufferStorage
// stays the only entry point allowed a synchronous acknowledgement.
//
// PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage drives glTexStorage2D and
// glRenderbufferStorage idioms through it, and is the negative control for a future
// widening.
// The per-record half of resource_respecify's kNeedsAck: the call flag says a record may
// need an acknowledgement, and this decides. Only an immutable buffer (glBufferStorage) is a
// synchronous allocation; glBufferData is not. glTexStorage* also sets Immutable, but texture
// and renderbuffer storage is allocated lazily at sync time, so neither may ack
// (ARCHITECTURE.md §8.3). PipeCatalogue.ResourceRespecifyAcksOnlyImmutableStorage
// (PipeCatalogueTest.cpp) is the negative control against widening it.
inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) {
return desc.Immutable != 0 && desc.Target == kMGPipeResourceTargetBuffer;
}
// P4a, ID-18 M4: A RESPECIFY WHOSE STORAGE-DEFINING FIELDS ALL EQUAL THE STORED
// DESCRIPTOR IS A METADATA UPDATE, NOT A REALLOCATION.
//
// MGPResourceDesc::BindMask and ImageBindableHint are STICKY facts the client discovers
// AFTER allocation - a texture first bound as a shader image, first used as a render
// target - and they ride resource_create and every resource_respecify. An IMMUTABLE
// texture never has a later respecify, so without a rule those two would reach the server
// only by accident, or never; with one, a mask change after allocation emits a
// resource_respecify that REPEATS the storage the resource already has.
//
// The applier and both twins must classify such a record as a metadata update:
// - NO reallocation acknowledgement. MGPipeResourceRespecifyNeedsAck above still
// answers the per-record question, but a metadata update allocates nothing, so a
// record it classifies as metadata is not acked even when that predicate says the
// call may require one.
// - NO PendingUploads clear when the call names NO level. This REFINES the whole-resource
// clear: identical storage fields with a null MGPRespecifiedLevel clear NOTHING. (The
// level-scoped rule exists because clearing the whole vector on a level-1 definition
// silently dropped level 0's accepted texels; a metadata update must drop neither.) A
// call that NAMES a level is that level's redefinition whatever the descriptor says -
// a non-base level's extent is not a descriptor field - and drops exactly that level
// (P4a final review C-1); the client's mask republish passes null on purpose.
// - The stored descriptor's BindMask and ImageBindableHint ARE updated: the applier
// replaces the descriptor WHOLE with the one the client sent (PipeApply.cpp), and the
// mask in it is the CLIENT's sticky OR (TextureEmit.h's entry, never cleared), so the
// replacement can never lose a bit the record once carried. The twin re-derives its
// storage flags from the new mask on its next sync, recreating backend storage only
// where the backend actually needs it. The record itself is not a request to recreate.
//
// THE STORAGE-DEFINING FIELD SET, named here so that neither side has to guess and a
// later field cannot join it by silence. It is every MGPResourceDesc member except the
// three metadata ones and the padding:
//
// Target, StorageKind, InternalFormat, Width, Height, Depth, ArrayLayers, Levels,
// Samples, FixedSampleLocations, Immutable, Usage, StorageFlags, HasDefinedContent,
// ViewOf, BufferForTexBuffer, BufOffset, BufSize.
//
// `Resource` is the identity the stored descriptor is looked up BY, not a comparand. The
// three fields that may differ on a metadata update are exactly BindMask,
// ImageBindableHint and GlNameForDiag (diagnostics only, never an identity, never a memo
// key). HasDefinedContent is storage-defining ON PURPOSE: glBufferData(size, NULL) at an
// unchanged size is an orphaning reallocation and has to keep clearing, rather than being
// mistaken for a mask change. A field added to MGPResourceDesc must be placed in one of
// the two lists in the same commit - PipeCatalogue pins the struct's size for that.
// A non-buffer respecify whose storage-defining fields all equal the stored descriptor is a
// metadata update: it republishes a BindMask or ImageBindableHint learned after allocation
// (an immutable texture has no later respecify to carry them). It gets no reallocation ack
// and, naming no level, clears no PendingUploads; the descriptor is still replaced whole (the
// mask is the client's sticky OR) and the twin re-derives its storage flags at its next sync.
// Storage-defining is every MGPResourceDesc member but Resource, BindMask, ImageBindableHint,
// GlNameForDiag, the respecify scope and padding. RespecifyRedefinesNoStorage (PipeApply.cpp)
// compares them field by field, so a new member must be added there.
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
// zero regions - that is how a pull that needs nothing is answered.
@@ -1505,29 +1283,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPReadback, 24);
// resource_copy_region = glCopyImageSubData (P5b, CONTRACT-P5B.md i1). The record had no
// producer and no consumer before P5b, so its shape is settled here for the first time:
//
// Src / Dst the two endpoints' handles - kind Texture, or kind Renderbuffer when the
// matching *Target is GL_RENDERBUFFER. The handle is the identity.
// SrcTarget / the GL target enum the APPLICATION named, verbatim (GL_TEXTURE_2D,
// DstTarget GL_TEXTURE_CUBE_MAP, GL_RENDERBUFFER, ...); every one fits a Uint16.
// Espryt's own MakeGLESCopyImageEndpoint does the ES lowering
// (DirectGLES.cpp) and must keep seeing the enum the app passed.
// SrcBox {SrcX, SrcY, SrcZ, Width, Height, Depth} of the copied region.
// SrcGlName / THE P5b DEBT, stated rather than hidden. GLFunctionsTable::
// DstGlName CopyImageSubData takes two CopyImageEndpoints, each a frontend SharedPtr,
// and the server has none. Under inproc and the verb barrier the server
// rebuilds a TEXTURE endpoint from the GL name through the barrier-pulled
// sticky forward gPipeInputs->GetTextureObject(name) (counted in `rsp`,
// retired by P7 / P3b-P4b when the backend takes handles). A GL name is
// NEVER an identity (section 4.2.1); it is the lookup key of a pull the
// handle beside it will replace. No forward hands out a renderbuffer,
// so a RENDERBUFFER endpoint is refused by name in P5b
// (Fatal{UnmigratedVerb, "CopyImageSubData+RENDERBUFFER"}, ID-57's shape).
//
// 64 -> 72: the two names did not fit the four pad bytes, and the record had no wire history
// to preserve.
// resource_copy_region = glCopyImageSubData only; glCopyTex(Sub)Image2D use
// copy_framebuffer_to_texture.
// Src / Dst endpoint handles: kind Renderbuffer when the matching *Target
// is GL_RENDERBUFFER, else Texture. The handle is the identity.
// SrcTarget / DstTarget the GL target the application passed, verbatim: the backend's
// CopyImageSubData does the ES lowering and needs the original enum.
// SrcBox source origin and the copy's extent, which GL states once for both.
// SrcGlName / DstGlName diagnostics only, never an identity.
struct MGPCopyRegion {
MGPipeHandle Src, Dst;
MGPBox SrcBox;
@@ -1562,26 +1325,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPClear, 48);
// MGPClear's two discriminants, and THE ONLY SPELLING OF THEM (P5b, CONTRACT-P5B.md f1).
// Through P5 the list lived in this struct's comment and both the client emitter
// (MG_Remote/Client/EmitTables.h) and the server sink (MG_Remote/Server/PipeApplier.h)
// minted their own copy of the numbers, each flagged "this belongs in MGPipeTypes.h". A
// disagreement between the two copies is a clear of the wrong attachment with the wrong
// value type, which renders plausibly - table 0's "a decoder that open-codes it is the
// class-1 defect". Both files now alias these.
//
// Kind is the comment's order, left to right. ValueClass reuses the numbering
// MG_State/GLState/Core.h gives the identical three-way split on MGPAttribValue::ValueClass.
//
// WHICH GL ENTRY POINT PRODUCES WHICH KIND, so the emitter and the sink cannot disagree:
// glClear(mask) Whole, BufferMask = mask, DrawBufferIndex = -1
// glClearBuffer{f,i,ui}v(GL_COLOR, i, v) Color, DrawBufferIndex = i, ValueClass by suffix
// glClearBufferfv(GL_DEPTH, 0, &d) Depth, DepthValue = d
// glClearBufferiv(GL_STENCIL, 0, &s) Stencil, StencilValue = s
// glClearBufferfi(GL_DEPTH_STENCIL, 0, d, s) DepthStencil, DepthValue = d, StencilValue = s
// The four glClearNamedFramebuffer* forms are the same rows with Fbo = the named
// framebuffer's handle (preceded by its MGPipeFramebufferTarget::Named record, ID-19);
// the bound forms carry Fbo = kMGPipeNullHandle, "the bound draw framebuffer".
// MGPClear's discriminants, shared by the emitter and the sink: a mismatch clears the wrong
// attachment with the wrong value type and still renders plausibly. ValueClass reuses the
// numbering of MGPAttribValue::ValueClass (MG_State/GLState/Core.h).
// glClear(mask) Whole, BufferMask = mask, DrawBufferIndex = -1
// glClearBuffer{f,i,ui}v(GL_COLOR, i, v) Color, DrawBufferIndex = i, ValueClass by suffix
// glClearBufferfv/iv/fi on GL_DEPTH, GL_STENCIL, GL_DEPTH_STENCIL give Depth, Stencil and
// DepthStencil with DepthValue / StencilValue. Named-framebuffer forms set Fbo (after a
// framebuffer record for it); bound forms use kMGPipeNullHandle, the bound draw framebuffer.
inline constexpr Uint32 kMGPipeClearKindWhole = 0;
inline constexpr Uint32 kMGPipeClearKindColor = 1;
inline constexpr Uint32 kMGPipeClearKindDepth = 2;
@@ -1624,29 +1375,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPReadbackInfo, 64);
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): read_pixels_to_buffer / get_texture_image_to_buffer,
// the two readbacks whose destination is a bound GL_PIXEL_PACK_BUFFER. The destination is
// the pack buffer's RESOURCE HANDLE (CONTRACT-P5 row 23: "a PBO destination is a resource
// handle and needs no Seg field"), so nothing comes back and the client does not wait: the
// server reads exactly the tight pixels the reply-slot form would have answered and writes
// them into Dst row by row through the applier's ordinary buffer-write gate, and the client
// marks Dst GPU-written (GpuWritePending.h row 4). A later CPU read of Dst reaches the bytes
// through resource_readback, like any other buffer the GPU wrote.
//
// Src the reply form's record, unchanged: the box, format, type, target and
// level; Src.DstOffset / Src.DstSize are 0 / the tight w*h*d*bpp extent
// and are checked, not used for placement.
// Dst the pack buffer.
// DstBase byte offset in Dst of the first texel of the first row of the first image:
// the application's offset plus GL_PACK_SKIP_{PIXELS,ROWS,IMAGES} already
// applied, because the layout arithmetic is the client's (ReadbackLayout).
// RowStride bytes between two rows in Dst (GL_PACK_ROW_LENGTH and GL_PACK_ALIGNMENT
// applied). Never smaller than a tight row.
// ImageStride bytes between two images in Dst (GL_PACK_IMAGE_HEIGHT applied). Only read
// when Src.Box.D > 1.
// SwapGroup GL_PACK_SWAP_BYTES as the width of the byte group to reverse (2, 4 or 8),
// 0 for no swap - the client's ApplyReadbackByteSwap rule, precomputed there so
// the server does not re-derive the type table.
// read_pixels_to_buffer / get_texture_image_to_buffer read into the bound pack buffer, named
// by its handle Dst, so nothing comes back and the client does not wait: the server writes the
// tight pixels into Dst row by row through the applier's buffer-write gate, and the client
// marks Dst GPU-written so a later CPU read goes through resource_readback. Src is the reply
// form's record (DstOffset/DstSize = 0 / tight extent, checked only). DstBase includes the
// application offset and GL_PACK_SKIP_*; RowStride (never below a tight row) and ImageStride
// (read only when Src.Box.D > 1) apply row length, alignment and image height; SwapGroup is
// GL_PACK_SWAP_BYTES as the byte-group width to reverse (2, 4 or 8; 0 = no swap).
struct MGPReadbackToBuffer {
MGPReadbackInfo Src;
MGPipeHandle Dst;
@@ -1720,22 +1456,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPDrawRange, 12);
// Present when the draw is indirect: draw_vbo's second tail under kDrawIsIndirect (P5b,
// CONTRACT-P5B.md d1), 8-aligned behind the (empty) range tail exactly where the user-index
// span would sit. The client resolves the COUNT itself, so the server never reads an
// indirect command block to learn how many draws there are:
// glMultiDraw*Indirect DrawCount = drawcount, ParameterBuffer = null
// glMultiDraw*IndirectCount DrawCount = maxdrawcount, ParameterBuffer = the bound
// GL_PARAMETER_BUFFER, ParameterOffset = drawcount (the byte
// offset the GL call spells as GLintptr drawcount). The server's
// backend slot takes exactly that pair and reads the real count
// where it does today; nothing about the parameter buffer's
// CONTENT crosses. (ROADMAP P8's HostResolve.cpp is where a
// client-side count resolve would live if a backend ever needed
// the number rather than the buffer; no backend does.)
// Buffer / Offset / Stride the bound GL_DRAW_INDIRECT_BUFFER handle, the command byte
// offset the GL call passed as `indirect`, and the stride (0 =
// tightly packed, as GL spells it - the backend normalises).
// draw_vbo's second tail under kDrawIsIndirect, 8-aligned where the user-index span would
// sit. The client resolves the draw count, so the server never reads a command block for it:
// glMultiDraw*Indirect DrawCount = drawcount, ParameterBuffer = null
// glMultiDraw*IndirectCount DrawCount = maxdrawcount, ParameterBuffer = the bound
// GL_PARAMETER_BUFFER, ParameterOffset = drawcount (a byte
// offset); the backend reads the real count from that buffer
// Buffer / Offset / Stride the bound GL_DRAW_INDIRECT_BUFFER, the `indirect` byte offset
// and the stride (0 = tightly packed; the backend normalises).
struct MGPDrawIndirect {
MGPipeHandle Buffer;
MGPipeHandle ParameterBuffer;
@@ -1796,7 +1524,7 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPFlush, 8);
// DAMAGE ON THE WIRE (eglSwapBuffersWithDamage, docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md
// DAMAGE ON THE WIRE (eglSwapBuffersWithDamage, docs/archive/notes/anland/plan-ahb-dmabuf.md
// item 5). At most kMGPMaxDamageRects rectangles, each x, y, width, height in GL window
// coordinates - origin at the bottom-left, as EGL hands them over. A count of 0 means the whole
// surface, which is also what every record from a client that sends no damage says.
@@ -1819,25 +1547,12 @@ namespace MobileGL::MG_Pipe {
MGP_ASSERT_POD(MGPSwapInterval, 8);
// ---------------------------------------------------------------------------------
// P5b: the five appended rows (MG_Remote/CONTRACT-P5B.md). APPENDED, never inserted:
// the wire opcode is a call's position in PipeCalls.def, so these sit after
// FenceWaitServer at opcodes 72..76 and no earlier opcode moved.
//
// Every one of them is a GLFunctionsTable VERB the class-C census measured as a first
// blocker and for which no existing row could carry the call. Four of the five exist
// because the backend slot does REAL WORK AT THE CALL rather than at the next validate,
// so a validate-time set record (set_shader_images, set_patch_state) cannot stand in for
// it: Espryt's BindImageTexture syncs the unit's binding to the driver on the spot,
// PatchParameteri pushes glPatchParameteri, BindTransformFeedback rebinds the driver
// object and invalidates the capture-binding shadows, and ShaderStorageBlockBinding
// pushes a rebinding onto an already-built driver program. The fifth,
// copy_framebuffer_to_texture, is a copy WITH the read framebuffer as its source, which
// resource_copy_region (resource -> resource) cannot express.
//
// NONE OF THEM HAS AN MGPipeApply* ENTRY POINT AND NONE GAINS ONE. They reach
// MG_Remote::Wire::WireVerbSink like the five P5 class-B verbs (the census's correction:
// a verb reaches the sink, not an applier), and under monolith they have no producer at
// all - the backend's own slot is called directly, byte for byte as today (G2/G14).
// Verbs whose backend slot acts at the call. bind_shader_image, patch_parameter,
// bind_stream_output and set_storage_block_binding cannot be validate-time set_* records,
// because the backend rebinds or pushes to the driver at the call itself;
// copy_framebuffer_to_texture copies from the read framebuffer, which resource_copy_region
// (resource to resource) cannot express. None has an MGPipeApply* entry point: each reaches
// the MGPipeVerbSink.
// ---------------------------------------------------------------------------------
// bind_shader_image = glBindImageTexture (P5b i1). The arguments VERBATIM plus the handle:
@@ -1909,23 +1624,14 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPStorageBlockBinding, 40);
// copy_framebuffer_to_texture = glCopyTexImage2D / glCopyTexSubImage2D (P5b f1). The
// source is the READ framebuffer the server already has bound (set_framebuffer_state), so
// no source handle crosses; the destination is the texture bound to the active unit at
// Target, which the backend slot resolves through the barrier-pulled unit state exactly as
// generate_mipmap does. glCopyTexImage2D (SubImage = 0) also REDEFINES the level, and the
// frontend's own TexImage2D_State has already emitted that level's resource_respecify
// (GL_Texture.cpp's CopyTexImage2D validator) before the backend slot is reached, so the
// record carries the copy and nothing about storage.
// Dst the destination texture's handle (P8 form; the P5b sink does not
// dispatch on it).
// Target the GL texture target enum verbatim (a 2D target or a cube face).
// Level the destination level.
// InternalFormat glCopyTexImage2D's internalformat (GL enum); 0 for the sub-image form.
// X, Y, Width, Height the read-framebuffer rectangle.
// XOffset, YOffset the sub-image form's destination origin; 0 for the image form.
// SubImage 0 = glCopyTexImage2D, 1 = glCopyTexSubImage2D (unmeasured; shares
// the row because the two calls differ in exactly these two fields).
// copy_framebuffer_to_texture = glCopyTexImage2D / glCopyTexSubImage2D. The source is the
// read framebuffer the server already has bound, so no source handle crosses; for
// glCopyTexImage2D the frontend has already emitted the level's resource_respecify.
// Dst the destination texture, resolved by the client at the active unit
// Target, Level the GL target verbatim (a 2D target or a cube face) and the level
// InternalFormat glCopyTexImage2D's internalformat; 0 for the sub-image form
// XOffset, YOffset the sub-image destination origin; 0 for the image form
// X, Y, Width, Height the read-framebuffer rectangle; SubImage 1 = glCopyTexSubImage2D
struct MGPCopyFromFramebuffer {
MGPipeHandle Dst;
Uint32 Target;
@@ -1940,7 +1646,7 @@ namespace MobileGL::MG_Pipe {
MGP_ASSERT_POD(MGPCopyFromFramebuffer, 48);
// ---------------------------------------------------------------------------------
// P5c: the three appended rows (MG_Remote/CONTRACT-P5C.md §5). APPENDED, never inserted:
// P5c: the three appended rows (docs/archive/contracts/CONTRACT-P5C.md §5). APPENDED, never inserted:
// applier_reset is opcode 77, object_death opcode 78, and set_context_values (rv, §5.3)
// opcode 79 - and no earlier opcode moved.
//
@@ -1981,47 +1687,13 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPBindContext, 8);
// shared_image = opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Colour
// buffers the SERVER allocates and exports as dma-buf descriptors, so one client's frames
// reach another client's textures without leaving GPU memory. One row, five operations, all
// answered through the reply slot with an MGPSharedImageReply:
//
// Allocate a new Width x Height Format image; the reply names it, and its descriptor
// reaches the client on the aux socket BEFORE the reply is posted.
// Import the descriptor the client queued on the aux socket (sideband: a
// MGPSharedImageFdOffer naming this record's seq) is identified as an image this
// server exported; the reply names it. Width/Height/Format must match it.
// Release the session's reference to ImageId goes.
// Present the current context's default framebuffer is copied, top row first, into
// ImageId (a window frame becoming a wl_buffer's contents); the reply is posted
// when the copy has completed on the GPU.
// Attach the texture Texture's storage becomes ImageId (glEGLImageTargetTexture2DOES);
// it lasts until the texture's next storage-defining respecify.
// QueryBufferAge EGL_BUFFER_AGE_EXT of the current context's draw surface, answered in the
// reply's BufferAge: how many presents ago the buffer the next frame draws into held
// the frame then presented, 0 = unknown. Asked in the stream, so it is answered
// for the buffer that frame's records will draw into. Format carries
// kMGPBufferAgeDamageRegionFollows when the application declares the region it draws
// (EGL_KHR_partial_update) before it draws.
// SetDamageRegion eglSetDamageRegionKHR for the current draw surface: Damage is the region
// the frame is about to draw (GL window coordinates; 0 rectangles = all of it).
// Flush glFlush/glFinish of a session that renders INTO images (an X server's glamor):
// every image the session used since its last boundary is published as written
// and read by the flush's fence (implicit sync); answered once published.
// NativeFence EGL_ANDROID_native_fence_sync's fence command: a sync_file that signals
// once everything the session submitted so far has completed (and, in a session
// that writes images, what Flush does)
// reaches the client on the aux socket BEFORE the reply is posted, with the reply's
// Format = kMGPNativeFenceFdFollows. Format = 0: the server has no descriptor to give
// (it cannot export fences, and none it gave before can stand in), and the work has
// completed by the time the reply is posted.
//
// A Present carries the region of the frame to copy (DamageCount rectangles of Damage, GL
// window coordinates, origin bottom-left; 0 = all of it): the client keeps per image what the
// image missed since it was last written, and sends that, so the rest of the image is already
// the frame.
//
// ImageId is a server-minted id, never reused within the server's life.
// shared_image: colour buffers the server allocates and exports as dma-buf descriptors, so
// one client's frames reach another client's textures without leaving GPU memory. Every op
// answers through the reply slot with an MGPSharedImageReply; a descriptor (Allocate,
// NativeFence) reaches the client on the aux socket before the reply is posted. Damage holds
// GL window-coordinate rectangles, origin bottom-left, 0 rectangles = the whole surface.
// ImageId is server-minted and never reused within the server's life. Per-op rules:
// ARCHITECTURE.md §15.4.
enum MGPSharedImageOpKind : Uint32 {
kMGPSharedImageAllocate = 1,
kMGPSharedImageImport = 2,
+148 -472
View File
@@ -483,21 +483,11 @@ namespace MobileGL::MG_Pipe {
}
// ----------------------------------------------------------------------------
// D5: the 29 PipeInputs fields that are PURE FUNCTIONS of RenderStateParameters.
//
// Once bind_render_state / set_dynamic_state have assembled the working block,
// copying these out of GLContext a second time would be exactly the per-verb pull
// P2 exists to remove - so the applier DERIVES them instead. Each line below is a
// transcription of the RenderState getter of the same name (RenderState.cpp);
// GLContext's accessors are one-line forwards to those, so this block and the pull
// path answer the same question from the same bytes.
//
// This departs from P1 brief D4's "no derivation logic is re-implemented in
// PipeInputs", deliberately and with a guard: MOBILEGL_PIPE_VERIFY's compare-at-read
// re-reads every one of these from the live context AT EVERY BACKEND READ and
// compares field-wise, so a transcription error is caught on the first draw that
// reads it. RenderStateSpansTest.DerivationMatchesTheFrontendGetters walks every
// setter and checks all 29 against GLContext on top of that.
// The PipeInputs fields that are pure functions of RenderStateParameters. The applier
// derives them from the assembled working block instead of copying them from GLContext:
// each line transcribes the RenderState getter of the same name (RenderState.cpp), which
// GLContext's accessors forward to. MOBILEGL_PIPE_VERIFY's compare-at-read and
// RenderStateSpans.DerivationMatchesTheFrontendGetters catch a transcription error.
// ----------------------------------------------------------------------------
// RenderState::IsCapabilityEnabled, transcribed against the assembled block. One of
@@ -1030,38 +1020,14 @@ namespace MobileGL::MG_Pipe {
// ID-12 says it does, and c0c's own static_assert is what keeps the two readings in
// step: a buffer record's Target is exactly kMGPipeResourceTargetBuffer.
// ID-18 M4: DOES THIS RESPECIFY REDEFINE ANY STORAGE, OR IS IT CARRYING METADATA?
//
// The question exists because a sticky BindMask / ImageBindableHint bit has exactly one
// way onto the wire - a respecify - and an IMMUTABLE texture has no further respecify to
// ride on. So B re-publishes the descriptor when the mask moves, and the applier has to
// tell that call apart from a real redefinition: the first must NOT drop the pending
// uploads standing against the storage (nothing replaced them), the second must.
//
// THE COMPARISON IS FIELD BY FIELD AND NOT A memcmp. MGPResourceDesc carries Pad0/Pad1
// and neither side of this comparison is guaranteed to have written them - the client
// fills a stack descriptor field by field - so a byte comparison of 88 bytes is a coin
// flip on padding, which is the defect clientsp's SamplerParameters cache already paid
// for once.
//
// WHAT IS NOT COMPARED IS THE SHORT LIST AND IT IS THE DEFINITION: BindMask and
// ImageBindableHint (the two this call exists to carry), GlNameForDiag (diagnostics, and
// never an identity - section 4.2.1) and the two padding words. EVERYTHING ELSE IS
// COMPARED, which is deliberately WIDER than "the fields that define an allocation":
// being wide can only mis-classify a metadata update as a redefinition, whose cost is
// one level's pending upload dropped by a call B does not emit; being narrow would
// mis-classify a REDEFINITION as metadata and keep boxes in a coordinate system that no
// longer exists, which is an upload past the end of the new level. The asymmetry decides
// the direction.
//
// A BUFFER IS NEVER METADATA-ONLY, and that is a rule rather than a consequence.
// glBufferData at an unchanged size is the canonical ORPHANING idiom - a real
// reallocation whose whole purpose is that the old store is gone - and glBufferStorage
// is the one entry point in the catalogue allowed a synchronous acknowledgement
// (MGPipeResourceRespecifyNeedsAck). Excluding the target here is what makes ID-18 M4's
// "no reallocation ack" true by construction instead of by a second suppression rule,
// and it costs nothing: the mask that M4 is about is a TEXTURE's (and a renderbuffer's),
// and a buffer has no pending upload for the arm to protect anyway.
// True when a non-buffer respecify carries only metadata: every storage-defining field
// (the set listed beside MGPipeResourceRespecifyNeedsAck in MGPipeTypes.h) equals the
// stored descriptor. The client resends the descriptor when a sticky BindMask /
// ImageBindableHint bit moves (for an immutable texture the only way the bit reaches the
// server), and that call must not drop pending uploads. Compared field by field because
// padding is not reliably written. The set is wide on purpose: too wide can only drop
// uploads on a call the client never sends, too narrow keeps boxes for a level that no
// longer exists. A buffer is never metadata-only: same-size glBufferData orphans its store.
Bool RespecifyRedefinesNoStorage(const MGPResourceDesc& stored, const MGPResourceDesc& next) {
if (next.Target == kMGPipeResourceTargetBuffer) return false;
if (stored.Target != next.Target) return false;
@@ -1088,38 +1054,14 @@ namespace MobileGL::MG_Pipe {
return true;
}
// WHY A DEAD HANDLE IS NOT A TRIP WIRE HERE, and the bounds faults below are - and
// why it is nonetheless COUNTED rather than silently dropped.
//
// A make-current no longer takes the records with it (MGPipeApplierReset), so the
// shared-store case that used to arrive here every context switch does not arrive at
// all: a buffer that outlives a switch keeps its record and its writes keep landing.
// What is left is (a) a genuine protocol error - an unknown slot, a stale generation -
// and (b) ONE legal sequence, which is why this is still a no-op and not a wire:
//
// the served context is torn down
// -> MGPipeApplierReleaseObjectRecords() (the applier goes away with it)
// -> ~BufferObject / ~VertexArrayObject for every object the context still owns
// -> resource_destroy / delete_vertex_elements, each naming a record that the
// line above has already dropped.
//
// Every one of those death notices is legal, unavoidable and arrives after the
// records are gone, and a wire here would abort a verify lane on the ordinary shutdown
// of a context. So the refusal stays a DEFINED no-op - nothing stored, nothing
// dispatched, no serial moved, and the debug assertion names it, which is the shape
// bind_render_state already uses for a dead CSO.
//
// But MOBILEGL_ASSERT compiles out at INFO, which is what all three gate builds and
// every shipped build are, so a no-op alone would make case (a) - a dropped
// glBufferSubData - invisible in every build that matters. Both refusal paths
// therefore go through ResolveResource / ResolveVertexElements below, which COUNT into
// MGPipeApplierState::Refused{Resource,VertexInput}Calls. That is the observable: a
// legal sequence leaves it at 0 and a refused call moves it, in every build.
//
// The faults below are the other class entirely: a record that does not describe its
// own bytes would have the BACKEND read or write outside a store, which is memory
// corruption rather than a dropped call, so those get the tag ARCHITECTURE.md reserves
// for exactly this - Fatal{ProtocolCorruption} - and the record's identity in the line.
// A call naming a record this applier lacks is a defined no-op, not a trip wire, because
// one legal sequence produces it: context teardown runs MGPipeApplierReleaseObjectRecords,
// then the destructors of objects the context still owns send resource_destroy /
// delete_vertex_elements for records already dropped. The refusal stores, dispatches and
// moves nothing; MOBILEGL_ASSERT compiles out at INFO, so the resolvers below count it
// (MGPipeApplierState::Refused*Calls) and a dropped write stays visible in every build. A
// record that would make the backend touch memory outside a store is the other class:
// Fatal{ProtocolCorruption}, with the record's identity in the line.
constexpr const char* kResourceRefusalNote =
"the record is not this applier's; the call is dropped, not applied";
@@ -1299,22 +1241,14 @@ namespace MobileGL::MG_Pipe {
static_cast<Uint32>(zEnd - z)};
}
// D-D5's SAFETY NET, and the whole reason it is server-side state. The client clears
// its own dirty flags AT EMISSION, for the levels whose record this applier accepted;
// Espryt's upload loop has bail arms - an incomplete texture returns early, a
// multisample target refreshes and skips - that today leave the frontend flag set, so a
// naive move of the clear to the client would lose exactly those texels. The emitted
// shape accumulates here instead, it survives any number of bails, and Espryt consumes
// and clears an entry only where it actually uploads.
//
// ACCUMULATION IS THE CLIENT'S OWN MODEL, ONE LEVEL UP. MipmapStorage keeps a union box
// and, behind it, a bounded disjoint rect list, and answers "0 rects" for everything it
// cannot describe that way - which means "upload the box instead" and covers every
// reason at once. So: boxes union; rect lists concatenate; and the moment either side
// says "box only", or the list would outgrow its bound, the entry becomes box only.
// Never a dropped region - the box still covers every texel the dropped list named.
// Returns false when the record names more distinct (target, level) keys than any
// texture can have, which the caller reports as a corrupt record.
// The server-side pending-upload set (ARCHITECTURE.md §6.1). The client clears a level's
// dirty flags when this applier accepts its record, but a backend's upload loop can bail
// (Espryt returns early for an incomplete texture and skips a multisample target), so the
// accepted shape accumulates here until the backend actually uploads it. Boxes union and
// rect lists concatenate; once either side is box-only or the list would exceed
// kMGPipeMaxPendingUploadRegions, the entry becomes box-only, which still covers every
// texel. Returns false when the record names more (target, level) keys than any texture
// can have; the caller reports that as a corrupt record.
Bool AccumulatePendingUpload(MGPipeResourceRecord& stored, const MGPSubData& record,
const MGPSubRegion* regions) {
for (MGPipeResourceRecord::PendingUpload& entry : stored.PendingUploads) {
@@ -1358,50 +1292,14 @@ namespace MobileGL::MG_Pipe {
void PinNoLiveHostWrites(const MGPipeResourceRecord&, MGPipeHandle, const char*) {}
#if MOBILEGL_PIPE_VERIFY
// P5's pin, RELAXED BY P7 WAVE 3 (CONTRACT-P7 §6) - and relaxed rather than deleted,
// because the thing it was guarding is now reachable instead of hypothetical.
//
// WHAT IT USED TO SAY. "The respecify SCOPE has a wire carrier (MGPResourceDesc's
// HasRespecifiedLevel + the pair) and NO PRODUCER: every descriptor P5 builds is
// whole-resource, and the per-level scope still arrives the old way, as the trailing
// MGPRespecifiedLevel* this function does not look at." So any descriptor arriving with
// the carrier set was, by definition, a producer nobody had announced, and a verify
// build refused it.
//
// WHY THAT IS NOW WRONG, AND EXACTLY HOW FAR. The producer landed:
// Wire_Escape_ResourceRespecify (Client/WireTables.cpp:425-433) writes the carrier from
// the pointer through MGPipeSetRespecifiedLevel, and the server codec
// (Wire/PipeWireCodec.cpp:1677-1683) rebuilds the pointer from the carrier. So under a
// split transport EVERY per-level glTexImage*D arrives here with the carrier set, and
// the old pin made verify x split unmeasurable - it fired on the server's apply thread
// during bring-up, before a single verify case could arm:
//
// Fatal{PipeRespecifyScope} resource_respecify {slot=12, gen=0}: the descriptor
// carries a per-level respecify scope (target=258, level=0), and no path in this
// phase may set one
//
// WHAT SURVIVES, AND IT IS THE WHOLE INVARIANT. The pin never cared that the carrier
// was set; it cared that THE TWO HALVES OF THE SCOPE COULD DISAGREE - "the first thing
// that can go wrong is exactly one of the two moving". That hazard did not go away when
// the producer landed, it became LIVE, and it is silent: the applier below drops pending
// uploads by the POINTER, so a descriptor that declares (target, level) while this call
// hands over a null pointer takes the WHOLE-RESOURCE arm and eats every other level's
// texels, and one that hands over a different pair erases the wrong key and keeps the
// one the client stopped owing. Neither shows up as anything but missing pixels, a
// frame later, on whichever level the wire dropped.
//
// So the rule becomes CONTRACT-P7 §6's: A PRODUCER OF A PER-LEVEL SCOPE MUST COVER THE
// RANGE IT DECLARES. Carrier set => the call passes a pointer, and that pointer names
// exactly the declared (uploadTarget, level).
//
// ONE-DIRECTIONAL ON PURPOSE. A whole-resource descriptor says NOTHING about the
// pointer, and must not: that is the monolith shape and it is still the majority of the
// calls in the tree. MG_Impl/Pipe/TextureEmit.h builds a value-initialized descriptor
// (carrier clear, by MGPipeClearRespecifiedLevel's own "the safe default") and states
// the scope in the trailing pointer alone, which is legal, load-bearing and untouched
// here. Checking that direction too would red every monolith glTexImage*D in the tree
// and would be asserting a convention, not an invariant - the pointer is the caller's
// statement, and a caller that has not been converted to the carrier has not lied.
// Verify builds: a per-level respecify scope declared in the descriptor must match the
// `level` pointer. The wire escape (MG_Remote/Client/WireTables.cpp) writes the carrier
// from the pointer and the codec rebuilds the pointer from it. The applier drops pending
// uploads by the pointer, so a declared scope with a null pointer would take the
// whole-resource arm and eat every other level's texels, and a different pair would drop
// the wrong key; both show up only as missing pixels. One-directional: in-process emission
// (MG_Impl/Pipe/TextureEmit.h) leaves the carrier clear and states the scope in the
// pointer alone, which is legal.
void PinRespecifyScopeCoversItsDeclaration(const MGPResourceDesc& desc,
const MGPRespecifiedLevel* level, MGPipeHandle res,
const char* call) {
@@ -1649,24 +1547,13 @@ namespace MobileGL::MG_Pipe {
}
// ----------------------------------------------------------------------------
// P4a: the three kVarTail unit sets share one body.
// The body shared by the three var-tail unit sets, which differ only in the entry type.
//
// They differ in exactly one thing - what an entry IS - and in nothing else: the same
// window rule, the same bound, the same "the entries outside the window are not
// cleared", the same serial discipline. set_vertex_buffers wrote this arithmetic once
// already; a second, third and fourth copy of it would be three more places to get the
// Start + Count overflow wrong.
//
// THE WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED. The record is "the
// last set as received": a set that names four units has said nothing about the other
// 188, and clearing them would unbind textures the client never mentioned.
//
// THE ENTRY'S OWN Unit FIELD IS NOT POLICED, deliberately and for MGPVertexBuffer::
// BindingIndex's reason: the DESTINATION is Start + i, which is the window this
// function has already bounded, and the field beside it is the client's own label for
// the entry. Policing it would hand the emitter a contract this applier cannot justify
// - the two agree by construction or the emitter is broken in a way a unit test on the
// emitter's side is the right place to catch.
// The window [start, start + count) is bounded against the destination (in 64 bits), and
// entries outside it are not cleared: the record is the last set as received, and a set
// naming four units says nothing about the other 188. An entry's own Unit field is not
// checked; the destination is start + i and the field is only the client's label, as
// with MGPVertexBuffer::BindingIndex.
template <class Entry, class ArrayT>
Bool ApplyUnitWindow(const char* call, Uint32 start, Uint32 count, Uint64 contentHash,
const Entry* tail, ArrayT& destination, Uint32& destinationStart,
@@ -1794,24 +1681,14 @@ namespace MobileGL::MG_Pipe {
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
namespace {
// P4a's BELT (ID-39). See MGPipeApplierState::RefusedNoConsumer for the whole argument;
// in one line: acceptance is a contract with the client, and accepting a record on a
// backend that consumes none of them makes the emitter clear a dirty flag the legacy
// pull path still owed.
//
// IT IS THE SAME SIGNAL THE CLIENT'S GATE READS, deliberately - g_resourceOps is the
// table a backend installs at its own bring-up and uninstalls at teardown, and
// MGPipeApplierReset does NOT touch it (see there), so the gate cannot flap between a
// make-current and the emissions that follow it. The two therefore agree on every path
// except the one this exists for: an emitter called directly, without passing PipeFill.
//
// EVERY CALLER PLACES IT AFTER THE RECORD'S OWN SHAPE CHECKS AND BEFORE ANYTHING MOVES.
// A malformed record is Fatal{ProtocolCorruption} whether or not anything would have
// read it - a trip wire that fires only on some backends is a trip wire nobody can
// trust - so the refusal is the LAST thing asked and the first thing that stops the
// write. The three unit-set entry points are the exception and say so at their call
// site: ApplyUnitWindow validates and writes in one step, so the question has to be
// asked in front of it.
// True (and counted in MGPipeApplierState::RefusedNoConsumer) when nothing in this
// process consumes texture, framebuffer, sampler and program records: accepting one makes
// the client clear dirty flags nobody will act on. It asks what the client's gate
// (PipeFill.cpp's P4aFamilyHasItsConsumer) asks: the published consumer bit under a
// transport, the registered op table in process. MGPipeApplierReset leaves g_resourceOps
// alone, so the answer cannot flap across a make-current. Callers ask after the record's
// shape checks (a malformed record is Fatal on every backend) and before anything moves;
// calls that start with a resolution, and the unit sets, ask first.
Bool NoP4aConsumer() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
@@ -1844,27 +1721,14 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplierReset() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.1 / §6 layer 2. With an active transport g_applier is
// SERVER-PRIVATE and the GL thread's reset crosses as the applier_reset RECORD
// (PipeFill.cpp's FreshlyPrimed arm emits it; ServerVerbSink::OnApplierReset runs this
// function ON the apply thread). Reaching here from any other thread WITH A LIVE WIRE
// is the direct call the record replaced - a write into server memory with no wire
// shape, and exactly the revert the red-once gate must catch - so it is a named Fatal
// rather than a silent reset of state another role owns. The window the Fatal
// deliberately does NOT cover is a configured-but-wireless transport: before
// ClientSession::Start() there is no wire for a record to cross (§6 layer 2's
// documented bring-up exception), and a ServerLoop fixture with no client at all has
// none either - there this call is the only reset that exists. Monolith keeps the
// direct call, byte for byte (G1); in a pull build none of this is compiled at all.
// D10: THE MIDDLE CONJUNCT WAS ClientSession::Active() != nullptr ALONE, WHICH IS
// PERMANENTLY NULL IN A SERVER PROCESS - so this guard could not arm at all under
// spawn, in the shape where calling the reset off the apply thread is most likely and
// least recoverable. MGPipeSessionLive() is the same question asked in a way both roles
// can answer, and MGPipeServerArm() is the process-scoped half of "am I the server".
//
// BOTH ARE ADDED AS DISJUNCTS, not substituted. The originals are the only facts a
// fixture without a full session has, and swapping them out silently disarms every such
// test - measured.
// Under a transport the applier is server-private: the GL thread's reset crosses as the
// applier_reset record, and ServerVerbSink::OnApplierReset runs this on the apply thread.
// A direct call from a client-role thread (not the apply thread, not MGPipeServerArm())
// while a session is live writes server memory with no record behind it: a named Fatal.
// Before a session exists (before ClientSession::Start, or a ServerLoop fixture with no
// client) the direct call is the only reset there is. MGPipeSessionLive() and
// MGPipeServerArm() sit beside ClientSession::Active() and OnApplyThread(), not in their
// place: fixtures without a full session have only the latter two.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
(MG_Remote::Client::ClientSession::Active() != nullptr || MG_Pipe::MGPipeSessionLive()) &&
!MG_Remote::Server::ServerLoop::OnApplyThread() && !MG_Pipe::MGPipeServerArm()) {
@@ -1879,22 +1743,13 @@ namespace MobileGL::MG_Pipe {
MGPipeApplier().ScatteredChunkBits = 0;
MGPipeApplier().PatchCarrierComparisons = 0;
MGPipeApplier().PatchCarrierDivergences = 0;
// P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one:
// MGPipeTracker::Update resets itself whenever the context pointer moves and the
// emitter calls this from the walk that follows, so a make-current BACK to a context
// that is still alive lands here too. Everything cleared below is therefore something
// a returning context may not inherit, and nothing else is cleared.
//
// THE OBJECT RECORDS ARE NOT CLEARED. A GL object lives in a share group, not in a
// context: the buffer a returning context is about to write to is the same buffer with
// the same storage, and its record is where the extent and the mutation serial that
// D-A4 re-keys IsBufferDrawClean onto now live. Dropping them here made every
// glBufferSubData after a context switch resolve to nothing and be dropped, with the
// only trace an assertion that compiles out at INFO. They go at the object's own death
// (resource_destroy, delete_vertex_elements) and at MGPipeApplierReleaseObjectRecords.
//
// The OP TABLE is deliberately not cleared either - it is installed and uninstalled by
// the backend's own bring-up and teardown, not by a state reset.
// This runs at every change of the current context, including a make-current back to a
// live context (MGPipeTracker::Update resets whenever the current context changes), so it
// clears only what a returning context may not inherit. Object records stay: GL objects
// live in the share group, and a buffer's record holds the extent and mutation serial its
// later writes and IsBufferDrawClean need. They go at the object's own death and at
// MGPipeApplierReleaseObjectRecords. The op table stays too; the backend installs and
// removes it at bring-up and teardown.
MGPipeApplier().RefusedResourceCalls = 0;
MGPipeApplier().RefusedVertexInputCalls = 0;
MGPipeApplier().RefusedObjectCalls = 0;
@@ -2201,28 +2056,13 @@ namespace MobileGL::MG_Pipe {
const FloatVec4 outer(patch.Outer[0], patch.Outer[1], patch.Outer[2], patch.Outer[3]);
const FloatVec2 inner(patch.Inner[0], patch.Inner[1]);
// THE SECOND TRIP WIRE (D6, D10). The patch trio travels TWICE - once in pipeline
// chunk P0, because it is pipeline state, and once as set_patch_state, because both
// backends bake it into the synthesized control stage from a shader-build path. The
// redundancy is the point: if the two carriers ever part, a stale set_patch_state
// silently clobbers what bind_render_state scattered and the tessellation levels a
// draw uses stop being the ones its CSO was minted for.
//
// Compared BITWISE, because a NaN outer level is a legal glPatchParameterfv value
// (ARCHITECTURE.md 5.2) and must compare equal to itself.
//
// ARMED BY THE APPLIER'S OWN SCATTER LEDGER, not by "some CSO has been bound"
// (PipeApply.h, MGPipeApplierState::ScatteredChunkBits). The question is whether the
// chunk-P0 bytes in the working block are the applier's, and that single condition
// covers both contracts this wire needs: the ORDERING one - a set_patch_state that
// legitimately precedes the first bind of a context has nothing to agree with yet -
// and the VERB-CLASS one - with the render-state subsystem off those bytes are the
// per-verb fill loop's, and FillPoints.def does not publish GetRenderStateParameters
// at kDispatch or kTextureOp, so they go stale there.
//
// It runs in the shipped push build too, because a wire that is compiled out of
// every build a device runs is not a wire. The cost is a 24-byte memcmp on a call
// that is emitted when the tessellation state CHANGES, i.e. about once per program.
// Patch carrier trip wire. The patch trio travels in pipeline chunk P0 and again as
// set_patch_state (both backends bake it into the synthesized control stage); if the two
// ever part, a stale set_patch_state overwrites what bind_render_state scattered. Compared
// bitwise so a NaN level equals itself. Armed only once this applier has scattered the
// trio's chunk itself (kChunksPatchTrio in ScatteredChunkBits): before the context's first
// bind there is nothing to agree with. Runs in every build; the cost is a 24-byte compare
// on a call emitted only when tessellation state changes.
if ((MGPipeApplier().ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) {
++MGPipeApplier().PatchCarrierComparisons;
const Bool agrees = working.PatchVertices == patch.Vertices &&
@@ -2304,26 +2144,13 @@ namespace MobileGL::MG_Pipe {
}
}
// ================================================================================
// P3a: the nine resource entry points (D-A1, D-A2).
// The resource entry points.
//
// WHAT THE APPLIER OWNS HERE IS IDENTITY, EXTENT AND ORDER - NOT CONTENT. A buffer's
// bytes are the backend's; what crosses is a {slot, gen} handle, a flat descriptor and,
// where the call carries content, the client's own shadow base. So each body below does
// three things in this order: resolve the handle against this applier's record, check the
// record against its own declared extent, and only then move the record and hand the call
// to the backend.
//
// NOTHING REGISTERS MGPipeResourceOps IN THIS PACKAGE, so every dispatch below is a null
// check that falls through, and the tree behaves exactly as it did. That is deliberate and
// it is what makes this commit landable on its own: the frontend still dispatches the op
// table these replace, the backend that will register one is a later package, and the
// records these bodies keep are already correct when it does.
//
// THE THREE SERIALS ARE MGGen-CLASS: server-owned, monotone, never crossing the line. No
// MGPipe call may require the client to supply or know one (ARCHITECTURE.md 4.2.2), which
// is why they are incremented here rather than carried in a payload.
// The applier owns identity, extent and order, not content: a buffer's bytes are the
// backend's. Each body resolves the handle against this applier's record, checks the record
// against its own declared extent, and only then moves the record and calls the backend.
// Serials are server-owned and monotonic and never travel in a payload (ARCHITECTURE.md §2.2).
// ================================================================================
Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc) {
@@ -2478,44 +2305,13 @@ namespace MobileGL::MG_Pipe {
// sampling-resolution shutter guarded - the server-side answer moves with it.
MGPipeApplierNoteTextureStateMoved();
// A RESPECIFY REDEFINES A STORE, SO THE PENDING UPLOADS AGAINST THE STORE IT REPLACES
// GO WITH IT - AND ONLY THOSE. They are boxes and rects in a level's coordinate system
// and that space has just been replaced, so a box kept across a shrink would have
// Espryt upload past the end of the new level. But WHICH storage a respecify replaces
// is the call's to say and the descriptor's to say nothing about:
//
// - `level == nullptr` is a WHOLE-RESOURCE redefinition - every glBufferData /
// glBufferStorage, every glTexStorage* (which defines every level at once), every
// texture view - and every key goes.
// - a non-null `level` is ONE glTexImage*D on a mutable texture, which redefines that
// (uploadTarget, level) AND NOTHING ELSE. Dropping the other keys here would lose
// texels the client no longer owes: MG_State's AllocateStorage / MarkStorageDirty
// are per (uploadTarget, level) too, so the re-mark this call causes covers ITS
// level only, while every other level's dirty flag was cleared at its own emission
// (D-D5 step 1). The canonical sequence that loses them is glTexImage2D(0, data) ->
// draw (the shape is emitted and accumulated; Espryt bails because the texture is
// not yet mipmap-complete, Managers.cpp's incomplete-texture early return, which is
// the arm this set exists for) -> glTexImage2D(1, data), which under a blanket
// clear destroys level 0's entry before anything ever uploaded it.
//
// - and a METADATA update (ID-18 M4) with a NULL level drops NOTHING. It refines the
// whole-resource arm rather than contradicting it: the rule is "the uploads against
// the storage this call REPLACES go with it", and a call whose storage-defining
// fields all equal the stored descriptor replaces no storage, so no level's
// coordinate system has moved and every pending box is still described in the
// space it was accumulated in. B re-emits the descriptor when a sticky bind bit
// moves - with a null level, deliberately - which can land between a
// glTexSubImage2D and the sync that consumes it; eating those texels there would be
// C1's bug with a different trigger, and just as silent.
//
// - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE base descriptor moved (P4a final
// review C-1). The companion scope now carries that level's exact W/H/D as well as
// its (uploadTarget, level) key, so the texture twin and staged-byte store update the
// same coordinate system. A metadata-only mask republish passes null and therefore
// cannot consume a standing upload on its behalf.
//
// A buffer never has a pending upload at all, so all three arms are inert for P3a's
// half - which is also why a buffer is never classified as metadata-only (below).
// A respecify drops the pending uploads against the storage it replaces, and only those
// (ARCHITECTURE.md §6.1): a box kept across a shrink would upload past the end of the new
// level. A null `level` replaces the whole resource and drops every key, unless the call
// is metadata-only, which replaces no storage and drops nothing. A named level (one
// glTexImage*D) drops exactly that (uploadTarget, level), even if the descriptor did not
// move; the other levels' dirty flags were cleared at emission, so their keys are the only
// record of texels the backend still owes. Buffers never have pending uploads.
if (level != nullptr) {
// The keys are unique by AccumulatePendingUpload's construction - it looks for the
// pair before it appends - so this erases at most one entry and stops.
@@ -2547,23 +2343,12 @@ namespace MobileGL::MG_Pipe {
DefineTextureWithoutBackendHook(desc, level);
}
// resource_respecify is the catalogue's only kNeedsAck call, and the per-record half
// of that flag is MGPipeResourceRespecifyNeedsAck(desc): glBufferStorage is a real
// synchronous allocation and the only entry point allowed a synchronous ack, while
// glBufferData travels through the same call and must not acknowledge one. In monolith
// the acknowledgement IS the return of this function - the applier is one call away -
// so the predicate has nothing to gate here and is deliberately not branched on: a
// branch whose arms were identical would be dead code the transport would then have to
// find and remove. PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage is what
// keeps the predicate honest until the doorbell reads it.
//
// ID-18 M4 ASKS FOR "NO REALLOCATION ACK" ON A METADATA UPDATE, AND IT IS TRUE HERE BY
// CONSTRUCTION RATHER THAN BY A BRANCH: the predicate is false for every target that is
// not a buffer, and RespecifyRedefinesNoStorage refuses the buffer target outright, so
// MGPipeResourceRespecifyNeedsAck(desc) is false for every record this applier
// classifies as metadata-only. That is why the classification excludes buffers rather
// than suppressing the ack afterwards - a suppression would have been a second rule the
// transport would then have to learn.
// resource_respecify is the only kNeedsAck call, and MGPipeResourceRespecifyNeedsAck(desc)
// narrows the ack to an immutable buffer, i.e. glBufferStorage (ARCHITECTURE.md §8.3). In
// process the ack is this function's return, so the predicate is not branched on;
// PipeCatalogue.ResourceRespecifyAcksOnlyImmutableStorage pins it. A metadata-only update
// never acks: the predicate is false for every non-buffer target, and
// RespecifyRedefinesNoStorage never classifies a buffer as metadata-only.
if (desc.Target != kMGPipeResourceTargetBuffer) return true;
if (g_resourceOps != nullptr && g_resourceOps->Respecify != nullptr) {
g_resourceOps->Respecify(desc.Resource, desc, initialBytes);
@@ -2889,20 +2674,11 @@ namespace MobileGL::MG_Pipe {
#endif
// ================================================================================
// P3a: the five vertex-input entry points (D-G, D-H, D-I).
// The five vertex-input entry points.
//
// THESE FIVE DISPATCH TO NOBODY, and that is not an omission: MGPipeResourceOps is the
// RESOURCE family's table, and the vertex-input calls have no backend hook because the
// backend does not act on them when they arrive. It reads them at its own draw-time sync,
// out of the state below, which is what the three serials here are for - they are what
// retires the twin's wrapping-Uint16-plus-identity patches, so a compare that used to ask
// "is my Uint16 configuration version still the frontend's?" asks "is my Uint64 serial
// still the server's?" instead.
//
// Nothing emits them in this package either: their dirty bits map to no subsystem, the two
// subsystem bits are not in MG_Impl/Pipe/PipeFill.cpp's kMGPipeWiredSubsystems, and the
// emitters beside them are stubs. The state below is therefore correct and unread until
// the packages that wire both ends land.
// None of them calls the backend. The backend reads this state at its own draw-time sync
// and compares the three Uint64 serials kept here (ContentSerial, VertexBuffersSerial,
// IndexBufferSerial) instead of a wrapping frontend version.
// ================================================================================
void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes) {
@@ -2910,25 +2686,12 @@ namespace MobileGL::MG_Pipe {
"create_vertex_elements named the reserved slot 0");
if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
// THE COUNTS ARE CHECKED BEFORE A BYTE OF THE BLOB IS TOUCHED, and the check is the
// record's own self-description: the blob is MGPVertexAttribWire[AttributeCount]
// immediately followed by MGPVertexBindingPointWire[BindingPointCount]. THIS is the
// reason the second view travels at all - a record that declares a BindingPointCount
// it does not carry would otherwise be a shape this gate had to police forever with
// nothing to police it against.
//
// Both counts are bounded by GL's attribute limit, which is also the size of the two
// arrays they are unpacked into, so the bound and the destination cannot drift apart.
// THE COUNTS ARE WHAT BOUNDS THE READ; the declared blob length is a cross-check.
//
// THE BLOB RULE IS THE SAME ONE resource_subdata IS HELD TO (SubDataBoxFault above,
// and MGPipeTypes.h states it on both records): a non-zero Blob.Size must be exactly
// the length the record's other fields describe, and a zero Blob.Size means "this
// record does not declare its blob" - which is what a monolith emission is, since the
// bytes travel beside the record through blobBytes and no MGPBlobRef is filled. One
// rule, both blob-carrying families: a transport that fills the field gets a real gate
// on the first truncated record, and a client that leaves it zero does not abort a
// verify build over a field it never used.
// The counts are checked before any blob byte is read, against the record's own layout:
// MGPVertexAttribWire[AttributeCount] then MGPVertexBindingPointWire[BindingPointCount].
// Both are bounded by kMGPipeMaxVertexAttribs, which also sizes the destination arrays, so
// the counts bound the read. Blob.Size follows resource_subdata's rule (SubDataBoxFault):
// non-zero must equal the length the counts describe; zero means the record does not
// declare its blob, as with in-process emission, which passes the bytes in blobBytes.
const Uint64 attributeBytes = Uint64{desc.AttributeCount} * sizeof(MGPVertexAttribWire);
const Uint64 bindingBytes = Uint64{desc.BindingPointCount} * sizeof(MGPVertexBindingPointWire);
const Uint64 declared = attributeBytes + bindingBytes;
@@ -3143,31 +2906,13 @@ namespace MobileGL::MG_Pipe {
}
// ================================================================================
// P4a: the fifteen object and working-state entry points
// The object and working-state entry points
// ================================================================================
//
// NOT ONE OF THEM DISPATCHES TO A BACKEND FUNCTION POINTER, and that is the structural
// decision the whole phase rests on rather than an omission. Nothing in these families
// reaches the backend at GL-call time today - texture storage only marks a level dirty,
// texture parameters run from the draw-time sync, renderbuffer storage is allocated inside
// SyncToBackend, a sampler twin is created lazily from the program pass, and the
// framebuffer, the unit sets and the program are all resolved at PrepareForDraw - so every
// call below is either an OBJECT RECORD the applier stores or WORKING STATE the applier
// stores, and the backend reads the applier at the sync points it already has, keyed on a
// server-owned Serial instead of a frontend version. MGPipeResourceOps is unchanged.
//
// THE SAME THREE STEPS, IN THE SAME ORDER, AS P3a's NINE: resolve the handle against this
// applier's record, check the record against its own declared extent, and only then move
// the record. The two verdicts are the same two, and the difference between them is the
// whole of this file's discipline - a call naming a record this applier does not have is a
// DEFINED NO-OP that is COUNTED (RefusedObjectCalls), because one legal sequence produces
// it; a record that would make the server index or allocate outside its own storage is
// Fatal{ProtocolCorruption} with the record's identity in the line.
//
// NOTHING EMITS ANY OF THEM IN THIS PACKAGE. The four subsystem bits are not in
// kMGPipeWiredSubsystems, every client emitter beside them is still a stub, and no backend
// reads the records yet - so the state below is correct and unread until the packages that
// wire both ends land, and this tree is behaviourally identical to the contract commit's.
// None calls a backend function pointer: the backend reads these records at its own sync
// points, keyed on server-owned serials. Same order as the resource entry points: resolve,
// check the record against its own extent, then move it. A record this applier lacks is a
// counted no-op (RefusedObjectCalls); one that would index outside storage is Fatal.
// ================================================================================
// w1: the framebuffer record, per bound target
@@ -3220,24 +2965,13 @@ namespace MobileGL::MG_Pipe {
return;
}
// NO HANDLE IS RESOLVED HERE AND NONE MAY BE. A framebuffer has a handle but no wire
// lifetime - the catalogue has no framebuffer create and no framebuffer destroy,
// because a framebuffer is state and this call is the only one that names one - so
// there is nothing to look up, this entry point never counts a refusal, and the record
// it writes OVERWRITES whatever the slot held. That last part is the point rather than
// a compromise: the slot's previous occupant is dead by construction (the client frees
// a framebuffer's slot when the object dies and the allocator bumps the generation on
// reuse), and the record arriving now describes whatever holds the slot today.
//
// The surfaces' Res handles are not resolved either: an attachment PINS its texture,
// and in monolith the frontend's own SharedPtr is that keep-alive, so a refusal here
// would be enforcing a lifetime rule monolith cannot need and split has not defined.
// P4a's belt, after the record's own shape checks and before the table is touched. This
// entry point is the ONE the client can reach without passing PipeFill's gate -
// GL_Framebuffer.cpp's PipePublishFramebufferByName calls the emitter directly at the
// fifteen DSA sites - so on a backend with no consumer this is where those records
// stop. RefusedNoConsumer and not RefusedObjectCalls: the framebuffer family's counter
// contract (see the header) is that no framebuffer call ever moves that one.
// No handle is resolved: a framebuffer has a handle but no wire lifetime (no create or
// destroy record), so this record overwrites the slot; the client frees the slot when the
// object dies and reuse bumps the generation. Attachment Res handles are not resolved
// either: attachments pin their textures on the client (ARCHITECTURE.md §5.6).
// The consumer check follows the shape checks. This is the one entry point the client
// reaches without PipeFill's gate (PipePublishFramebufferByName calls the emitter
// directly), and it counts into RefusedNoConsumer, never RefusedObjectCalls.
if (NoP4aConsumer()) return;
MGPipeFramebufferRecord* record =
@@ -3523,24 +3257,14 @@ namespace MobileGL::MG_Pipe {
return true;
}
// The three of them, and NO STAGE DIMENSION on any of them: MobileGL's texture-unit space
// is one merged array of 192, the same unit may be sampled from two stages, and stage is
// derived server-side from the reflection archive only where the target API needs it.
//
// A NULL HANDLE IN A TAIL ENTRY IS LEGAL EVERYWHERE HERE and is not a refusal: a unit the
// program does not resolve carries a null view, a unit with no sampler object carries a
// null sampler CSO (the texture's built-in sampler applies then, exactly as today), and a
// unit with no texture carries a null resource. None of the three is resolved against a
// record either - a set is WORKING STATE, the records it names are OBJECT state, and the
// backend resolves the pair at its own sync point where both are current.
//
// P4a's belt sits IN FRONT of ApplyUnitWindow on all three, and this is the one place it
// is not last: ApplyUnitWindow validates the window and writes it in the same step, so
// there is no point between the two to stand at. The cost is that on a backend with no
// consumer a malformed window is declined rather than Fatal - which is the right trade the
// one way round it can be made, because that backend would never have been handed the
// window at all (the client's gate stops it) and the shipped configuration that DOES
// consume these still trips the wire.
// The three unit sets carry no stage dimension: the texture-unit space is one merged array
// of 192, a unit may be sampled from two stages, and the server derives stage from the
// reflection archive only where the target API needs it. A null handle in a tail entry is
// legal (unresolved unit; no sampler object, so the texture's built-in sampler applies; no
// texture), and nothing is resolved here: the backend pairs a set with the object records at
// its own sync point. The consumer check runs before ApplyUnitWindow, which validates and
// writes in one step, so a backend with no consumer declines a malformed window instead of
// tripping; the client's gate never sends it one, and a consuming backend still trips.
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
if (NoP4aConsumer()) return;
if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail,
@@ -3583,7 +3307,7 @@ namespace MobileGL::MG_Pipe {
}
// ---------------------------------------------------------------------------------
// P5e's two entry points, DECLARED BY c0e (MG_Remote/CONTRACT-P5E.md §1)
// P5e's two entry points, DECLARED BY c0e (docs/archive/contracts/CONTRACT-P5E.md §1)
// ---------------------------------------------------------------------------------
//
// set_shader_buffers HAS ITS BODY (package sb); set_program_bindings still refuses by name
@@ -3592,25 +3316,14 @@ namespace MobileGL::MG_Pipe {
// quiet return: the picture would be wrong and the lane would be green, which is the one
// failure mode this campaign spends its refusals on.
// set_shader_buffers, ONE RECORD PER CLASS (CONTRACT-P5E.md §5.6, rulings 10/11). The whole
// body is "validate the window, copy the tail into that class's array, take the mask, move
// the serial" - the binding points are WORKING STATE and the objects they name are resolved
// by the backend at its own sync point, from the handle each range carries.
//
// IT DOES NOT GO THROUGH ApplyUnitWindow, and the reason is worth a line rather than a
// diff: that helper's refusal message names "the merged texture-unit space" and the three
// unit sets share one capacity, while this row has THREE windows keyed by Class and two
// refusals of its own that the contract names by string ("SetShaderBuffers.Class" /
// ".Count"). A shared helper that had to grow a class parameter and a message parameter
// would be the same code with more ways to pass the wrong one.
//
// AND THERE IS NO NoP4aConsumer BELT HERE, deliberately. P4a's belt exists because a client
// clears a per-level dirty flag on ACCEPTANCE, so accepting on a backend that consumes
// nothing loses uploads (ID-39). This family's liveness gate is the client's own
// P5eFamilyIsLive (PipeFill.cpp), which asks R-8 for bit 13 in particular - a server that
// does not publish bit 13 never receives one of these records at all - and storing a window
// no backend reads costs nothing and loses nothing, because the emitter's only latch is the
// set-hash suppressor and the record it latched did cross.
// set_shader_buffers: one record per class (uniform, storage, atomic counter). The body
// validates the window, copies the tail into that class's array, takes the writable mask and
// moves the serial; the backend resolves each range's handle at its own sync point. It does
// not use ApplyUnitWindow, which serves the one merged texture-unit space, because this row
// has three windows keyed by Class and its own named refusals. There is no consumer check:
// the client sends these only to a server that publishes the buffer-binding consumer
// (PipeFill.cpp's P5eFamilyIsLive), and an unread window loses nothing because the emitter
// latches only its set-hash suppressor.
void MGPipeApplySetShaderBuffers(const MGPShaderBuffers& hdr, const MGPBufferRange* tail) {
if (hdr.Class >= kMGPipeShaderBufferClassCount) {
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
@@ -3734,27 +3447,12 @@ namespace MobileGL::MG_Pipe {
if (op == MGPWireOp::DrawVbo && payload != nullptr) {
const auto& draw = *static_cast<const MGPDrawInfo*>(payload);
if ((draw.Flags & static_cast<Uint8>(kDrawClientArrays)) != 0) return true;
// ---- ESCALATION (iii) WAS HERE AND IS WITHDRAWN (P5e ra2, ID-133 then ID-136) ---
//
// ID-133 escalated a PLAIN multi-draw (`NumDraws > 1 && !kDrawIsIndirect`) so that
// MultiDrawImpl::RunIndirect's read of the client's GL_DRAW_INDIRECT_BUFFER binding
// became a legal barriered pull instead of an unbarriered Fatal. It worked, and it
// cost a rendezvous on EVERY plain glMultiDraw* on the DEFAULT tier: there is one
// draw opcode - all twenty entry points collapse onto draw_vbo - and the tier is
// chosen on the server per batch (MultiDraw.cpp's ResolveTierForBatch), so no
// predicate both roles can compute names the arm that actually needed it.
//
// THE CHECK ON AN ESCALATION IS "WHICH ARM PAYS FOR IT", NOT "WHICH LANE GOES
// GREEN", and that is the rule this pair of rulings exists to record. The read was
// a save/restore of a GL binding NAME around the tier's own scratch buffer, not a
// data dependency, so the answer was to ask the side that did the binding:
// BoundDrawIndirectBufferId now takes the handle arm its neighbour
// ResolveBoundIndexBuffer already had, and the pull is GONE rather than legalised.
//
// Both halves landed in ONE commit on purpose: the pull retired without this clause
// withdrawn is a cost with no reason, and this clause withdrawn without the pull
// retired puts 18 lane entries back on the unbarriered arm. Neither is a state to
// gate or to measure, so neither was ever a head.
// A plain multi-draw is not escalated. Espryt's indirect multi-draw tiers resolve the
// GL_DRAW_INDIRECT_BUFFER binding from a handle (MultiDraw.cpp's
// BoundDrawIndirectBufferId) instead of reading client state, and the tier is chosen on
// the server per batch, so no clause both roles can compute would name the arm that
// needs it. Judge a new escalation by which arm pays for it (see MGPipeBarriered's
// declaration in PipeApply.h).
}
return false;
}
@@ -3793,20 +3491,12 @@ namespace MobileGL::MG_Pipe {
}
// ================================================================================
// w3: the shader CSO, the two program bindings and the default uniform block
// The shader CSO, the two program bindings and the default uniform block.
//
// AND THE SERVER NEVER RE-LINKS. glslang lives entirely on the client and SPIRV-Cross
// entirely on the server, a file-level cut: what crosses is the per-stage SPIR-V and the
// reflection archive, never source, and there is no server-side compile pool to hand a
// failure back from. Link failure needs no synchronous return either - it is one error log
// plus a bind-program-0 empty draw today, GL_LINK_STATUS is never withdrawn, and the
// synchronous query is answered by the client out of its own ProgramObject.
//
// WHAT THE SERVER STILL SPECIALISES IS NOT THIS RECORD'S BUSINESS. A backend program
// depends on eight more inputs than the artefacts - the draw framebuffer's clamp masks and
// fragColor broadcast count, the storage-block binding signature, the live image formats,
// the patch parameters - and every one of them now arrives through ANOTHER record this
// applier holds. create_shader_state publishes the artefacts; the verb specialises.
// The server never re-links: SPIR-V and the reflection archive cross, never source, and
// GL_LINK_STATUS is answered by the client from its ProgramObject (ARCHITECTURE.md §7).
// The other inputs a backend program is specialised on (framebuffer clamp masks, storage
// binding signature, image formats, patch parameters) arrive through other records.
// ================================================================================
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
@@ -4064,27 +3754,13 @@ namespace MobileGL::MG_Pipe {
++stored->GlobalConstantsSerial;
}
// THE MONOLITH BODY IS A NO-OP AND THAT IS THE WHOLE OF IT: the emulation this names still
// runs, exactly as it does today, on the same code path. What the call site buys is that
// the set of emulations a split server cannot serve is NAMED, GREPPABLE and PINNED, so P5
// and P8 give it teeth by editing one function instead of rediscovering five call sites.
//
// It takes a literal and does nothing with it. Not a log line, not a counter: it sits on
// paths a frame can reach many times, and ROADMAP.md forbids committing hot-path
// instrumentation.
// P5 GIVES IT TEETH, and this is the whole of it: ONE function edit arms FIVE call sites
// (Managers.cpp:5334, DirectGLES.cpp:8051, :8702, :8997, :10623), exactly as the header
// and P4a's comment above promised. Every one of them reaches back into the CLIENT's
// address space - a texture shadow, a CPU mipmap fallback, a host-side copy - and a server
// has no client address space to reach into, so degrading silently is the one outcome that
// must not happen.
//
// THE ARM IS THE TRANSPORT, NOT THE BUILD. build-split runs MOBILEGL_TRANSPORT=monolith in
// every unit and integration-gpu lane, and several of the five are on ordinary monolith
// paths that those lanes exercise (glGenerateMipmap reaches two of them); arming on the
// build would turn 1117 integration cases red for running code that is correct in the role
// they run it in. None of the five is on P5's reduced path, so under a real transport this
// costs nothing and catches a lot.
// MGPipeUnmigratedEmulation, below: a backend emulation that reads or writes client memory
// names itself with a literal. In process it is a no-op and the emulation runs; under a
// transport the server has no client memory, so it aborts with Fatal{UnmigratedEmulation}
// rather than degrade silently. It arms on the transport, not the build, because split
// builds also run in-process lanes where such code is correct. No log or counter: it may sit
// on hot paths. Product code has no call sites; scripts/ci/unmigrated_emulation_sites.py
// and PipeCatalogue.EveryUnmigratedEmulationIsNamedOnce keep the names and sites equal.
namespace {
thread_local Bool tl_serverOriginatedWrite = false;
} // namespace
+166 -568
View File
@@ -11,39 +11,19 @@
#include "MGPipeRenderStateSpans.h"
#include "MGPipeTypes.h"
// P5e (MG_Remote/CONTRACT-P5E.md §2.1): MGPipeBarriered takes an MGPWireOp and reads the
// P5e (docs/archive/contracts/CONTRACT-P5E.md §2.1): MGPipeBarriered takes an MGPWireOp and reads the
// generated wait-class and call-class tables, all of which live in MGPipe.h. It is a sibling
// header in the same layer - MG_Pipe includes MG_Pipe - so the closure does not move.
#include "MGPipe.h"
// The in-process applier: the SERVER half of the calls P2 emits. Under split this file is
// MG_Remote/Server/PipeApplier (ARCHITECTURE.md 8.3); in the monolith it writes
// MG_Backend/MGPipe/PipeInputs' gPipeInputs directly, so a call and its effect are one
// function call apart and nothing is serialised.
//
// THE SERVER'S PER-CONTEXT WORKING BLOCK *IS* PipeInputs::m_renderState. bind_render_state
// and set_dynamic_state scatter their chunks straight into it, which is why DirectGLES'
// SyncRenderState is not one line changed (ROADMAP.md P2, G5): the block Espryt binds by
// const reference is the assembled block. It is also what makes the MOBILEGL_PIPE_VERIFY
// comparator a real oracle instead of a tautology - the compare-at-read now proves
// "assembled == live", field by field, at every backend read.
//
// This header FORWARD-DECLARES PipeInputs rather than including it: the applier's callers
// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend.
//
// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol.
// P4a: create_shader_state carries the reflection ARCHIVE, and in monolith the archive does
// not travel - the two structs ride beside the record through the entry point's companion
// pointers, exactly as P3a's `const void* initialBytes` does (D-H3, the one Blob rule). So
// this header needs their NAMES and never their definitions; the forward declaration is the
// whole coupling and the closure gate is what keeps it one. The verify build is the only
// place the codec runs, and it runs from PipeApply.cpp.
// P5e (pg) ADDS A THIRD NAME AND KEEPS THE RULE: ProgramArchive is what a SPLIT create carries
// - the record's OWN copy of both structs plus the stage of each module - and the record holds
// it through a SharedPtr, which needs no definition either (the deleter is captured where the
// archive is constructed, in the decoder's TU). So the coupling is still three names and no
// closure change, and MG_Backend, which does need the definition, includes
// ProgramArtifactsCodec.h for it.
// The applier: the server half of every MGPipe call (ARCHITECTURE.md §10.1). In one process the
// routing tables call these entry points directly; under a transport the server's PipeApplier
// decodes each record and calls the same ones. bind_render_state and set_dynamic_state scatter
// their chunks straight into PipeInputs::m_renderState, the block Espryt's SyncRenderState
// reads, so the verify comparator checks the assembled block against live state at every read.
// PipeInputs and the program artefact types are only forward-declared (MG_Pipe sits below
// MG_Backend): entry points take artefacts by pointer and a record holds its archive by
// SharedPtr, whose deleter is captured where it is built; users include ProgramArtifactsCodec.h.
namespace MobileGL::MG_State::GLState {
struct LinkArtifacts;
struct SpirvArtifacts;
@@ -74,28 +54,13 @@ namespace MobileGL::MG_Pipe {
// P3a: the handle-shaped resource op table (D-A1)
// ---------------------------------------------------------------------------------
// The SECOND backend op table, beside BufferBackendOps. Registered by the active backend
// at bring-up and cleared at shutdown, exactly as that one is; a null table means "this
// backend has not taken the resource family over", and the frontend then dispatches the
// old way, which is what lets the client half land on its own and what keeps a backend
// whose buffer path is a later phase untouched.
//
// NO FRONTEND TYPE APPEARS HERE, and that is the whole point of the conversion: every
// hook it replaces took a frontend heap reference and four of them read that object's
// shadow bytes. A resource is an MGPipeHandle plus a payload record plus, where the call
// carries content, a companion `const void*`.
//
// THE COMPANION POINTER IS NOT A NEW IDEA - MGPipeApplyCreateRenderState already carries
// a blob beside its POD for the same reason: in monolith a blob needs no MGPBlobRef and
// the pointer is the client's own shadow base, so the call is zero-copy and behaviour is
// unchanged. How those bytes cross under a real transport is that phase's problem and
// that phase's flag edit; resource_respecify deliberately does NOT carry kHasBlob here,
// because a kHasBlob record must own an MGPBlobRef member and MGPResourceDesc has none.
//
// SubDataResident MAY BE NULL and stays nullable on purpose: one backend deliberately
// does not implement it (kOptional in the catalogue), the frontend checks it exactly as
// it checks the op table it replaces, and giving that backend a real implementation is a
// behaviour change that belongs in its own change, not in this migration.
// The backend's resource op table, registered at bring-up and cleared at shutdown. Null means
// no backend consumes resource records (see RefusedNoConsumer and PipeFill.cpp's
// FamilyIsLive). No frontend type appears here: a resource is an MGPipeHandle, a payload
// record and, where the call carries content, a companion `const void*` (in one process the
// client's own shadow, so the call is zero-copy). resource_respecify carries no kHasBlob,
// because such a record must own an MGPBlobRef and MGPResourceDesc has none.
// SubDataResident is kOptional and may be null; callers check it.
struct MGPipeResourceOps {
void (*Create)(MGPipeHandle res, const MGPResourceDesc& desc);
void (*Respecify)(MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes);
@@ -126,7 +91,7 @@ namespace MobileGL::MG_Pipe {
void (*TextureRespecify)(MGPipeHandle res, const MGPResourceDesc& desc,
const MGPRespecifiedLevel* level);
void (*TextureDestroy)(MGPipeHandle res);
// P11 B2 (T0, MG_Remote/CONTRACT-P11.md B2): the two members a backend that can take a
// P11 B2 (T0, docs/archive/contracts/CONTRACT-P11.md B2): the two members a backend that can take a
// client's AHardwareBuffer as a store registers. Both null = this backend has no T0 and
// every session it serves runs T2. Appended after the texture trio for the same reason.
//
@@ -157,27 +122,13 @@ namespace MobileGL::MG_Pipe {
// P3a: the applier's own records (D-G4)
// ---------------------------------------------------------------------------------
// THE SLOT A CLIENT MAY NAME IS BOUNDED, and the bound lives here rather than at the
// client's allocator because the two tables below are grown BY the slot index. An array a
// handle indexes is the right shape for a dense slot space (MGPipeHandles.h) and the price
// of that shape is that one corrupt Uint32 in a payload otherwise arrives at an allocator
// as a four-billion-entry request from inside the bounds gate's own commit. A slot at or
// above these is Fatal{ProtocolCorruption} - the same verdict as any other record that
// would make the server act outside its own storage - and never a resize.
//
// The two numbers differ because the two records do: a resource record is descriptor-sized
// and a vertex-elements record carries both unpacked views at ~1.3 KB, so one bound would
// mean two very different worst cases. Both are far above what a GL application has live
// at once, and NEITHER IS EVER ALLOCATED BY BEING NAMED: the tables grow to the client's
// own dense high-water mark and no further, so the bound costs nothing until a record is
// already corrupt. Package C bounds handle.Slot the same way before
// BackendSlotTable::EntryAt, which resizes on a client-supplied index too.
// P4a: THE BOUND IS PER KIND, not per table, and that is what keeps one number honest
// while the number of tables grows. The slot spaces of kinds Buffer, Texture and
// Renderbuffer are INDEPENDENT (MGPipeSlotAllocator allocates per kind), so three
// different objects can hold slot 7; the applier therefore keeps one Vector per resource
// KIND and indexes it by slot, rather than one Vector indexed by slot alone. Each is
// bounded by kMGPipeMaxResourceSlots and each grows only to its own dense high-water mark.
// Slot bounds. The tables below grow by the slot index a client names, so one corrupt Uint32
// would otherwise become a four-billion-entry resize: a slot at or above its bound is
// Fatal{ProtocolCorruption}, never a resize. Naming a slot allocates nothing beyond the
// client's own dense high-water mark. The bounds differ because the records do (a
// vertex-elements record is ~1.3 KB). Resource bounds are per kind: Buffer, Texture and
// Renderbuffer slots are allocated independently (MGPipeSlotAllocator), so each kind has its
// own table, each bounded by kMGPipeMaxResourceSlots.
inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20;
inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16;
// create_render_state records retain one 396-byte pipeline half each. Keep their
@@ -296,7 +247,7 @@ namespace MobileGL::MG_Pipe {
// DirectGLESBufferDrawProbe pair now pins from both sides.
Bool HasLiveHostWrites = false;
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Nonzero: this
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). Nonzero: this
// texture's level-0 storage IS the server's shared image of this id (an EGLImage target),
// set by shared_image's Attach and cleared by the next respecify that redefines storage.
// The backend resolves the id at its texture sync (MG_Remote::Server::SharedImages::Find)
@@ -326,34 +277,14 @@ namespace MobileGL::MG_Pipe {
// The SamplerViewCso minted for this texture (P4a D-F2: one per ITextureObject,
// re-issued on the same handle whenever the restrictions move).
MGPipeHandle ViewCso = kMGPipeNullHandle;
// THE PENDING-UPLOAD SET, and it is server-side state on purpose (D-D5). The client
// clears its own dirty flags at EMISSION, for the levels whose record the applier
// accepted; Espryt's upload loop has bail arms - an incomplete texture returns early,
// a multisample target refreshes and skips - that today leave the frontend flag set,
// so a naive move of the clear to the client would lose those texels. The applier
// accumulates the emitted shape here instead, it survives any number of bails, and
// Espryt consumes and clears an entry only where it actually uploads.
//
// The verify lane's RETAIN MODE is what gates the shape: a consume-and-clear set
// cannot be recomputed after emission, so the tracker retains the pre-clear set and
// the comparator compares the emitted (UnionBox, RegionCount, Regions[]) against it
// field by field.
//
// THE SET IS KEYED (UploadTarget, Level) AND EVERY KEY IS INDEPENDENT OF EVERY OTHER.
// That is not a detail: a respecify redefines ONE level when it arrives from
// glTexImage*D (MGPipeApplyResourceRespecify's trailing MGPRespecifiedLevel*), so it
// may only drop that one key - the frontend's AllocateStorage / MarkStorageDirty are
// per (uploadTarget, level) too, and the other levels' dirty flags were cleared at
// THEIR emission, so nothing anywhere still owes them.
//
// THE ACCUMULATED RECT LIST MAY OVERLAP, AND A CONSUMER MUST TOLERATE THAT. Behind one
// level the frontend's own model is pairwise disjoint (MipmapStorage keeps it so), but
// this list CONCATENATES the lists of successive emissions and the applier's gate only
// asks that each rect be inside the record's own union box - so two emissions that
// touch the same texels leave two rects that do. Staging N rects therefore uploads
// those texels twice, which is a cost and never a correctness problem; nothing here
// de-duplicates and nothing downstream may assume "the frontend's model" means disjoint
// once the shapes have been accumulated.
// The pending-upload set, server-side state (ARCHITECTURE.md §6). The client clears a
// level's dirty flags once the applier accepts its record, but a backend's upload loop
// may bail without uploading (an incomplete texture, a multisample target), so emitted
// shapes accumulate here and the backend consumes and clears an entry only where it
// uploads. Keys (UploadTarget, Level) are independent: a per-level respecify drops only
// its own key. Regions concatenate successive emissions and may overlap; staging them
// uploads some texels twice, which costs time but stays correct, so no consumer may
// assume them disjoint.
struct PendingUpload {
Uint16 UploadTarget = 0;
Uint16 Level = 0;
@@ -399,24 +330,13 @@ namespace MobileGL::MG_Pipe {
Uint64 Serial = 0;
};
// create/bind/delete_shader_state, plus set_global_constants' per-program half.
//
// THE ARTEFACTS ARE NOT HELD HERE IN MONOLITH: MGPProgramDesc's seven MGPBlobRefs are all
// declared with Size 0 ("this record does not declare its blob") and the LinkArtifacts /
// SpirvArtifacts ride beside the record through the entry point's companion pointers, so
// the applier stores the DESCRIPTOR and the identity and the server reads the frontend's
// own archive. That is what keeps the codec off the monolith hot path entirely; the verify
// build is where it is exercised, by serialising, deserialising and field-comparing before
// storing.
//
// GlobalConstants is the one allocation P4a adds per program, it is bounded by
// Desc.GlobalUboSize, and it is NOT on the hot path: set_global_constants is
// (ShaderCso, Version) keyed and fires at most once per program per frame.
// P5e (MG_Remote/CONTRACT-P5E.md §1): one entry of set_program_bindings' third tail as the
// RECORD owns it. The wire carries the block name as an MGHostSpan into SEG_STAGE, which
// retires with the record that named it (rule C), so the applier copies the bytes here
// rather than keeping the span: a rebuild happens at the next bind, which may be a frame
// later, and by then the staged run is somebody else's.
// create/bind/delete_shader_state, plus set_global_constants' per-program half. The record
// stores the descriptor and adopts the link's artefacts as Archive (see
// MGPipeApplyCreateShaderState). GlobalConstants is bounded by Desc.GlobalUboSize and written
// at most once per program per frame, keyed by (ShaderCso, Version).
// MGPipeProgramStorageOverride is one entry of set_program_bindings' storage-override tail as
// the record keeps it: the block name is copied because its wire span into SEG_STAGE retires
// with the record that named it, while the bindings are rebuilt at a later bind.
struct MGPipeProgramStorageOverride {
String Name;
Int32 Binding = 0;
@@ -433,26 +353,13 @@ namespace MobileGL::MG_Pipe {
Uint64 GlobalConstantsSerial = 0;
Uint64 Serial = 0;
// ---- P5e's server-owned archive (CONTRACT-P5E.md §5.5, gap G-A) --------------------
//
// FILLED BY PACKAGE pg, AND ONLY UNDER A TRANSPORT. This is the whole of rule F for
// this family: before P5e the record stored the DESCRIPTOR and the artefacts rode
// beside it as two companion pointers into the frontend's own ProgramObject, so every
// reflection question the program twin asked - forty accessors in SyncToBackend alone -
// was a read of client memory that `ProgramObject::Link()` REPLACES IN PLACE. Under
// run-ahead the client is already several records past the create, so those reads are
// torn or stale by construction and no amount of care at the read site can fix it.
//
// The client encodes the archive once per link (ProgramEmit.h -> SEG_STAGE), the
// decoder frames and deserialises it, and the record ADOPTS it here - a SharedPtr and
// not a Vector, because a program-pipeline composite and its stage programs are three
// records over one link's artefacts, and because the twin holds the pointer across the
// build it is running. It retires with the record, which is rule C: the staged run the
// bytes arrived in belongs to somebody else by the next frame.
//
// NULL UNDER MONOLITH, and that is the arm selection and not an omission (ruling 1):
// the push-monolith build keeps its frontend arms token for token, so its twin reads
// the frontend's archive as it always has and this pointer is never consulted.
// ---- The program archive the record owns -----------------------------------------
// A snapshot of this link's artefacts, so every reflection read by the program twin is
// of memory the record owns: ProgramObject::Link() replaces the frontend's artefacts in
// place, and a client running ahead may already have relinked. Under a transport the
// decoder deserialises it from SEG_STAGE; in one process PipeRoute copies it without the
// codec. A SharedPtr because a program-pipeline composite and its stage programs share
// one link's artefacts and the twin holds it across a build. Every create replaces it.
SharedPtr<const MG_State::GLState::ProgramArchive> Archive;
// ---- P5e's set_program_bindings tails (CONTRACT-P5E.md §1, §5.5) -------------------
@@ -563,35 +470,13 @@ namespace MobileGL::MG_Pipe {
};
// =====================================================================================
// P14 S5: THE SHARE-GROUP HALF OF THE APPLIER'S STATE.
// The share-group half of the applier's state (design/11-state-ownership.md).
// =====================================================================================
//
// THE TWO HALVES HAVE DIFFERENT LIVES, and until this slice they were one struct with one
// owner (the process), which is what made a second served context either share working state
// it must not share or lose object records it must not lose. They are two structs now because
// they are two OWNERS:
//
// MGPipeObjectRecords a GL object lives in a SHARE GROUP, so these records are keyed by
// {session, share group}: two contexts of one group see one set, two
// NON-shared contexts of one session see two sets - which is the whole
// reason the frontend gives each context its own name space
// (MG_State/GLState/ShareGroupState). A GL object survives a
// make-current and its record is the only thing the backend has left to
// read that object's storage extent and mutation serial out of (D-A4
// re-keys IsBufferDrawClean onto exactly those two); dropping them at a
// make-current made every subsequent glBufferSubData on a pre-existing
// buffer resolve to nothing.
//
// MGPipeWorkingState what the next draw fetches with - bindings, current program, the unit
// windows, the CSO store, the serials. Keyed by {session, context}: a
// returning context may not inherit another's bindings, and two
// contexts of one share group may not overwrite each other's.
//
// THEY ARE CLEARED BY DIFFERENT EVENTS. A record here is cleared by the object's OWN death
// signal - resource_destroy, delete_vertex_elements, which is what D-L makes the buffer's death
// crossing - and by the share group's own teardown (MGPipeApplierReleaseGroupRecords) when its
// last context goes away. MGPipeApplierReset does NOT touch this struct; MGPipeApplierState's
// working half is what a make-current resets.
// GL objects live in a share group, so these records are keyed by {session, share group}:
// contexts of one group see one set, unshared contexts see separate sets. A make-current
// keeps them, because the backend reads a buffer's extent and serial from its record. A
// record is cleared by its object's death signal, or by MGPipeApplierUnregisterContext when
// the group's last context goes; MGPipeApplierReset clears only MGPipeWorkingState.
struct MGPipeObjectRecords {
// ---- object records: indexed by MGPipeHandle::Slot of kind Buffer /
// VertexElementsCso, and NOT part of the working state. They are cleared by the object's
@@ -621,23 +506,12 @@ namespace MobileGL::MG_Pipe {
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
// exactly as they name any other program.
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
// started with could not describe a framebuffer that is bound to neither binding, and
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
// Espryt exactly that.
//
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
// between contexts, but its record is addressed by a slot out of one global allocator,
// so nothing aliases across a switch - and dropping the table would leave a
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
//
// P14 S5 keys it by SHARE GROUP like its siblings, which is safe for the reason just
// given: the slot space is global, so a group's table can only ever answer for handles
// its own contexts minted, and a shared group holding both contexts' records is a wider
// table rather than an aliasing one.
// Framebuffer records, keyed by the framebuffer handle's slot (see
// MGPipeFramebufferRecord), so a framebuffer bound to neither binding - the target of a
// DSA blit or clear - still has one. MGPipeApplierReset keeps this table: on a fresh
// context the client re-emits only the two bound records, so a framebuffer described
// only by name would lose its record. Slots come from one global allocator, so a share
// group's table answers only for handles its own contexts minted and never aliases.
Vector<MGPipeFramebufferRecord> FramebufferRecords;
};
@@ -654,24 +528,13 @@ namespace MobileGL::MG_Pipe {
Vector<MGPipeRenderStateCsoRecord> RenderStateCsos;
// The last bind, so a rebind of the same handle can be answered without a scatter.
MGPipeHandle BoundRenderStateCso = kMGPipeNullHandle;
// The GLOBAL chunk bits (MGPipeRenderStateSpans.h's numbering) this applier has
// itself scattered into the working block since the last reset - its own ledger of
// which bytes of PipeInputs::m_renderState are the APPLIER'S rather than the per-verb
// fill loop's. Both trip wires arm off it, and that is the whole of their contract:
//
// - with the render-state subsystem OFF (MOBILEGL_PIPE_PUSH bit 0 clear - the
// per-subsystem A/B of D14) nothing is ever scattered, the ledger stays empty and
// the wires say nothing. The working block is then the fill loop's, published per
// VERB CLASS (MG_Pipe/FillPoints.def), so at a kDispatch or kTextureOp verb - the
// two classes that publish IsCapabilityEnabled but NOT GetRenderStateParameters -
// it still holds the previous draw's bytes and is an oracle for nothing;
// - with it ON the applier is the block's only writer (D5 takes an emitted field
// out of the fill loop), so the bytes it has scattered are current at every verb
// of every class and comparing against them is honest.
//
// set_patch_state's own write to the working block deliberately does NOT enter the
// ledger: that is the OTHER carrier, and a wire comparing against bytes it had just
// written itself would be a tautology.
// The global chunk bits (MGPipeRenderStateSpans.h numbering) this applier has scattered
// into PipeInputs::m_renderState since the last reset: which bytes of the working block
// are the applier's rather than the residual fill loop's. MGPipeApplySetPatchState's
// trip wire compares against chunk P0 only once P0 is in this ledger: a set_patch_state
// before a context's first bind has nothing to agree with, and bytes the applier did not
// scatter are no oracle. set_patch_state's own write stays out of the ledger: comparing
// that carrier against itself would prove nothing.
Uint32 ScatteredChunkBits = 0;
// What the two trip wires last did. A wire nothing can observe is a gate that cannot
@@ -691,57 +554,23 @@ namespace MobileGL::MG_Pipe {
// build. Per context, like the four render-state wire counters above.
Uint64 RefusedResourceCalls = 0;
Uint64 RefusedVertexInputCalls = 0;
// P4a's, in the same shape and for the same reason: every sampler, sampler-view,
// program and texture-params call this applier refused because it named a record this
// applier does not have. One counter rather than four, because the families share one
// legal refusal sequence (teardown -> MGPipeApplierReleaseObjectRecords -> ~Object ->
// death notices naming records already dropped) and an operator reading a log wants to
// know that ANY object call was dropped; the log line names the call and the handle.
//
// set_framebuffer_state IS DELIBERATELY NOT ON THAT LIST AND CANNOT BE. D-I2 gives a
// framebuffer a handle and NO wire lifetime, so the call resolves no record - there is
// nothing to look up, nothing to find missing and therefore nothing to refuse - and
// MGPSurface::Res is likewise left unresolved on purpose (D-I3: the keep-alives are the
// frontend's SharedPtrs and enforcing them is a later phase's). Its only verdict is
// Fatal{ProtocolCorruption} on a malformed record, and this counter must stay at 0
// across every framebuffer call in every build. ID-19(b) does not change that: the
// per-object table is WRITTEN by that call and never looked up by it, and the refusal
// that the table CAN produce - a lookup whose generation has moved on - happens on the
// server's own read path and is counted apart, in StaleFramebufferRecordLookups.
//
// THE OTHER CLASS IS NOT COUNTED HERE AND MUST NOT BE: a var-tail window outside its
// bound, or a set_texture_params whose BuiltinSampler is the null handle, would make
// the backend act outside its own storage or sample an object that does not exist -
// that is Fatal{ProtocolCorruption}, not a dropped call.
// Sampler, sampler-view, program and texture-params calls refused because they named a
// record this applier does not have. One counter, because the families share one legal
// refusal sequence (a teardown drops records before the objects' death notices arrive);
// the log line names the call and the handle. set_framebuffer_state never moves it: a
// framebuffer has no wire lifetime, so the call looks nothing up, and a stale lookup on
// the server's read path counts in StaleFramebufferRecordLookups. A malformed record (a
// var-tail window out of bounds, a null BuiltinSampler) is Fatal{ProtocolCorruption}.
Uint64 RefusedObjectCalls = 0;
// P4a's BELT (ID-39): every call in one of the four families P4a migrates that this
// applier declined because NO BACKEND HAS REGISTERED MGPipeResourceOps - i.e. because
// nothing in this process consumes what the record publishes.
//
// WHY THE APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL, when it is otherwise
// backend-neutral: acceptance is a CONTRACT WITH THE CLIENT since ID-18 M3. The
// emitters clear a texture level's dirty flags, advance their descriptor mirrors and
// latch their suppressors on the answer this applier returns, so an applier that
// accepts a record nothing will ever read makes the client forget work the legacy pull
// path still owed - which is exactly how 66 texture-upload-shaped DirectVulkan cases
// went red on the push build (ID-39). The client's own gate
// (MG_Impl/Pipe/PipeFill.cpp's FamilyIsLive) stops the emission upstream; this is the
// belt under it, so a record that reaches here by any other route - GL_Framebuffer.cpp's
// PipePublishFramebufferByName calls its emitter directly, without passing PipeFill -
// is declined rather than accepted.
//
// IT IS NOT A DEFECT COUNTER, WHICH IS WHY IT IS SILENT. RefusedResourceCalls,
// RefusedVertexInputCalls and RefusedObjectCalls each mean "a record named something
// this applier should have had"; a non-zero value there is a seam defect. A non-zero
// value HERE is the designed steady state of a backend with no P4a twins, so logging it
// would put an ERROR line in every ordinary Magma run. The number is the observable.
//
// THE DEATH PATHS ARE DELIBERATELY NOT ON THIS LIST. resource_destroy,
// delete_sampler_state, delete_sampler_view and delete_shader_state are idempotent
// cleanup that must keep working whatever the registration did, and with no consumer
// there is no record for them to find anyway (they count their own refusal). Per
// context and cleared by MGPipeApplierReset, like the three above it.
// Non-buffer resource, sampler, program and framebuffer calls declined because nothing
// consumes them: no MGPipeResourceOps is registered and, under a transport, the session's
// caps do not name the family either. The client clears dirty flags, advances its mirrors
// and latches suppressors on acceptance, so accepting a record nobody reads loses work.
// PipeFill.cpp's FamilyIsLive stops these upstream; this catches emitters called directly,
// such as GL_Framebuffer.cpp's PipePublishFramebufferByName. Not a defect, so not logged.
// Idempotent death paths (resource_destroy, delete_*) are not counted. Cleared by
// MGPipeApplierReset.
Uint64 RefusedNoConsumer = 0;
// ---- working state: what the next draw fetches with. All of it is per context and
@@ -894,24 +723,13 @@ namespace MobileGL::MG_Pipe {
// both roles evaluate the same predicate over the same value (§2.1, escalation (i)).
Bool IsTransformFeedbackActive = false;
// ---- P5c (rv, CONTRACT-P5C.md §5.3): the server-side answer for the three texture
// SHUTTERS. FieldOwnership.def moves GetSamplingResolutionGeneration /
// GetTextureBindGeneration / GetTextureContextId to APPLIER_DERIVED - "a shutter, not
// a value: the server answers from its own Serial", which their rows have said since
// P5 - and THESE are the serials, read by the PipeInputs accessors under a
// server-stamped verb instead of the client's residual-fill copy.
//
// TextureShutterSerial answers the two GENERATIONS. It is an MGGen like its seven
// siblings above: server-owned, monotone, ++ on every applied record that can move
// what the frontend's bind / sampling-resolution generations guard - the three unit
// sets, set_texture_params, a respecify or destroy of any resource, a bind_shader_image
// verb (the sink bumps it, PipeApplier.cpp) - and ADVANCED, never zeroed, by
// MGPipeApplierReset / MGPipeApplierReleaseObjectRecords. Over-firing is the safe
// direction for a memo key: a moved serial costs a re-sync, a stuck one renders stale.
//
// ContextSerial answers GetTextureContextId: stable within the served context, moved
// by every MGPipeApplierReset (a make-current is a fresh server, §5.1), which is all
// the backends' per-context memo keys need.
// ---- Server-side answers for the three texture shutters (FieldOwnership.def,
// APPLIER_DERIVED): the PipeInputs accessors answer from these serials under a
// server-stamped verb. TextureShutterSerial answers GetSamplingResolutionGeneration and
// GetTextureBindGeneration; it moves on every applied record that can change what they
// guard (the unit sets, set_texture_params, a resource respecify or destroy,
// bind_shader_image) and resets advance it, never zero it. Over-firing only costs a
// re-sync. ContextSerial answers GetTextureContextId; every MGPipeApplierReset moves it.
Uint64 TextureShutterSerial = 0;
Uint64 ContextSerial = 0;
@@ -1035,22 +853,12 @@ namespace MobileGL::MG_Pipe {
TextureShutterSerial = ContextSerial = base;
}
// ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
// members because the storage moved under them and their callers must not have to know
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
// used to answer - "which record describes the framebuffer bound to this binding" - by
// resolving BoundFramebuffer[t] through FramebufferRecords.
//
// NULL IS A REAL ANSWER AND HAS EXACTLY THREE CAUSES: nothing is bound to that binding
// (the null handle, which is what a make-current leaves and is NOT an error), no record
// has been written at that slot, or the slot's generation has moved on under the handle
// (which IS an error and is counted and logged - see StaleFramebufferRecordLookups). A
// caller that used to test `MGPipeHandleIsNull(st.DrawFramebuffer.Fbo)` tests the
// pointer instead; the two are the same question.
//
// Defined in PipeApply.cpp rather than inline HERE so this header keeps its include
// closure: the stale-generation path logs, and MG_Util/Debug/Log.h is not in this
// header's closure and may not become part of it.
// ---- The framebuffer accessors. DrawFramebuffer() / ReadFramebuffer() resolve
// BoundFramebuffer[t] through FramebufferRecords. Null has three causes: nothing is
// bound (what a make-current leaves; not an error), no record was written at the slot,
// or the slot's generation moved on under the handle (an error, counted in
// StaleFramebufferRecordLookups and logged). Defined in PipeApply.cpp because the
// logging path needs MG_Util/Debug/Log.h, which must stay out of this header's includes.
const MGPFramebufferState* FramebufferRecordFor(MGPipeHandle fbo) const;
const MGPFramebufferState* DrawFramebuffer() const;
const MGPFramebufferState* ReadFramebuffer() const;
@@ -1088,29 +896,13 @@ namespace MobileGL::MG_Pipe {
void MGPipeNotifyCompileEnvChanged();
// =====================================================================================
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
// Whose applier is it? (docs/Disaggregated/design/11-state-ownership.md)
// =====================================================================================
//
// THE SAME SHAPE AS P14 S2's gPipeInputs RESOLVER AND S4's NATIVE TUPLE KEY, one level down:
// a process-wide hook the server's session registry installs ONCE, which answers for the
// CALLING thread. Nothing in MG_Backend or MG_Impl names a session, and no call site moved.
//
// THE KEY IS TWO PAIRS, because the two halves have two owners:
//
// {SessionKey, ContextToken} the WORKING state, one per served context;
// {SessionKey, ShareGroupKey} the OBJECT records, one per share group within the session.
//
// A SHARE GROUP DOES NOT CROSS A SESSION, and this is not a limitation: an EGL share group
// is a property of one client's connection (EGL 1.5 3.7.1 - contexts of one share group must
// belong to one display), and the two sessions are two connections. Cross-session sharing is
// an explicit non-goal of the phase, so keying the first by session is the whole of the rule.
//
// REGISTRATION, NOT INFERENCE. The session layer registers a context the moment the
// CreateContext control frame reaches the apply thread (ServerSession::CreateContext) and
// unregisters it at DestroyContext, so the resolver above stays a two-word read of state the
// session already keeps (its current token) and never has to reach into the session's map
// from a thread that does not own it. A context that was never registered resolves to its
// session's "no context" applier, which is the same object the session's bring-up reads.
// A resolver the session registry installs once answers for the calling thread, so
// MG_Backend and MG_Impl never name a session. Working state is keyed by {SessionKey,
// ContextToken}, object records by {SessionKey, ShareGroupKey}; a share group never spans
// sessions (EGL ties it to one display, i.e. one client connection). Contexts are registered
// on the apply thread at CreateContext; an unregistered token gets a group of its own.
struct MGPipeApplierKey {
Uint64 SessionKey = 0;
Uint64 ContextToken = 0;
@@ -1149,43 +941,14 @@ namespace MobileGL::MG_Pipe {
// invent.
Uint64 MGPipeApplierShareGroupKeyFor(Uint64 sessionKey, Uint64 contextToken);
// ---------------------------------------------------------------------------------
// P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3)
// ---------------------------------------------------------------------------------
//
// ONE FUNCTION, TWO CALLERS, AND THAT IS THE WHOLE POINT. The client calls it at the emit
// table to decide whether to wait after publishing; the sink calls it in ApplyOne to decide
// whether the record it is about to apply may read client memory. If the two answers ever
// differ the failure is silent: the client runs on while the server reads a value that has
// already moved. So it is a PURE FUNCTION OF WIRE-VISIBLE DATA - the static wait-class
// column, the record's own payload, and applier state that a record earlier on the ring put
// there - and both roles evaluate the same three clauses:
//
// 1. MGPipeWaitClassFor(op) != kWaitNone the static column (§2.2)
// 2. kCtxVerb with an open transform-feedback span escalation (i): XFB stays lockstep
// 3. draw_vbo carrying kDrawClientArrays escalation (ii): client vertex arrays
//
// NO OTHER RUNTIME ESCALATION EXISTS. Adding one is an integrator ruling and a row in the
// contract, not a condition somebody adds at a call site - because a fourth clause that only
// one side computes is the same silent failure one level down.
//
// THERE WAS AN ESCALATION (iii) AND IT WAS WITHDRAWN, which is worth a line here rather than
// only in the history: ID-133 barriered a plain multi-draw to legalise a pull in Espryt's
// indirect multi-draw tier, ID-136 retired the pull instead (MultiDraw.cpp's
// BoundDrawIndirectBufferId) and took the clause back out. The rule it leaves behind is the
// one to apply to the next candidate: ask WHICH ARM PAYS for an escalation, not which lane
// it turns green - (iii) was keyed on the only wire fact available and therefore charged the
// default tier for a read only two opt-in tiers made.
//
// Escalation (ii) is a REFUSAL under run-ahead (§5.1), so on a run-ahead server it never
// reaches the sink at all; it is here so the predicate is TOTAL and so the lockstep arm,
// where such a draw is legal, gets the barrier it needs.
//
// `payload` may be null, which answers "the fixed payload is not available here" and makes
// clause 3 fall through - a caller that has an opcode but no bytes (the emit table before it
// has built the record) must not be told a draw is unbarriered on that account, so DrawVbo
// with a null payload is treated as carrying no client arrays and the emit site passes the
// real MGPDrawInfo it is about to publish.
// ---- The barriered predicate (ARCHITECTURE.md §11.6.1) --------------------------------
// The server's ApplyOne asks it whether the record it is about to apply may read client
// memory; the client evaluates the same clauses before it publishes (ClientSession's wait
// rule, PipeFill.cpp's ClientVerbIsBarriered). It reads only wire-visible data (the static
// wait class, the payload, applier state set by earlier records), so both sides can agree.
// Clauses: wait class != kWaitNone; a kCtxVerb inside an open transform-feedback span; a
// draw_vbo with kDrawClientArrays. A new clause goes here and into the client's mirror in
// the same change. A null `payload` skips the last clause.
Bool MGPipeBarriered(MGPWireOp op, const void* payload, const MGPipeApplierState& st);
// Whether the record the apply thread is INSIDE is a barriered one. Server-private: written
@@ -1204,50 +967,22 @@ namespace MobileGL::MG_Pipe {
// the call site (it is the line ra rebases onto); c0e owns the storage so both compile.
void MGPipeApplierSetCurrentRecordBarriered(Bool barriered);
// ---- P5e (gl), ID-128: WAS THIS RECORD BARRIERED BY ESCALATION? ----------------------
//
// MGPipeBarriered is the static wait class PLUS two payload-derived escalations (ID-83): an
// open transform-feedback span makes every context verb inside it barriered, and a draw
// carrying client vertex arrays makes that draw barriered. "Escalated" is therefore exactly
//
// MGPipeBarriered(op, payload, st) && MGPipeWaitClassFor(op) == kWaitNone
//
// and BOTH HALVES ARE ALREADY COMPUTED on every record - ApplyOne computes the predicate
// unconditionally for ID-103's reason and the static class is a table - so this is a bit
// carried beside the existing stamp rather than new plumbing.
//
// IT IS A SEPARATE FLAG AND NOT A REFINEMENT OF THE ONE ABOVE, because the two answer
// different questions. The stamp above answers "is the client parked behind this record",
// which the allocator guard and the frontend-keyed registry guard ask. This one answers "and
// was it parked for a reason the STATIC table cannot see", which only the strict knob asks -
// and it asks because the escalations happen exactly for XFB-active and client-array draws,
// both of which CONTRACT-P5E puts outside this phase (§5.7 and ID-82). A pull on such a
// record is a debt some later phase owes, not a migration P5e skipped.
//
// FALSE IS THE DEFAULT, which is the opposite of the stamp's default and deliberately so:
// "not escalated" is the answer that admits nothing, so a reader that runs before any writer
// is strict rather than lax.
// ---- Was the current record barriered only by escalation? ----------------------------
// True when MGPipeBarriered(op, payload, st) holds but MGPipeWaitClassFor(op) == kWaitNone:
// the transform-feedback or client-array clause alone barriered it. Separate from the stamp
// above, which answers "is the client parked behind this record" for the allocator and
// registry guards. The only reader is the residual-pull check under
// MOBILEGL_IPC_STRICT_ERRORS (PipeInputs.cpp), which admits a pull on an escalated record.
// Defaults to false, so a reader that runs before any writer is strict.
Bool MGPipeApplierCurrentRecordIsBarrieredByEscalation();
void MGPipeApplierSetCurrentRecordBarrieredByEscalation(Bool escalated);
// THE CLIENT'S HALF OF THE WAIT RULE IS NOT LANDED YET, AND THIS CONSTANT IS THAT FACT.
// MGPipeBarriered above describes what the client WILL do; until package ra changes
// EmitAndWaitTails the client still blocks after every record, so the answer the server
// must stamp is `true` whatever the wire says - the two named exemption scopes (§4.4) are
// legal exactly while the client is parked, and it is parked behind all of them today.
// IT FLIPS WITH kMGPipeP5eRunAheadReady, IN THE INTEGRATION COMMIT, AND NOT WHEN ra LANDS
// (ra's amendment to ID-103's wording, which said "the same commit that lands the wait
// rule"). The argument ID-103 makes is about whether the CLIENT is parked, not about
// whether ra's code exists: RunAheadArmed() is a conjunction whose third term is the
// server's caps bit, so between ra landing and the caps bit being published the client
// still blocks after every record - and stamping `false` there would withdraw §4.4's
// exemptions from probes that client's own wait still makes safe, which is exactly the
// "refusal with no defect behind it" ID-103 refused. The two constants are therefore ONE
// switch with two spellings, and the integration commit throws both.
//
// Computing the predicate anyway (PipeApplier::ApplyOne) is deliberate: it keeps the
// function exercised on every record for the whole phase rather than first run on the day
// it starts deciding.
// Whether the client implements the run-ahead wait rule MGPipeBarriered describes. The
// server stamps a record with the predicate only when this and its own run-ahead cap bit
// both hold (MGPipeApplierStampsBarriered); otherwise the client blocks after every record,
// so every record is stamped barriered. It moves together with kMGPipeP5eRunAheadReady
// (MG_Backend/Init.cpp). ApplyOne computes the predicate on every record either way, so it
// stays exercised.
inline constexpr Bool kMGPipeP5eClientWaitRuleLanded = true;
// P5c (rv): the two serials the PipeInputs texture-shutter accessors answer with under a
@@ -1261,43 +996,14 @@ namespace MobileGL::MG_Pipe {
// this, so the bump rule lives in exactly one place.
void MGPipeApplierNoteTextureStateMoved();
// A MAKE-CURRENT, NOT A TEARDOWN - and the distinction is the whole of this function's
// contract. It runs on every change of the current GLContext (MGPipeTracker::Update resets
// the tracker whenever the context pointer moves, and the emitter calls this from the
// first walk that follows), including a make-current BACK to a context that is still alive
// and whose objects are all still there.
//
// So it drops what a returning context may not inherit - the render-state CSOs (whose
// client-side cache is dropped on the line above it, so both sides start over together),
// the residual mirror, and the vertex-input WORKING state - and it ADVANCES the two global
// vertex-input serials rather than zeroing them. It does NOT drop the resource or
// vertex-elements records: those describe share-group objects that the switch does not
// destroy, and dropping them is a dropped write on the far side of it.
//
// P4a EXTENDS BOTH HALVES AND THE RULE IS UNCHANGED (D-J4). Cleared: the two framebuffer
// BINDINGS, the three unit sets, DrawProgram / DispatchProgram / BoundShaderCso - all of it
// per-context working state - with their serials ADVANCED and never zeroed. Not cleared:
// texture and renderbuffer resources, sampler CSOs, sampler views, shader CSOs, the
// framebuffer RECORDS, and the texture params and pending uploads that ride on a resource
// record, because a texture lives in a share group exactly as a buffer does.
//
// ID-19(b) MOVED THE FRAMEBUFFER RECORD ACROSS THAT LINE and the reason is worth stating.
// Before it, the whole framebuffer state was working state and a make-current took it. Now
// the RECORD is an object record and only the two BOUND HANDLES are working state, so a
// switch clears the bindings - after which DrawFramebuffer() / ReadFramebuffer() answer
// null, exactly as the cleared records used to answer a null Fbo - and leaves the table
// standing. Dropping the table instead would silently lose the record of every framebuffer
// that is described by NAME and never bound, because the client's re-emission on a fresh
// context is driven by MGPipeSetHashSuppressor::InvalidateAll, which re-sends the two bound
// records and nothing else.
//
// AND THEREFORE NO P4a TRACKER NEEDS A RE-PUBLICATION PATH ON FreshlyPrimed, AND NONE MAY
// HAVE ONE: re-emitting create_sampler_state for a record the applier still holds would
// move its Serial for nothing. What DOES reset on a fresh context is each emitter's
// BOUND-HANDLE latch - the framebuffer and unit-set hashes through
// MGPipeSetHashSuppressor::InvalidateAll, and the program emitter's BoundShaderCso mirror -
// because those mirror working state this function just cleared.
// A make-current, not a teardown: runs on every change of the current context (the tracker
// resets when the context moves), including a switch back to a live context. It clears the
// working state a returning context may not inherit - render-state CSOs (the client drops
// its cache too), vertex input, framebuffer bindings, unit sets, buffer binding windows,
// program handles - and advances their serials rather than zeroing them. Object records
// survive: they belong to the share group. So on a fresh context emitters re-send only
// bound-handle state (MGPipeSetHashSuppressor::InvalidateAll); re-sending an object record
// would move its Serial for nothing.
void MGPipeApplierReset();
// THE OTHER SCOPE: the served context is going away and its applier with it, so the object
@@ -1415,93 +1121,26 @@ namespace MobileGL::MG_Pipe {
// 0, a descriptor whose target names no resource kind, and a slot at or above
// kMGPipeMaxResourceSlots.
Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc);
// resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is
// the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call,
// MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks.
//
// P4a: `level` IS THE SCOPE OF THE REDEFINITION, and MGPResourceDesc cannot carry it - the
// descriptor describes the resource, and a mutable texture redefines its levels ONE
// glTexImage*D AT A TIME. Null means "this respecify redefines the WHOLE resource" - every
// glBufferData / glBufferStorage, every glTexStorage*, every texture view - and drops every
// pending upload, which is right because every level's coordinate system has just been
// replaced. Non-null names the single (uploadTarget, level) the call redefines and drops
// ONLY that key: the frontend's AllocateStorage / MarkStorageDirty are per
// (uploadTarget, level) as well (MG_State/GLState/TextureState/TextureObject.h), so a
// glTexImage2D(level 1) re-marks level 1 AND NOTHING ELSE, while the levels already
// emitted had their client dirty flags cleared at THEIR emission (D-D5 step 1) and nothing
// anywhere still owes them. Clearing the whole set here would lose exactly those texels,
// silently, in every build - the loss the server-side set exists to prevent.
//
// Trailing and defaulted for W1's reason: P3a's buffer call site (PipeFill.cpp:691) and
// every existing case compile unchanged. PACKAGE B PASSES THE PAIR IT JUST ALLOCATED at
// every per-level respecify; it has both halves in hand at the AllocateStorage call site.
//
// A METADATA RESPECIFY IS A RESPECIFY THAT REDEFINES NO STORAGE (ID-18 M4). A sticky
// BindMask / ImageBindableHint bit reaches the applier only on a respecify, and an
// IMMUTABLE texture has no further one - that is what immutable means - so the canonical
// order (glTexStorage2D, then glBindImageTexture or an FBO attachment) would leave the
// record's hint at 0 for ever, and the hint is what PREVENTS the texture re-mint (a
// server-side readback and re-upload, P9 W2). So B re-emits the descriptor when the mask moves, and a record whose
// STORAGE-DEFINING fields all equal the stored descriptor's is applied as a metadata
// update:
//
// - the descriptor is replaced, so BindMask and ImageBindableHint take their new values;
// - NO pending upload is dropped, whatever `level` says. This REFINES the rule above
// rather than contradicting it: that rule drops the uploads against the storage a
// respecify REPLACES, and a call that replaces no storage replaces no coordinate system
// either, so there is nothing to drop. A mask change arriving between a
// glTexSubImage2D and the sync that consumes it must not eat the texels;
// - the serial advances, which is the whole publication - the twin re-derives its storage
// flags from the new mask at its next sync and recreates only where the backend needs
// it (D's side);
// - and MGPipeResourceRespecifyNeedsAck is false for it BY CONSTRUCTION, because a buffer
// is never classified this way (see the body: glBufferData at an unchanged size is a
// real orphaning reallocation, and glBufferStorage is the one entry point allowed a
// synchronous ack).
//
// Returns true when the descriptor was stored - metadata updates included, since the record
// did move - and false when the call was refused: a descriptor whose target names no
// resource kind, or a handle this applier has no live record for at that generation.
// SHARED IMAGES: the texture `texture`'s level-0 storage becomes shared image `imageId`
// (MGPipeResourceRecord::SharedImageId). False when the texture has no live record.
// MGPipeApplyResourceRespecify (below) replaces the stored descriptor and bumps Serial;
// `initialBytes` is the shadow when desc.HasDefinedContent, else null, and only an immutable
// buffer store acks (MGPipeResourceRespecifyNeedsAck). A null `level` redefines the whole
// resource and drops every pending upload; a named level drops only its own key. A null
// level with unchanged storage-defining fields is a metadata update (how a BindMask or
// ImageBindableHint change reaches an immutable texture): it drops nothing, moves Serial.
// Returns false when refused. MGPipeApplyAttachSharedImage makes `texture`'s level-0 storage
// shared image `imageId` (MGPipeResourceRecord::SharedImageId); false without a live record.
Bool MGPipeApplyAttachSharedImage(MGPipeHandle texture, Uint64 imageId);
Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
const MGPRespecifiedLevel* level = nullptr);
// resource_subdata, buffer half: the destination range rides in the record's box through
// MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split.
// The applier stores nothing per record - contents are the backend's - and bumps Serial.
//
// P4a: `regions` IS THE CALL'S VARIABLE TAIL - MGPSubRegion[record.RegionCount] - and it is
// a trailing DEFAULTED parameter rather than a second entry point. The call has carried
// kVarTail since P2 (PipeCalls.def) and the texture half cannot be applied without it: the
// applier's pending-upload set is (UnionBox, RegionCount, Regions[]) and the verify lane's
// retain mode compares all three. The buffer half declares no regions, so P3a's one call
// site and every existing case are unchanged by the default.
//
// THE RETURN IS THE ACCEPTANCE SIGNAL D-D5 STEP 1 NAMES: true when the record was stored -
// the buffer half landed its range, or the texture half accumulated the shape onto the
// record - and false when it was refused. THE EMITTER MUST GATE ITS DIRTY-FLAG CLEAR ON IT
// ("only for levels whose record the applier ACCEPTED"), because the two refusal paths are
// otherwise invisible to it: a dead or stale handle is a counted no-op and a corrupt record
// is a Fatal that does NOT move RefusedResourceCalls, so in a shipped push build a refused
// upload and an accumulated one are indistinguishable from the call site. A client that
// clears on the strength of having emitted drops those texels for good.
//
// THE RESOURCE-TARGET HALF OF record.Target PICKS THE HALF. MGPSubData::Target is PACKED
// (ID-12): low byte = MGPipeResourceTarget, high byte = the cube-face upload target. The
// buffer half is the whole field being 0 - the encoding the emitter is held to, since a
// buffer has no upload target - and the texture half additionally requires the low byte to
// name a TEXTURE target: Buffer, Renderbuffer and anything at or above
// MGPipeResourceTarget::Count are Fatal{ProtocolCorruption} rather than an upload onto
// whatever object holds that slot in the texture slot space.
// P13 W4b: A WRITE THE SERVER PRODUCED, going through the ordinary resource_subdata gate - the
// pack-buffer landing of a readback (PipeApplier.cpp LandReadbackInBuffer) is the one producer.
// Its bytes are the server's own scratch and live for the call only, so a store that ALIASES
// the frontend's shadow (monolith's record arm) must not take them as its base: it lands them
// in the GL store and marks it GPU-written instead, which is how the frontend and every CPU
// reader learn of them (SyncGpuWrites / SplitHostBytesForCpuRead). A copying store takes them
// as it always has. Thread-local; scoped by the producer.
// MGPipeApplyResourceSubData (below): the buffer half takes its range from the record's box
// (MGPipeSetSubDataBufferRange); the texture half adds `regions` (MGPSubRegion[RegionCount])
// to the pending-upload set. record.Target packs the resource target (low byte) and the
// cube-face upload target; a buffer is the whole field 0, and a non-texture target on the
// texture half is Fatal{ProtocolCorruption}. Emitters clear dirty flags only on a true return.
// MGPipeServerOriginatedWriteScope marks a server-produced write (a readback landing in a pack
// buffer): its bytes live for the call only, so a store aliasing the frontend's shadow lands
// them in the GL store and marks it GPU-written instead of adopting them. Thread-local.
Bool MGPipeApplyingServerOriginatedWrite();
class MGPipeServerOriginatedWriteScope {
public:
@@ -1573,53 +1212,22 @@ namespace MobileGL::MG_Pipe {
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record);
// ---------------------------------------------------------------------------------
// P4a: the fifteen object and working-state entry points (D-A1, D-B1, D-J1)
// The object and working-state entry points
// ---------------------------------------------------------------------------------
//
// NOT ONE OF THEM DISPATCHES TO A BACKEND FUNCTION POINTER, and that is the single most
// important structural decision in P4a rather than an omission. Nothing in these families
// reaches the backend at GL-call time today - texture storage only marks a level dirty and
// Espryt allocates lazily at sync, texture params run from SyncTextureObjectToBackend at
// draw sync, renderbuffer storage is allocated inside SyncToBackend on a four-field cache,
// a sampler twin is created lazily from the program pass, and the framebuffer, unit sets
// and program are all resolved at PrepareForDraw. So every call below is either an OBJECT
// RECORD the applier stores or WORKING STATE the applier stores, and Espryt reads the
// applier at the sync points it already has, keyed on a server-owned Serial instead of a
// frontend version. MGPipeResourceOps is therefore UNCHANGED - nine members, same
// signatures - and P4a adds no backend op table and no op-table member at all.
//
// The consequence for the four resource entry points above: they BRANCH on
// record.Desc.Target. A buffer target dispatches into MGPipeResourceOps exactly as P3a
// wrote it; every other target stores and returns. The branch is one comparison against
// kMGPipeResourceTargetBuffer and it is where a mis-typed descriptor becomes visible.
//
// The bodies are in PipeApply.cpp. The signatures are what the client, the backend and the
// gates compile against, and the records above are what they write into.
// None calls into the backend: each stores an object record or working state, which the
// backend reads at the sync points it already has (texture and renderbuffer storage,
// framebuffer, unit sets and program are resolved at draw sync), keyed on server-owned
// Serials. The resource entry points above branch on the target: a buffer reaches the buffer
// members of MGPipeResourceOps, a texture only the optional Texture* members.
// set_framebuffer_state. Fully resolved - nothing in the record requires a lookup on the
// far side. ContentHash covers every field including Fbo and DrawBuffers[8], which is what
// makes a suppressed record provably mean "the draw-buffer array did not move" and
// therefore "the fragColor broadcast count did not move".
//
// `state.Target` NOW SAYS TWO THINGS AT ONCE (ID-19(b)), and the record always does the
// first of them:
//
// - THE RECORD IS ALWAYS WRITTEN, at FramebufferRecords[state.Fbo.Slot], whatever the
// target is. The table is keyed by the framebuffer HANDLE, so one framebuffer's record
// can never displace another's, and a slot whose object has been recycled is simply
// overwritten by its successor's record (D-I2: no wire lifetime, so nothing to retire).
// - Draw / Read / Both ADDITIONALLY set BoundFramebuffer[Draw] / [Read] / both.
// MGPipeFramebufferTarget::Named sets NEITHER: it is how a DSA entry point hands Espryt
// a framebuffer it is about to blit into or clear WITHOUT claiming it is bound.
//
// FramebufferSerial advances on every applied record, Named included.
//
// Two refusals, both Fatal{ProtocolCorruption} and neither counted (see RefusedObjectCalls:
// this entry point resolves nothing and can only ever fault): a target above Named, a
// draw-buffer entry outside the record's own Color[], a slot at or above
// kMGPipeMaxFramebufferSlots, and the NULL HANDLE - a record that named {0,0} would install
// itself where "nothing is bound" is read, and every emitter has a handle for every
// framebuffer it describes (kMGPipeDefaultFramebuffer {0,1} for the default one).
// set_framebuffer_state. Fully resolved; ContentHash covers every field including Fbo and
// DrawBuffers[8], so a suppressed record means the draw-buffer array, and with it the
// fragColor broadcast count, did not move. The record is always written at
// FramebufferRecords[state.Fbo.Slot], overwriting a recycled slot's predecessor; Draw, Read
// and Both also set BoundFramebuffer, Named (DSA blits and clears) sets neither.
// FramebufferSerial advances on every record. Fatal{ProtocolCorruption}, never counted: a
// target above Named, a draw buffer outside Color[], a slot at or above
// kMGPipeMaxFramebufferSlots, or the null handle (the default framebuffer is {0,1}).
void MGPipeApplySetFramebufferState(const MGPFramebufferState& state);
// create_sampler_state. `parameters` is the client's canonical SamplerParameters copy,
@@ -1687,23 +1295,13 @@ namespace MobileGL::MG_Pipe {
const MGPProgramStorageOverride* storageOverrides,
const char* const* storageOverrideNames);
// create_shader_state. THE ARTEFACTS TRAVEL BESIDE THE RECORD, by pointer: all seven of
// desc.Spirv[] and desc.Reflection are declared with Size 0 ("this record does not declare
// its blob"), which is what a monolith emission is, and the codec is NOT called - zero
// serialisation cost on the monolith path. A verify build serialises, deserialises and
// field-compares before storing, and a mismatch is Fatal{PipeVerifyDiffer, "program-archive"}.
// Splitting this record for a transport whose ring caps one record at half its capacity is
// P5's problem, not this entry point's.
//
// P5e (pg) ADDS THE FOURTH ARGUMENT AND DID NOT DEFAULT IT, for the reason PipeRoute.h
// gives about the three byte counts it also refused to default: a caller that HAS the
// archive and forgets to pass it would compile, and the failure would be a program twin
// reading the frontend's artefacts on the apply thread - silent, and exactly what this
// package exists to end. `archive` is null under monolith (the two companion pointers are
// the frontend's own and the handle arm is not taken there) and non-null under a
// transport, where `link` and `spirv` point INTO it: the record adopts it, so every later
// reflection read is of memory the server owns. A non-null archive whose Link/Spirv are
// not the two pointers passed beside it is a caller bug this entry point asserts on.
// create_shader_state. `link` and `spirv` are the program's artefacts and `archive` is the
// snapshot the record adopts (a null clears the previous one); desc's blob refs are not
// read. Under a transport the decoder passes its deserialised archive and pointers into it;
// in one process PipeRoute copies the frontend's artefacts into a new archive without the
// codec. The verify build round-trips the artefacts through the codec before storing, and a
// mismatch is Fatal{PipeVerifyDiffer, "program-archive"}. `archive` has no default so that a
// caller holding one cannot silently omit it.
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
const MG_State::GLState::LinkArtifacts* link,
const MG_State::GLState::SpirvArtifacts* spirv,
+22 -175
View File
@@ -6,157 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The single source of truth for the MGPipe call catalogue (plan B section 4.1 / 4.4 /
// appendix A). One line per call; seven generators consume this file
// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test
// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic.
//
// X(Name, PayloadStruct, Class, Flags, WaitClass)
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
// kScreen lands in struct MGPipeScreen, every other class in struct
// MGPipeContext (plan section 4.3).
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
// WaitClass : kWaitNone | kWaitApplied | kWaitReply | kWaitPresent
// P5e (MG_Remote/CONTRACT-P5E.md section 2.2). WHAT THE CLIENT DOES AFTER IT HAS
// PUBLISHED A RECORD OF THIS CALL, on a server that publishes kCapRunAheadApply.
// It is A VALUE AND NOT A NEW OP: the opcode order and the flag column are
// untouched, and gen_pipe.py emits the column beside kMGPipeCallFlags as
// MGPipeWaitClassFor(op) in generated/PipeWire.inc, so the emit table and the
// sink compute the barriered predicate from one table instead of two copies.
//
// kWaitReply the answer is not derivable and the client acts on it (R-5), so
// it blocks on the reply slot. EVERY kReplySlot ROW CARRIES IT and
// gen_pipe.py refuses a row where the two disagree - a reply-slot
// row that did not wait would be a waiter that hangs, and a waiting
// row with no slot would block on an answer nobody posts.
// resource_subdata is here because the row OWNS a slot; the buffer
// half's wantReply=false is a per-record ROUTE decision one level
// down (CONTRACT-P5E.md section 2.5), not a second wait class.
// kWaitPresent present, whose pacing is the present CREDIT taken BEFORE the
// encode rather than a wait after the publish (section 2.4).
// kWaitApplied the record's apply still reads a BARRIER_PULLED row or probes the
// client allocator, so the client parks in its own wait and the row
// keeps CONTRACT-P5C's semantics exactly: its pulls count into rsp
// and its probes are legal inside a scope. This column is the
// static half of MGPipeBarriered(op, payload, applierState); the
// other two escalations (an open XFB span, a draw with client
// vertex arrays) are payload-derived and live in that function.
// kWaitNone rule F holds for this row's sink: its apply names no client
// memory at all, so the client publishes and moves on.
//
// THE COLUMN IS INERT UNTIL THE INTEGRATION COMMIT publishes kCapRunAheadApply
// (MG_Backend/Init.cpp's kMGPipeP5eRunAheadReady): with the bit unpublished the
// client runs today's lockstep path and never reads the table.
// kNeedsAck on a call means records of this call MAY require an ack; a
// per-record predicate decides. resource_respecify carries it for
// glBufferStorage - a real synchronous allocation, and the only entry point
// allowed a synchronous ack - and MGPipeResourceRespecifyNeedsAck(desc)
// (MGPipeTypes.h) is what says so, which is why the same call still carries
// every glBufferData without acknowledging one.
//
// kHasBlob MEANS "THE PAYLOAD OWNS AN MGPBlobRef MEMBER", nothing weaker (P5
// R-13.1). It is not "this call carries bytes": three calls carry bytes with no
// MGPBlobRef at all and they are named in CONTRACT-P5.md table 1, not flagged
// here, because a decoder that trusts kHasBlob has to find a member to read.
// The flags are EXPORTED, once, as kMGPipeCallFlags[] in generated/PipeWire.inc
// (P5 R-13.4): before that table existed every consumer hard-coded its own copy,
// which is how the two carriers below came to disagree with their payloads.
//
// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire
// opcode is the 1-based position in this list, so reordering is a protocol break.
//
// ---------------------------------------------------------------------------------------
// COUNTS. MGP_CALL_LIST_DOCUMENTED_COUNT below is the authority; PipeCatalogueTest asserts
// that the expansion, the two generated tables and this number agree.
//
// class entries group (as the plan tabulates it)
// kScreen 14 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// appended server-side fence wait 1 and P5c's applier_reset 1 and
// P14 S1's bind_context 1 and the shared-image row 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
// + P5c rv's set_context_values + P5e's set_program_bindings
// kCtxObject 11 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus
// P5c's object_death 1 and get_texture_image_to_buffer 1
// kCtxVerb 20 3 context-reading transfer calls + the 10 commands, plus the five
// P5b-appended verbs (bind_shader_image, patch_parameter,
// bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md), plus
// delete_stream_output 1 and read_pixels_to_buffer 1
// total 85
//
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
// do not add up to a set of UNIQUE records and this file has to hold unique records:
// - "screen 14" tabulates the fence and query families together with the screen block.
// Section 4.3 assigns the query NAMESPACE to the context ("VAO / FBO / XFB object /
// query namespaces, the command stream, present"), so the six query calls carry
// kCtxQuery and live in MGPipeContext. Screen keeps 10 of the plan's (11 with the appended
// FenceWaitServer, below). The eight EGL lifecycle entry points stay virtual functions on
// pActiveBackendObject and are deliberately NOT calls here (section 4.4.1, last row).
// - "CSO 15" is create/bind/delete x 5 kinds. Two of those binds are ALSO named in the
// set_* catalogue as their array forms - bind_sampler_states and set_sampler_views
// (section 4.4.3) - and a call may only exist once, so they are emitted under
// kCtxState and the CSO group holds 13: create/delete x 5 plus the three remaining
// binds (render state, vertex elements, shader).
// - "transfer 12" enumerates 11 calls in section 4.4.4 plus appendix A
// (resource_subdata, buffer_subdata_resident, resource_flush_range, resource_readback,
// resource_copy_region, blit, clear, generate_mipmap, read_pixels, get_texture_image,
// resource_subdata_complete). Eleven is what is emitted; the twelfth is not named
// anywhere in the plan.
// - "about 74 items" in section 4.1 is the sum of those headline numbers, so it inherits
// the same double counting. 68 unique records was the honest total of the plan's own
// catalogue.
// - Three LIVE GLFunctionsTable entries had no carrier in it at all: GetGpuTimestampNs
// (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp
// (glQueryCounter, a one-shot stamp rather than a begin/end pair) and WaitSync (the
// GPU-side wait, which FenceWait's client-side wait does not express). They are
// QueryTimestamp, QueryCounter and FenceWaitServer, APPENDED at the end of the list -
// not slotted into their groups - because the wire opcode is the position, so a record
// that arrives late goes last. 71 unique records.
// - P5b appended FIVE more after them (opcodes 72..76), for the same reason and by the same
// rule: five GLFunctionsTable verbs the class-C census measured as first blockers - the
// image bind, the patch parameter, the XFB object bind, the storage-block rebinding and
// the framebuffer-to-texture copy - for which no existing row could carry the call.
// 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the
// reflection archive" is CORRECTED by the row: the archive carries the bindings the
// program LINKED with, and glShaderStorageBlockBinding moves one AFTER link.
// - P5c (MG_Remote/CONTRACT-P5C.md §5.1/§5.2) appended TWO more after those (opcodes 77..78),
// same rule: applier_reset, the make-current edge's server-side reset that used to be a
// GL-thread direct call into server memory, and object_death, the framebuffer family's
// FIRST wire delete opcode and the handle-keyed replacement for the death-notice mailbox.
// Neither has an MGPipeApply* entry point and neither gains one; both reach WireVerbSink.
// - P5c rv (§5.3) appended ONE more (opcode 79): set_context_values, the residual-value
// record that retires the value-class BARRIER_PULLED rows. Unlike the two control records
// it IS an ordinary set_* row: MGPipeApplySetContextValues writes gPipeInputs, the row is
// routed through gMGPipeContext like its seventeen siblings, and under monolith it has no
// producer at all - the fields keep coming through the residual fill, byte for byte as
// before (G1). 79 unique records.
// - P5e (MG_Remote/CONTRACT-P5E.md section 1) appended ONE more (opcode 80):
// set_program_bindings, the three POST-LINK mutable reflection fields - the uniform-block
// bindings, the sampler uniform units and the name-keyed storage-block override map. They
// live inside the LinkArtifacts the reflection archive snapshots, and all three move AFTER
// the link, so no existing row can carry them: create_shader_state describes the link and
// this one describes what happened to it since. It lands with a NULL ROUTE and a sink that
// refuses it by name until package pg wires the emitter, exactly as set_shader_buffers has
// sat catalogued-and-dead since P4a. 80 unique records.
// - delete_stream_output (opcode 81) took the XFB object's deletion across; 81 records.
// - P9 (MG_Remote/CONTRACT-P9.md §1) appended TWO more (opcodes 82..83):
// read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two
// readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach
// WireVerbSink only and have no monolith producer. 83 unique records.
// - P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended ONE more (opcode 84):
// bind_context, the in-band record that carries the client's CURRENT context token. It has
// the applier_reset shape - a kScreen control record, no MGPipeApply* entry point, no
// monolith producer - because what it moves is session attribution, not context state. The
// design's per-record ContextSlot was NOT taken here: the token is announced at the edge
// (a context change) instead of stamped on every verb, which costs one record per
// make-current and leaves the 71 context-family payloads untouched. 84 unique records.
// shared_image (85), the server-allocated colour buffers exported as dma-bufs. 85 unique
// records.
// ---------------------------------------------------------------------------------------
// The MGPipe call catalogue (ARCHITECTURE.md §3), one X(Name, PayloadStruct, Class, Flags,
// WaitClass) per call, read by scripts/gen_pipe.py into MG_Pipe/generated/*.inc. The wire
// opcode is the 1-based row position: append new calls at the end, keep retired rows in place.
// MGP_CALL_LIST_DOCUMENTED_COUNT is the row count, pinned by MG_Test/Pipe/PipeCatalogueTest.cpp.
// Class: kScreen rows go to MGPipeScreen, all others to MGPipeContext. kHasBlob means the
// payload owns an MGPBlobRef member; kNeedsAck means a per-record predicate may ask for an ack.
// WaitClass (MGPipeWaitClass) is what the client does after publishing under run-ahead;
// kWaitReply goes with kReplySlot, which gen_pipe.py enforces (ARCHITECTURE.md §3.2.1).
#define MGP_CALL_LIST_DOCUMENTED_COUNT 85
@@ -182,7 +39,7 @@
/* as resource_subdata records IMMEDIATELY AFTER this one. Reuses a path that is */ \
/* already chunked (MGPipeForEachSubDataRecordRange) and already acceptance-gated; the */ \
/* cost is one extra record. NOTE the SECOND uncarried companion on this call, the */ \
/* MGPRespecifiedLevel* scope (PipeApply.h:792) - it is NOT bytes and NOT covered by */ \
/* MGPRespecifiedLevel* scope (PipeApply.h) - it is NOT bytes and NOT covered by */ \
/* this ruling; table 1 row 19 gives it the two pad fields of MGPResourceDesc. */ \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck|kReplySlot, kWaitReply) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone, kWaitNone) \
@@ -294,7 +151,7 @@
/* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \
/* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \
X(FenceWaitServer, MGPFenceWait, kScreen, kNone, kWaitNone) \
/* ---- APPENDED BY P5b (MG_Remote/CONTRACT-P5B.md): five GLFunctionsTable verbs the ---- */ \
/* ---- APPENDED BY P5b (docs/archive/contracts/CONTRACT-P5B.md): five GLFunctionsTable verbs the ---- */ \
/* ---- class-C census measured as first blockers and that no existing row could carry. ---- */ \
/* ---- Opcodes 72..76. Every one reaches WireVerbSink, none has an MGPipeApply* entry ---- */ \
/* ---- point, and under monolith none has a producer - the backend slot is called ---- */ \
@@ -313,7 +170,7 @@
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
/* which resource_copy_region (resource -> resource) cannot express. */ \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone, kWaitNone) \
/* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \
/* ---- APPENDED BY P5c (docs/archive/contracts/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \
/* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \
/* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \
/* ---- MGPipeApplierReset() call and the death notice's mailbox hop, byte for byte as ---- */ \
@@ -340,7 +197,7 @@
/* ---- (PipeFill.cpp gates both the emission and the residual-fill skip on it): under ---- */ \
/* ---- monolith there is no producer and the fields keep being pulled, byte for byte (G1). ---- */ \
X(SetContextValues, MGPContextValues, kCtxState, kNone, kWaitNone) \
/* ---- P5e (MG_Remote/CONTRACT-P5E.md §1), opcode 80: the post-link binding record. ------ */ \
/* ---- P5e (docs/archive/contracts/CONTRACT-P5E.md §1), opcode 80: the post-link binding record. ------ */ \
/* ---- THREE POST-LINK MUTABLE REFLECTION FIELDS LIVE INSIDE THE FRONTEND'S LinkArtifacts ---- */ \
/* ---- and therefore inside the archive create_shader_state carries - but glUniformBlock- ---- */ \
/* ---- Binding, glUniform1i on a sampler and glShaderStorageBlockBinding all move one ---- */ \
@@ -355,7 +212,7 @@
X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone) \
/* Deletion carries the GL argument and its never-reused capture identity. */ \
X(DeleteStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitNone) \
/* ---- APPENDED BY P9 (MG_Remote/CONTRACT-P9.md §1), opcodes 82..83: the pack-buffer ---- */ \
/* ---- APPENDED BY P9 (docs/archive/contracts/CONTRACT-P9.md §1), opcodes 82..83: the pack-buffer ---- */ \
/* ---- halves of read_pixels and get_texture_image. The reply-slot rows above keep every ---- */ \
/* ---- read into client memory; a read whose destination is a bound PIXEL_PACK_BUFFER ---- */ \
/* ---- crosses as one of these instead, names the buffer by handle, and is answered by ---- */ \
@@ -377,7 +234,7 @@
/* ---- kWaitNone: the apply reads no client memory and the record's placement in the ring ---- */ \
/* ---- is the whole of its ordering. ---- */ \
X(BindContext, MGPBindContext, kScreen, kNone, kWaitNone) \
/* ---- APPENDED FOR SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md), ---- */ \
/* ---- APPENDED FOR SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md), ---- */ \
/* ---- opcode 85: allocate / import / release / present / attach of a server-allocated ---- */ \
/* ---- colour buffer exported as a dma-buf. kScreen: the image table is the server's, and ---- */ \
/* ---- present / attach act on the session's current context the way bind_context does. ---- */ \
@@ -386,21 +243,11 @@
X(SharedImage, MGPSharedImageOp, kScreen, kReplySlot, kWaitReply)
// clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry -
// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each,
// the only indexed pnames the device rather than the frontend answers - live in MGPCaps
// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled
// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from
// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names
// frontend state and is answered before any table is consulted.
// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact
// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not
// a backend answer at all; nothing a backend knows about a program crosses this way.
// - ShaderStorageBlockBinding WAS listed here as "folded into MGPProgramDesc's reflection
// archive". P5b's set_storage_block_binding row (opcode 75) replaces that: the archive
// carries the bindings as LINKED, and the call moves one after link, which the backend
// applies to an already-built driver program (DirectGLES.cpp ShaderStorageBlockBinding).
// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a
// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views
// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3).
// Not in the catalogue (ARCHITECTURE.md §3.2):
// - GetIntegeri_v / GetInteger64i_v: the only device-answered indexed pnames, the compute
// work-group count and size limits, travel in MGPCaps (DynamicBackendParameters); every
// other indexed pname is frontend state.
// - GetProgramiv: GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact
// (ProgramObject::GetComputeLocalSize).
// - set_pixel_unpack_state, compressed-format concepts, pipe_transfer, and a stage dimension
// on set_sampler_views (the texture unit space is merged across stages).
+3 -3
View File
@@ -145,7 +145,7 @@
#define MGP_FIELDS_MGPStreamOutputTargets(F) \
F(Count) F(Generation) F(ContentHash)
// P5e (MG_Remote/CONTRACT-P5E.md §1): set_program_bindings and its two tail element types. The
// P5e (docs/archive/contracts/CONTRACT-P5E.md §1): set_program_bindings and its two tail element types. The
// third tail's element embeds an MGHostSpan, which has a list of its own below, so the
// comparator sees the NAME SPAN's four members rather than memcmp'ing a struct with a pointer
// and a pad word in it - the exact false-difference MGPSamplerDesc's blob used to produce.
@@ -269,7 +269,7 @@
#define MGP_FIELDS_MGPSurfaceInfo(F) \
F(Width) F(Height) F(InternalFormat) F(Samples) F(Layers) F(IsDefault)
// ---- P5b's five appended rows (MG_Remote/CONTRACT-P5B.md). Pad-named members excluded, as
// ---- P5b's five appended rows (docs/archive/contracts/CONTRACT-P5B.md). Pad-named members excluded, as
// everywhere; MGPStorageBlockBinding's Name is an MGPBlobRef and recurses into that list.
#define MGP_FIELDS_MGPImageBind(F) \
@@ -288,7 +288,7 @@
F(Dst) F(Target) F(Level) F(InternalFormat) F(X) F(Y) F(Width) F(Height) F(XOffset) F(YOffset) \
F(SubImage)
// ---- P5c's two appended rows (MG_Remote/CONTRACT-P5C.md §5). object_death's payload IS
// ---- P5c's two appended rows (docs/archive/contracts/CONTRACT-P5C.md §5). object_death's payload IS
// MGPHandleOnly, whose list already exists above; applier_reset's is one field.
#define MGP_FIELDS_MGPApplierReset(F) \
+40 -144
View File
@@ -9,23 +9,13 @@
#pragma once
#ifndef MOBILEGL_MG_PIPE_MUTATION_H // belt and braces: reachable as <MG_Pipe/..> and <..>
#define MOBILEGL_MG_PIPE_MUTATION_H
// Push-on-mutation (P1 lane finding F2). MGP_FILL copies a verb's may-read set out of the
// live GLContext at the verb boundary; the backend then reads that copy for the whole verb.
// A backend that WRITES a frontend object inside its own verb - Magma synthesising a
// fallback texture for an unbound sampler, materialising a queued clear, or overriding a
// sampler's filter - moves a value the boundary already copied, and every read after that
// point sees a block that no longer equals the live context. That is a real divergence, not
// a harness artefact: the pull build reads the moved value and the push build does not.
//
// The frontend mutator that moves such a value spells MGP_NOTE_MUTATION(Field) right where
// it moves it. The notice refreshes that ONE field in the pushed block when the field
// belongs to the verb currently in flight, so "the pushed block equals the live context at
// every read" stays literally true and the push build keeps pull semantics. It refreshes
// the value only and never the poison stamp, so a withheld stamp (MOBILEGL_PIPE_POISON_OMIT,
// negative control B) stays withheld.
//
// In the pull build the macro is ((void)0) and this header includes nothing, so the pull
// build is byte-identical to a tree without it.
// Frontend mutation notices. The verb fill (MG_Impl/Pipe/PipeFill.cpp) copies a verb's may-read
// set out of the live GLContext at the verb boundary, and the backend reads that copy for the
// whole verb. A frontend value written inside the verb (Magma synthesising a fallback texture,
// materialising a queued clear, overriding a sampler's filter) would leave the copy stale, so
// the mutator calls MGP_NOTE_MUTATION(Field) where it moves the value. The notice refreshes
// that one field when it belongs to the verb in flight. It never touches the poison stamp, so a
// stamp withheld by MOBILEGL_PIPE_POISON_OMIT stays withheld.
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext).
@@ -33,25 +23,14 @@ namespace MobileGL::MG_Pipe {
// class's may-read mask; a forwarded (sticky) field has no storage and is never copied.
void MGPipeNoteFrontendMutation(MGPipeInputField field);
// ---- the aggregate generations (P2 brief D4, ARCHITECTURE.md 5.2) ----
// ---- aggregate generations (ARCHITECTURE.md §5.2) ----
//
// MGP_NOTE_MUTATION answers "a backend moved a frontend value INSIDE its own verb".
// MGP_NOTE_AGGREGATE answers a different question, which is why it is a second macro
// and not an overload: "did ANY object of this class move since the last time the
// tracker looked", collapsed onto one monotonic Uint64 per class so a per-verb dirty
// walk is a handful of compares rather than a scan over 32 attributes, 16 attachments,
// 32 texture units and 84 binding points.
//
// The counters are members of the owning MG_State container, all guarded by
// MOBILEGL_PIPE_PUSH so the pull build's state objects do not change size (G1). The
// bump points sit on OBJECTS, which have no back-pointer to their state, so the macro
// goes through a free function that finds the live GLContext - the same shape, and for
// the same reason, as MGP_NOTE_MUTATION (MG_Impl/Pipe/PipeFill.cpp). It costs a global
// load on a path that has just written object state.
//
// Monotonic and never reset: the tracker widens and compares, it never subtracts.
// Over-firing is free (one extra re-push); under-firing renders stale, which is why
// every counter here is deliberately COARSER than the state it guards.
// MGP_NOTE_AGGREGATE answers "did any object of this class move since the tracker last
// looked", one monotonic Uint64 per class, so the per-verb dirty walk is a few compares
// instead of a scan over attributes, attachments, texture units and binding points. The
// counters live in the owning MG_State container; objects have no back-pointer to it, so the
// note finds the live GLContext through a free function. Never reset. Over-firing costs one
// re-push and under-firing renders stale, so each counter is coarser than the state it guards.
enum class MGPipeAggregate : Uint32 {
// VertexArrayState: any VAO attribute format / buffer / enable moved.
VaoAttribute = 0,
@@ -153,40 +132,14 @@ namespace MobileGL::MG_Pipe {
// is asked rather than assumed.
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId);
// ---- P4a: ONE CLIENT-SIDE DEATH HELPER PER KIND P4a MINTS (brief D-I1) ----
// ---- client-side death helpers, one per kind the client mints ----
//
// BACKEND-NEUTRAL FROM DAY ONE, and this is the P3a final-review lesson taken forward
// rather than repeated. Before it, the only thing that ever returned a VertexElementsCso
// slot was DirectGLES' StateObjectDeathOps table; under a backend that installs none -
// DirectVulkan/Magma, which keeps its own age-reclaimed identity table on purpose - every
// VAO ever created held its slot and its applier record for the life of the process, and
// past 65536 slots every create became a permanent Fatal{ProtocolCorruption}. P4a mints
// SIX kinds, so the rule is stated once and obeyed six times: whatever mints a handle owns
// the death of that handle, the client mints all six, and a backend death notice is a
// redundant SECOND path that must be idempotent - which it is, because it resolves through
// the same lifetimeId -> slot map these free, and MGPipeSlotAllocator::Free refuses a slot
// that is not live at that generation.
//
// THE ORDER INSIDE EACH IS FIXED AND IS NOT A PACKAGE'S CHOICE:
// 1. emit the wire delete FIRST - it drops the applier's record while the record still
// exists, so a recycled slot cannot inherit a field;
// 2. raise NotifyStateObjectDestroyed SECOND - it resolves the handle through the
// allocator, and a backend told after the Free could no longer find its twin, which
// moves the leak from the client to the driver object;
// 3. free the slot LAST, and a double free on a stale generation is a proven no-op
// because Free bumps no generation (the bump rides the next handout).
//
// ALL SIX TAKE THE LIFETIME ID rather than the object, for MGPipeEmitVertexElementsDestroy
// AndFree's reason: they run from a destructor, where the last SharedPtr has already
// dropped, and the lifetime id is what the slot allocator resolves the handle from. It is
// also what keeps this header a declaration-only coupling - no frontend class needs
// forward-declaring for any of them.
//
// Each returns whether its wire delete actually went out, which is the LATCH taken at the
// object's create and not a second reading of the subsystem predicate: an object born
// while a subsystem bit was clear and destroyed after it was set would otherwise free its
// slot with the applier's record still Live, on a slot about to be handed out again. The
// legacy path runs only when the answer is false.
// Whoever mints a handle owns its death, so these are backend-neutral; a backend death
// notice is a redundant, idempotent second path (MGPipeSlotAllocator::Free refuses a slot not
// live at that generation). Fixed order: emit the wire delete while the applier record
// exists, then NotifyStateObjectDestroyed while the allocator still resolves the handle, then
// free the slot. All take the lifetime id because they run from destructors. Each returns
// whether its delete went out, decided by the publication latch taken at create.
// ResourceDestroy, and then the SamplerViewCso minted off this same lifetime id (P4a
// D-F2: one sampler view per ITextureObject). Called from TextureObjectBase's VIRTUAL
@@ -214,33 +167,14 @@ namespace MobileGL::MG_Pipe {
Bool MGPipeEmitShaderCsoDestroyAndFree(Uint64 lifetimeId);
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- THE DEFERRED DESTROY QUEUE (the MOBILEGL_IPC_BATCH_WAITS class) ----
// ---- deferred destroy queue ----
//
// Every helper above speaks on the CLIENT's behalf: it resolves the handle through the
// client allocator, emits a client wire record and frees a client slot - so it may only
// ever run on the GL thread. The per-record barrier made that free: the client waited
// out every apply, so a server-side SharedPtr (an applier's endpoint local, a
// gPipeInputs entry) could never be an object's last owner. Batched waits remove the
// guarantee - the client races ahead, drops its own references at teardown, and the
// apply thread's local CopyImageEndpoint becomes a texture's last owner, whose
// destructor then touches the client allocator from the server role
// (Fatal{RoleViolation, "MGPipeSlots"}). The helpers therefore START with
// MGPipeDeferDestroyAndFreeIfOnApplyThread: on the apply thread it enqueues the
// (kind, lifetime id) pair - the only thing a death announcement needs - and the GL
// thread replays the helper at the next verb hook (EmitTables' BeforeDrawVerb /
// BeforeReadOnlyVerb) through MGPipeDrainDeferredDestroys. A delayed free is safe:
// slots are plentiful, reuse is delayed rather than corrupted, and the replay runs the
// helper itself, so every helper's own three-step order is untouched.
// P5e (ra), CONTRACT-P5E §2.7 / ruling 13 (ID-91): THE QUEUE STAYS LIVE, AND AN
// UNBARRIERED ENQUEUE IS A FINDING. After the family packages land, an unbarriered apply
// holds no frontend SharedPtr at all - every object is resolved from a handle - so the
// only apply thread that can still be a last owner is one inside a BARRIERED record (a
// fill's O-class rows, XFB's pinned targets). An enqueue from an unbarriered record
// therefore means some site still pins a frontend object across a record, which is a
// migration this phase believes it finished: it logs once with the kind, and is Fatal
// under MOBILEGL_IPC_STRICT_ERRORS so the strict lane owns the red. The queue itself is
// NOT removed - it is the belt that keeps the barriered case from corrupting the
// allocator while the finding is being read.
// The helpers above act for the client, so they run only on the GL thread. When the apply
// thread drops an object's last reference, the helper its destructor calls starts with
// MGPipeDeferDestroyAndFreeIfOnApplyThread, which queues (kind, lifetime id); the GL thread
// replays the helper at its next verb hook (MGPipeDrainDeferredDestroys), so only slot reuse
// is delayed. Only a barriered record should hold such a reference: an enqueue from an
// unbarriered one logs once, and is Fatal under MOBILEGL_IPC_STRICT_ERRORS.
Bool MGPipeDeferDestroyAndFreeIfOnApplyThread(MGPipeKind kind, Uint64 lifetimeId);
void MGPipeDrainDeferredDestroys();
@@ -305,43 +239,13 @@ namespace MobileGL::MG_Pipe {
void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer);
// ================================================================================
// P4a: THE BIRTH HALF, one hook per client path MG_State owns (D-C .. D-I)
// Birth hooks, one per client path MG_State owns
// ================================================================================
//
// The death helpers above are half a lifetime. The other half is emitted from MG_State
// too - a texture's create from its constructor, a renderbuffer's respecify from its
// storage mutators, a texture's params from glTexParameter*, a sampler CSO from the
// sampler object, a shader CSO from the program - because that is where the event
// happens, exactly as P3a's buffer family emits from BufferObject's own dispatchers
// (ARCHITECTURE.md 5.1 names those as the ONE exception to push-at-validate). Only the
// texture sub-data DRAIN runs at the validate point, and even it is fed from here: the
// drain list is appended on a level's first dirty mark.
//
// WHY THEY ARE DECLARED HERE. This header is the one door MG_State has into the client
// (check_include_closure.py's mutation-header probe pins it: reaching
// MG_Impl/Pipe/*Emit.h from a frontend mutator would pull the client's emitters into the
// state machine that calls them). So a hook a frontend mutator calls is DECLARED here and
// DEFINED in MG_Impl/Pipe/PipeFill.cpp, which is package A's for the whole phase - the
// same "declaration here, definition there" split MGPipeMintResourceHandle and
// MGPipeEmitResourceCreate use, and the reason no file is touched twice.
//
// WHAT EACH BODY DOES, and the division is fixed:
// * PipeFill.cpp owns the GATE - the subsystem bit in MOBILEGL_PIPE_PUSH *and* the
// family's own kMGPipeWired*Subsystem constant, the same pair the validate point's
// `wants()` applies to every emission - and the four MINTS, which are pure allocator
// work and need no family knowledge;
// * the FAMILY EMITTER (MG_Impl/Pipe/<Family>Emit.h, owned by package B or C) owns the
// payload build, the handle rule for its own kind and the PUBLICATION LATCH below.
// PipeFill.cpp forwards to it through an entry point that is compiled only while that
// family's wired constant is non-zero, so this tree links against the STUB emitters
// and against the finished ones with no edit to PipeFill.cpp - and a family that sets
// its constant without providing the entry point is a COMPILE ERROR in its own commit
// rather than a surprise at the merge. The entry point each hook forwards to is named
// beside it and spelled out in PipeFill.cpp's contract block.
//
// NOTHING CALLS ANY OF THEM AT THE CONTRACT COMMIT. B and C add the call sites in the
// five MG_State directories C.7 gives them, in the SAME commit that gives the emitter its
// body - by EDITING an existing constructor/mutator body, never by adding one (G1).
// Emitted from MG_State where the event happens, like the buffer family (ARCHITECTURE.md
// §5.1). Declared here because this header is MG_State's one door into the client
// (check_include_closure.py); PipeFill.cpp owns the family gate and the mints and forwards to
// the family emitter (MG_Impl/Pipe/<Family>Emit.h), which owns the payload and the latch.
// ---- the publication latch (D-I1), and it is the ONE answer both halves read ----
//
@@ -381,22 +285,14 @@ namespace MobileGL::MG_Pipe {
// door into it, and it is not a frontend construction event.
void MGPipeMintShaderCsoHandle(MG_State::GLState::ProgramObject& program);
// ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h, package B ----
// ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h ----
//
// resource_create from ITextureObject's constructor and RenderbufferObject's;
// resource_respecify from every storage-defining entry point, including
// RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples}, which publish
// nothing at all today (D-D2); set_texture_params from the parameter mutators, which is
// where the READ-attachment-only gap D-E3 closes.
//
// Entry points MGPipeTextureEmitter must provide, all taking the frontend object by
// reference and returning void:
// EmitResourceCreate(ITextureObject&)
// EmitResourceRespecify(ITextureObject&, MGPipeTextureRespecifyScope, Uint32 uploadTarget,
// Uint32 level)
// EmitTextureParams(ITextureObject&)
// NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level)
// EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&)
// resource_create from the ITextureObject and RenderbufferObject constructors,
// resource_respecify from every storage-defining entry point (renderbuffer storage included),
// set_texture_params from the parameter mutators. MGPipeTextureEmitter provides, as void:
// EmitResourceCreate, EmitResourceRespecify(.., scope, uploadTarget, level),
// EmitTextureParams, NoteLevelDirty(storageOwner, uploadTarget, level),
// EmitRenderbufferCreate, EmitRenderbufferRespecify. Each takes the object by reference.
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
// WHICH STORAGE A TEXTURE RESPECIFY REPLACES (P4a final review C-1). The applier scopes
+8 -21
View File
@@ -6,27 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The MONOLITH arm of R-17's routing: thirty-eight adapters that unpack a generated table
// row's parameters and call the MGPipeApply* entry point the call site used to call directly.
// Owner: package c1. See PipeRoute.h for why the arm exists and what R-17 actually cost.
//
// G2 IS THE WHOLE SPECIFICATION OF THIS FILE. Under monolith transport the thunks must reach
// EXACTLY the code they reach today - so every adapter below is a parameter shuffle and
// nothing else. There is no branch, no cache, no early return and no logging on any of them,
// because each of those is a way for `MOBILEGL_TRANSPORT=monolith` to stop being the control
// arm that the split arm is measured against. The one thing an adapter may do beyond calling
// through is POST A REPLY, and only the five rows whose call site consumes a value do that.
//
// WHO CALLS THE INSTALL, AND WHY IT IS NOT IN THIS FILE. `gMGPipeScreen` and `gMGPipeContext`
// are inline variables with CONSTANT initialisation, so they are zero - every entry null,
// which is the pre-migration state - before any dynamic initialiser runs. The installer is
// driven from an inline variable in PipeRoute.h rather than from a static initialiser here,
// and the comment beside `detail::gMonolithTablesInstalled` says why: a static initialiser in
// THIS file is only in the program if THIS object file is in the link, and over a static
// archive it was not. That makes "the tables are installed" an invariant rather than a step
// someone can forget, and it is why no MGP_* thunk needs a null check - which matters, because
// a null check on a table slot is precisely the shape CONTRACT-P5 §7 spends the 41 call sites
// killing.
// The in-process arm of the call routing: adapters that unpack a generated table row's
// parameters and call the matching MGPipeApply* entry point. An adapter is a parameter shuffle,
// with no cache, logging or logic of its own, so the in-process arm stays the reference the split
// arm is measured against. Three kinds of row do one thing more: rows whose call site waits for
// an answer post a reply, the create_shader_state escape builds the record's archive, and the
// retired set_residual_value_state row aborts. The tables are installed before main by
// detail::gMonolithTablesInstalled in PipeRoute.h, not by a static initialiser here (a
// static-archive link can drop this object), so no MGP_* thunk needs a null check.
#include "PipeRoute.h"
+38 -225
View File
@@ -6,80 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// THE CLIENT -> WIRE ROUTING OF THE 38 MGPipeApply* ENTRY POINTS (P5, integrator ruling R-17;
// 38 since P5c rv added set_context_values, CONTRACT-P5C.md section 5.3).
// Owner: package c1.
//
// WHAT WAS MISSING. `gMGPipeWireRecordApply` is the DECODE hook and it has existed since w1:
// a record arrives, `PipeWireCodec` picks it apart and calls `MGPipeApply<Name>`. There was no
// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply<Name>` DIRECTLY, so under
// `MOBILEGL_TRANSPORT=inproc` every resource, CSO, texture and program record still executed
// synchronously on the GL thread against a context v1 is moving to the apply thread - where
// all sixteen `IsBackendContextCurrentOnThisThread()` and sixteen `CanTouchGLNow()` guards
// answer false. `ClearThenReadPixels` might have survived that; `TriangleScenario` needs a VBO
// and a program and could not.
//
// The two generated tables (`gMGPipeScreen`, `gMGPipeContext`, `generated/PipeTables.inc`) are
// the boundary this repository already had, and they were never installed - every entry null,
// which `MGPipe.h` correctly calls "precisely the pre-migration state". R-17 installs them.
// This header is the frontend-facing half: the names `MG_Impl/Pipe` calls, and the mechanism
// that gets a kReplySlot row's answer back to its call site.
//
// ---------------------------------------------------------------------------------------
// THE SIZE OF R-17, MEASURED RATHER THAN ESTIMATED
// ---------------------------------------------------------------------------------------
//
// R-17 costed this as "37 thin client emitters over EmitAndWait". The emitters are thin. The
// ROUTING was not, and the reason is that the generated table signature is the P2-era monolith
// interface: `void (*Name)(const Payload* payload, ...)`, async-with-handle, no return value,
// and NO COMPANION POINTER. Of the 37 entry points, as the tables stood:
//
// 24 the row could carry every argument the call site passes today
// 13 it could not
//
// The thirteen split three ways, and only one of the three is a generator question:
//
// (a) NINE carry a blob companion - `chunkBytes`, `blobBytes`, `parameters`, `bytes`,
// `initialBytes` - which is CONTRACT-P5 table 1's "companion pointer today" column. A
// kVarTail row already gains `const void* varTail, Uint32 varTailCount`; a kHasBlob row
// gained nothing. THE GENERATOR CHANGE IS THAT ONE ASYMMETRY, and nothing else: five
// lines in `Call.Signature` giving a kHasBlob row `const void* blobBytes, Uint64
// blobByteCount`. Ten rows gained the pair (the nine plus `GetCaps`, which has no applier
// entry point). `--check` up to date, `--self-test` 9/9 trips.
//
// (b) FOUR return a value the row cannot: `ResourceCreate`, `ResourceRespecify`,
// `ResourceSubData` and `SetTextureParams` return Bool and `MapPersistent` returns void*.
// These are exactly CONTRACT-P5 §2's reply-slot rows, and R-5 forbids re-deriving any of
// them on the client. The answer therefore has to come back THROUGH the reply slot, which
// is what `MGPipeTakeReply*` below is, and it is the part of R-17 that no amount of
// "thin emitter" covers: `MGPReplySlot` is `{Uint64 Id;}` - an IDENTIFIER, not a value -
// so a reader had to exist for the identifier to be worth minting.
//
// (c) FOUR CANNOT GO THROUGH A GENERATED ROW AT ALL, and each one is a contract ruling
// rather than an oversight. They are the ESCAPES below. Saying "four" out loud is the
// honest answer to "tell me if it is bigger than it looks": it is, by four rows and one
// reply-reading mechanism.
//
// ---------------------------------------------------------------------------------------
// THE REPLY MAILBOX, AND WHY ONE ENTRY IS THE RIGHT DEPTH
// ---------------------------------------------------------------------------------------
//
// `MGPReplySlot` carries an id; the answer lands beside it. The mailbox is ONE ENTRY DEEP and
// thread-local, and that is not a simplification - it is R-1's verb barrier stated as a data
// structure. While the barrier holds, the in-flight depth is exactly one (`ReplySlot.h`:
// "under R-1's verb barrier the in-flight depth is one"), so a second posting before the first
// is taken is not a capacity problem, it is a barrier that has stopped working. It Fatals.
//
// THE MAILBOX HAS NO DEFAULT ANSWER AND THIS IS THE WHOLE POINT. `MGPipeTakeReplyBool` on a
// slot nothing posted to is `Fatal{ReplyMissing, "<row>"}`, not `false` and emphatically not
// `true`. "Always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between, and
// "always refuse" is an emitter that re-sends for ever. A row that forgets to answer is a bug
// that must be impossible to ship, so it is impossible to READ.
//
// WHEN THE BARRIER RETIRES (R-1 opens family by family) this becomes a real slot pool keyed on
// seq, which is what `Transport::ReplySlot` already is on the wire side. The mailbox is the
// frontend-side handle onto it and nothing else; it is deliberately NOT a second id space,
// because the split arm stamps the record's own seq into `MGPReplySlot::Id` (R-3).
// Client-side routing of the MGPipe calls. MG_Impl/Pipe emitters call MGPipeRoute<Name>, never
// MGPipeApply<Name>, so every record goes through the installed tables (gMGPipeScreen,
// gMGPipeContext, gMGPipeRouteEscapes): the monolith adapters or the client wire emitters.
// A kReplySlot answer comes back through a thread-local mailbox one entry deep: each route takes
// its answer before returning, so a second post before the take is Fatal{ReplyOverrun}. Taking
// an answer nothing posted is Fatal{ReplyMissing}, never a default: assuming acceptance loses
// uploads, assuming refusal makes the emitter resend for ever. Under split the slot id is the
// record's own sequence number, so the mailbox adds no second id space.
#pragma once
#include <Includes.h>
@@ -121,45 +55,13 @@ namespace MobileGL::MG_Pipe {
Uint64 MGPipeRepliesDeclined();
// ---------------------------------------------------------------------------------
// The escapes: the four rows no generated signature can express
// The escapes: the rows no generated signature can express
// ---------------------------------------------------------------------------------
//
// A hand-written third table, installed and uninstalled by the same two functions as the
// generated pair, so there is ONE mechanism and not two. Each row's signature is the
// applier's own, because that is the shape the monolith arm has to reproduce byte for
// byte (G2), and each row is here for a ruling that is written down:
//
// ResourceRespecify `initialBytes` has NO kHasBlob and R-13.3 forbids giving it one
// ("initialBytes is always nullptr under split; initial content
// arrives as ResourceSubData records immediately after this one").
// So the row is correct to omit it AND monolith must still pass it.
// ResourceFlushRange `bytes` likewise, by R-13.2, and for a sharper reason: a blobref
// here would be "a second, forgeable way to say the same thing".
// MapPersistent `size` and `seedBytes` have no carrier at all - `MGPHandleOnly` is
// {Handle, Kind} - and the call returns void*. R-6 makes the split
// answer a constant DECLINE, so the carrier is not needed; the
// monolith arm needs both arguments.
// CreateShaderState SEVEN blobrefs (`Spirv[6]` + `Reflection`) and TWO typed frontend
// pointers. One `blobBytes` pair cannot express seven runs, and
// serialising under monolith to un-serialise in the adapter would put
// `EncodeProgramArtifacts` on the monolith path, which `PipeApply.h`
// explicitly promises it is not ("zero serialisation cost on the
// monolith path").
// SetProgramBindings THREE tails in three index spaces plus a PARALLEL NAME ARRAY
// (P5e, MG_Remote/CONTRACT-P5E.md §1/§5.5). One `varTail` +
// `varTailCount` pair cannot express three runs at all, and the
// fourth argument is not a run: the storage-override key is a
// `MGHostSpan` on the wire and a plain `const char*` under monolith,
// so the applier takes the resolved names beside the tail rather
// than resolving segments itself (rule C - a span retires with the
// record that named it, and the applier copies the bytes it keeps).
// The generated `gMGPipeContext.SetProgramBindings` row therefore
// STAYS NULL, which is what PipeCatalogueTest already pins.
//
// OVERTURN CONDITIONS, one per row: give `MGPResourceDesc` and `MGPFlushRange` a blobref
// (overturns R-13.2/R-13.3, and c0 owns `MGPipeTypes.h`); give `MGPHandleOnly` a size
// (same owner) and P6 a real remote map; measure that a per-stage SPIR-V run beats one
// archive, at which point `CreateShaderState` needs a multi-blob row rather than this one.
// A hand-written third table, installed with the generated pair. Each row keeps the applier's
// signature because the monolith arm passes what the wire record does not carry:
// ResourceRespecify's `initialBytes` and ResourceFlushRange's `bytes` (sent as resource_subdata
// records instead), MapPersistent's `size`, `seedBytes` and void* result, CreateShaderState's
// seven SPIR-V/reflection blobs (monolith never serialises them), SetProgramBindings' tails.
struct MGPipeRouteEscapes {
Bool (*ResourceRespecify)(const MGPResourceDesc* desc, const void* initialBytes,
const MGPRespecifiedLevel* level);
@@ -207,24 +109,11 @@ namespace MobileGL::MG_Pipe {
const MGPipeRouteEscapes& MGPipeMonolithEscapes();
namespace detail {
// THE INSTALL THAT CANNOT BE LINKED AWAY, and the first version of this WAS.
//
// It began as a static initialiser inside PipeRoute.cpp, on the reasoning that
// `gMGPipeScreen` and `gMGPipeContext` are inline variables with CONSTANT (zero)
// initialisation - sequenced before every dynamic initialiser - so an installer in
// dynamic init necessarily beats any GL entry point. That reasoning is correct and the
// mechanism still failed, because it assumed PipeRoute.o would be in the link at all:
// every MG_Test target is its OWN binary over a static archive, `CsoCache.h` reaches
// the table through an inline thunk and names no symbol from PipeRoute.o, so the
// linker dropped the object, the initialiser never ran, and five CsoCacheTest cases
// took a null function pointer. The shared library linked it and was fine, which is
// exactly the shape that ships.
//
// AN INLINE VARIABLE FIXES BOTH HALVES AT ONCE. Its initialiser is emitted as a COMDAT
// in every translation unit that includes this header, so any binary that can call a
// thunk has one copy of it; and naming `MGPipeInstallMonolithTables` from a header the
// call sites already include is an undefined reference that forces the archive member
// into the link. There is still exactly one install, and it still happens before main.
// Installs the monolith tables before main in every binary that includes this header.
// A static initialiser in PipeRoute.cpp is not enough: a test binary linking the static
// archive may drop PipeRoute.o (CsoCache.h reaches the tables only through an inline
// thunk), leaving the tables null. This inline variable is emitted in each including
// translation unit, and its call to MGPipeInstallMonolithTables pulls PipeRoute.o in.
inline const Bool gMonolithTablesInstalled = (MGPipeInstallMonolithTables(), true);
} // namespace detail
@@ -240,31 +129,13 @@ namespace MobileGL::MG_Pipe {
void MGPipeNoteInstalledArm(MGPipeRouteArm arm);
// ---------------------------------------------------------------------------------
// The call-site names: MGPipeRoute<Name> for each of the thirty-eight
// The call-site names: MGPipeRoute<Name> for each routed MGPipeApply* entry point
// ---------------------------------------------------------------------------------
//
// EVERY ONE TAKES THE APPLIER'S OWN SIGNATURE, so converting a call site is a rename and
// nothing else. That is not tidiness, it is the G1 lesson from round 1 stated as a rule:
// rewriting `if (const auto f = TABLE.GL.Slot)` into a Bool-valued macro is semantically
// identical and generates DIFFERENT code (-144 bytes, two symbols resized). A wrapper that
// took `&expr` would force every site holding a temporary - `HandleOnly(handle)`,
// `BufferHandleOnly(handle)` - to grow a named local, which is a second expression shape
// change at forty sites. A reference parameter keeps the call site's text identical but
// for the name.
//
// THREE OF THE THIRTY-SEVEN TAKE ONE ARGUMENT MORE THAN THEIR APPLIER, and the extra is
// always the same thing: A BYTE COUNT THE RECORD DOES NOT DECLARE. CONTRACT-P5 table 1
// rule A requires every kHasBlob row to declare its length under split; five rows already
// do and the wrapper reads it back off the record, three do not and their call site is the
// only place the number exists:
// SetGlobalConstants declares 0 (ProgramEmit.h); the length is GetUBOSize()
// ResourceSubData buffer half declares real, TEXTURE half declares 0 on the
// grounds that the count is "the server's to compute" - which
// table 1 row 7 says "cannot be a bounds check". The level
// shadow's byte size is in scope at that call site.
// BufferSubDataResident the application's staging store, sized by the caller
// A defaulted argument was rejected: a default would let a site that has the number forget
// to pass it and still compile, and the failure would be a silently short upload.
// Each takes the applier's signature by reference, so converting a call site is a rename and a
// site passing a temporary needs no named local. Three also take a byte count the record does
// not declare and only the call site knows: SetGlobalConstants (GetUBOSize()), the texture half
// of ResourceSubData, and BufferSubDataResident. The count has no default, so a site cannot
// forget it and upload short.
// ---- screen ------------------------------------------------------------------------
inline Bool MGPipeRouteResourceCreate(const MGPResourceDesc& desc) {
@@ -272,27 +143,12 @@ namespace MobileGL::MG_Pipe {
MGP_ResourceCreate(&desc, &reply);
return MGPipeTakeReplyBool(reply, "resource_create");
}
// P12: WHICH RESPECIFIES STILL WAIT - one predicate, read by the wire emitter, so the two
// halves of this row cannot disagree about whether an answer is owed.
//
// THE BUFFER HALF NEVER DOES. Both call sites discard its Bool outright
// (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify(desc, nullptr);`), so the wait
// bought a value that was thrown away - on the device run this row was the second largest
// source of round trips in one frame, at ~5 ms each.
//
// THE TEXTURE HALF WAITS EXACTLY UNTIL THE OBJECT IS CONFIRMED, and "confirmed" is
// MGPipeHandleIsPublished: the latch is set only when the object's create was ACCEPTED, and
// the applier keeps texture records across a make-current (PipeFill.cpp:3756-3759), so for a
// published object the acceptance this row asks for is a fact the client already holds. The
// confirmation itself is paid once, at first use: EmitResourceRespecify sends a create when
// the latch is clear (TextureEmit.h:869-879), and that create is blocking.
//
// AND WHEN THE LATCH IS CLEAR THIS RETURNS TRUE, which is the whole fallback. It also covers
// the one place the two ends' consumer gates are worded differently - the client asks
// "any bit of the family" (PipeFill.cpp:1577-1595) while the applier asks "bit 10"
// (PipeApply.cpp:1408-1419) - because a create refused by the narrower server gate leaves the
// latch clear, so every later row for that object keeps taking the real answer. The cost of
// that case is throughput, never silence.
// Whether a resource_respecify record waits for its answer; the wire emitter reads this same
// predicate. Off the client wire arm it always does. The buffer half never waits: its callers
// discard the Bool. The texture half waits until the texture's create is accepted
// (MGPipeHandleIsPublished; a blocking create at first use), after which acceptance is known
// because the applier keeps texture records across make-current. A refused create leaves the
// latch clear, so later records keep waiting for the real answer.
inline Bool MGPipeResourceRespecifyWantsItsReply(const MGPResourceDesc& desc) {
if (MGPipeInstalledArm() != MGPipeRouteArm::kClientWire) return true;
if (static_cast<MGPipeResourceTarget>(desc.Target) == MGPipeResourceTarget::Buffer)
@@ -316,56 +172,13 @@ namespace MobileGL::MG_Pipe {
// ---- resources ---------------------------------------------------------------------
// P5e (ra), CONTRACT-P5E §2.5 / ruling 15 (ID-93): DOES THIS SUB-DATA RECORD WANT ITS
// ANSWER? One predicate, two callers - this route and the wire emit table - because the
// route cannot pass a flag through the thunk (the call table's signature is the
// catalogue's) and two hand-written copies of "is this the buffer half" is exactly the
// drift that would make the client wait for an answer the emitter told the server not to
// bother with, or read a slot that was never posted.
//
// THE BUFFER HALF DOES NOT: its Bool is discarded at the only call site there is
// (MGPipeEmitResourceSubData), and because the row carries kReplySlot the client used to
// wait for it anyway - one full round trip per 64 KB persistent-map block, whose answer
// nobody looked at.
//
// THE TEXTURE HALF DOES NOT EITHER, ON THE WIRE ARM (P12, the handoff's item B), and the
// argument is that its answer is DERIVABLE there - not that anybody stopped reading it.
// DrainTextureSubData still clears the level's dirty flag on an accepted answer (D-D5), and
// it still gets one; what it does not get any more is a ROUND TRIP for it:
//
// * THE EMISSION IS ALREADY GATED ON THE ANSWER'S CONTENT. A texture record is produced
// only where FamilyIsLive(kMGPipeSubsystemTextureResources, ...) holds, and under split
// that gate IS CapsMirrorInstance().ServerConsumes(bit 10) (PipeFill.cpp:1719-1740) - the
// same published fact the applier's own acceptance belt reads (PipeApply.cpp:1408-1419,
// NoP4aConsumer). So the one server-side refusal this acceptance exists to carry - "no
// consumer", the ID-39 / D-D5 / ID-18 M3 case that is WHY the texture half ever asked -
// cannot be the answer to a record this client emitted.
// * WHAT REMAINS IS A BUG CLASS, AND IT IS LOUD ALREADY. A record whose resource target
// names no texture, or that declares no texels or no bytes, is an MGP_TRIP_WIRE_REPORT on
// the server (PipeApply.cpp:1201-1218, :2245-2252); a record for a handle the applier
// does not hold is counted and named (ResolveResourceIn, :721-728), and the client's own
// tracker prevents it by creating before it ever sends sub-data (PipeFill.cpp:1045-1051).
// * AND EVERY ANSWER THE CLIENT CAN STILL GET FOR FREE, IT STILL GETS: the emitter reads
// this route's Bool exactly as before, so a record the CLIENT cancelled (the device-lost
// latch, an exhausted stage) still answers "not accepted" and the level stays dirty and
// is retried - D-D5's safe direction, unchanged.
//
// THE WAIT DOES NOT DISAPPEAR, IT MOVES TO THE STAGE. A record whose run does not fit
// SEG_STAGE reclaims retired bytes and parks on the stage doorbell (PipeWireCodec.cpp
// StageAllocate / ReclaimStagedBytes) - the one wait a run-ahead client owes, and the one
// that is proportional to the WINDOW rather than to the call count. That is the whole of item
// B: on Minecraft 26.2's atlas frame, 55,428 reply waits at ~4.4 ms each (96 MiB of uploads
// through a 32 MiB window) become the handful of stage retirements those same 96 MiB need.
// (Measured on the host by RemoteClientControls.TextureUploadsStopOwningAReplyOnTheWireArm,
// which counts the reply slots the client opens for a batch of uploads: N -> 0.)
//
// MONOLITH IS UNTOUCHED, and the arm test is what says so: there the applier's answer is
// returned by Mono_ResourceSubData on the calling thread and costs nothing, so there is
// nothing to stop waiting for and the predicate keeps its old answer.
//
// The test is MGPSubData::Target == kMGPipeResourceTargetBuffer, whole field, which is the
// invariant MGPipeTypes.h asserts beside the packer and which the applier's own
// SubDataNamesABuffer already reads.
// Whether a resource_subdata record waits for the server's answer (ARCHITECTURE.md §13.1.1).
// The route and the wire emit table both read this predicate, so neither waits for a reply the
// other did not request. On the wire arm no record waits: MGPipeEmitResourceSubData discards
// the buffer half's Bool, and a texture record is emitted only when the server consumes the
// texture family, so acceptance is known; a record the client cancels still answers false.
// Monolith answers for free and keeps the texture half's answer. The test reads the whole
// MGPSubData::Target field, as SubDataNamesABuffer does.
inline Bool MGPipeSubDataWantsItsReply(const MGPSubData& record) {
// THE WIRE ARM ONLY. Everything above is an argument about a client talking to a server
// over SEG_CMD/SEG_STAGE; a monolith run inside a split build (every unit and
+7 -29
View File
@@ -6,36 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P7 wave 0 (plan section 1.3's P7-marked refusal row, item (0); Ph slice (2)). HOW A BACKEND DEATH REACHES
// Session::Fail WITHOUT THE BACKEND KNOWING MG_Remote EXISTS.
// How a backend death reaches Session::Fail without the backend naming MG_Remote.
//
// The three Magma wire funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc),
// WireDescriptorFatal (Renderer/UniformManager.cpp) and WireBufferLegacyFatal
// (Renderer/WireDraw.inc) - each logged their line and raised their OWN std::abort(). Fifteen
// P7-marked refusals (the grep key is the at-sign form, not spelled here) and two more sites die through them, and every one of those deaths:
//
// * published NO SessionFault frame, so the peer read a bare EOF and could not name what
// ended the session (CONTRACT-P6 5.2's whole point);
// * bumped NO SessionFaultCount(), so exit gate S8's "zero faults over a good run" could
// neither confirm nor falsify them;
// * carried a HAND-TYPED family word with no path to FatalFamilies.def's projection, so
// nothing checked that `UnmigratedVerb` in the string still meant UnmigratedVerb.
//
// Calling SessionFail() from the .inc files directly would be the obvious fix and is the wrong
// one: MG_Backend's renderer would then name an MG_Remote symbol on its own account rather than
// through the two staging headers it already borrows under `#if MOBILEGL_BUILD_DISAGGREGATED`,
// and the layering that P13's module boundaries have to establish would be one more edge worse.
//
// So the shape is MGP_TRIP_WIRE_REPORT's (PipeApply.cpp:66): MG_Pipe owns the entry point, the
// DEFAULT behaviour is exactly what the site did before (same log line, same abort), and the
// richer behaviour is a function pointer that a layer above installs. MG_Remote installs
// InstallPipeSessionFailHook() at server-role init (MG_Backend/Init.cpp's InitServerRoleCommon,
// which both the inproc server role and the spawn child run), and from then on these deaths go
// through SessionFail like every other one.
//
// THE MESSAGE STRING IS PASSED VERBATIM, family word and all - the same rule FatalFunnel.h
// states. The lines these three funnels write are already counted by name in the retrace refusal
// census and in every recorded P7-refusal measurement, so they are byte-identical before and after.
// The backend's fatal funnels (Magma's wire refusals, Espryt's slot-table refusals) call
// MGPipeSessionFail, which by default logs the line and aborts. MG_Remote installs a hook at
// server-role init (MG_Backend/Init.cpp's InitServerRoleCommon, run by both server roles), so the
// death publishes a SessionFault frame, counts in SessionFaultCount() and carries a checked
// family word. The message passes verbatim, as FatalFunnel.h requires, so refusal censuses read
// the same line either way.
#pragma once
+13 -35
View File
@@ -42,25 +42,14 @@ namespace MobileGL::MG_Pipe {
virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0;
};
// ---- the verb sink: the five rows with no MGPipeApply* to delegate to ---------------
// ---- the verb sink: the rows with no MGPipeApply* entry point to delegate to ------------
//
// The decoder owns NO semantics, so every arm ends in an existing MGPipeApply* free
// function - except five, and they are exactly contract §7's class B: Clear (57), Blit
// (56), ReadPixels (58), DrawVbo (59) and Present (67). Those are GLFunctionsTable VERBS.
// MG_Pipe has no applier for any of them (the 37 MGPipeApply* entry points are the object
// and state families), so for these five P5 writes the first consumer as well as the first
// producer - and the consumer is the SERVER'S backend call, which is v1's, not the codec's.
//
// So the codec does what it can prove and stops there: it bounds-checks, cross-checks the
// tail, resolves the segments and hands over a DECODED, VALIDATED argument list. With no
// sink installed those five arms return false ("this build does not implement it"), which
// is the same answer the other unimplemented rows give.
//
// SetShaderBuffers (38) and SetStreamOutputTargets (39) also have no applier entry point,
// and they deliberately get NO sink method: both are off P5's reduced path (BRIEF §4's
// exclusion list), so inventing a consumer for them would be building a semantics nobody
// can test this phase. Their arms validate both tails - which is the part a later phase
// must not have to re-derive - and return false.
// The decoder owns no semantics. A row with an applier entry point ends in its MGPipeApply*
// function; the GLFunctionsTable verbs, fences, queries and control records are bounds-checked,
// tail-checked and segment-resolved, then handed to the sink below as a validated argument
// list. With no sink installed those arms return false ("this build does not implement it"),
// as does every row with neither an entry point nor a sink method, once validated
// (SetStreamOutputTargets checks both its tails first).
struct MGPipeQueryResultReply {
Uint64 Value = 0;
Uint32 Produced = 0;
@@ -139,22 +128,11 @@ namespace MobileGL::MG_Pipe {
return false;
}
// ---- P5b (MG_Remote/CONTRACT-P5B.md): the rows the four migration packages consume.
// ---- the remaining verbs with no MGPipeApply* entry point: dispatch, barriers, copies,
// image and storage-block binds, transform feedback, patch parameter and mipmaps.
//
// Every one below is a GLFunctionsTable verb with NO MGPipeApply* entry point - the
// census's correction - so, exactly like the five above, the codec validates and hands
// over and the SERVER'S sink (Server/PipeApplier.cpp's ServerVerbSink) makes the
// backend call. The default bodies return false ("this build does not implement it");
// ServerVerbSink's stubs die Fatal{UnmigratedVerb, "<GL slot>"} by name until the owning
// package lands the real body, so a client that flips a slot ahead of its server half
// aborts with the same line the census greps rather than rendering nothing.
//
// i1 OnLaunchGrid, OnMemoryBarrier, OnResourceCopyRegion, OnBindShaderImage,
// OnSetStorageBlockBinding
// t2 OnBeginStreamOutput, OnEndStreamOutput, OnPauseStreamOutput,
// OnResumeStreamOutput, OnBindStreamOutput, OnPatchParameter
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture (and OnClear's non-Whole kinds)
// d1 OnDrawVbo's indirect tail and user-index span (above)
// Same hand-over as the rows above: the codec validates the record, and the backend call is
// made by MG_Record::RecordVerbSink, the base of the server's ServerVerbSink.
virtual Bool OnLaunchGrid(const MGPGridInfo& grid) {
(void)grid;
return false;
@@ -212,7 +190,7 @@ namespace MobileGL::MG_Pipe {
return false;
}
// ---- P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records, opcodes 77..78.
// ---- P5c (docs/archive/contracts/CONTRACT-P5C.md §5): the two control records, opcodes 77..78.
//
// Same hand-over shape as the P5b rows: the codec validates the record (a fixed-size
// POD, no blob, no tail) and the SERVER'S sink does the work - OnApplierReset runs the
@@ -244,7 +222,7 @@ namespace MobileGL::MG_Pipe {
return false;
}
// ---- shared images, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
// ---- shared images, opcode 85 (docs/archive/notes/anland/plan-ahb-dmabuf.md).
// `seq` is the record's reply-slot id, which an Import's descriptor names on the aux
// socket. True fills `reply`; false is a DECLINED answer.
virtual Bool OnSharedImage(const MGPSharedImageOp& op, Uint64 seq,
+8 -61
View File
@@ -5,67 +5,14 @@
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// D-K2's SUBSYSTEM DEPENDENCY RULE, IN ONE PLACE (P3b/P4b R-5).
//
// WHAT WAS WRONG. The rule "bit X requires bit Y" was stated in SIX places, and the audit found
// two of them WRONG in the same direction:
//
// * MG_Pipe/MGPipe.h's comment said "THREE OF THEM HAVE A DEPENDENCY" (there are six rows) and
// "The mirror pairs (10 without 11, 10 without 9, 7 without 10) are all fine" - but 10
// without 11 is exactly D-K2's FOURTH row, refused by the server at
// Managers.cpp ResolveTextureResourceSubsystemArm and withheld by the client at
// PipeFill.cpp's table. PipeFill.cpp says so out loud: "The brief's original
// 'bit 10 without 11 is fine' is WITHDRAWN for P4a as built.";
// * MG_Backend/DirectGLES/Managers.h's header comment repeats both defects, in the header of
// the file that IMPLEMENTS the refusal.
//
// Neither wrong comment could fail anything, which is the whole argument for a table: the two
// EXECUTABLE statements of the rule are in different link units with no shared header, and the
// four PROSE statements drifted because nothing compared them to anything.
//
// WHAT THIS FILE IS. The rule, once, as rows. It is a preprocessor-only table like
// MG_Remote/FatalFamilies.def - no generator reads it - and it carries pure bit constants, so
// including it from MGPipe.h keeps check_include_closure.py's value-header rule satisfied.
//
// WHO READS IT. MGPipe.h turns it into kMGPipeSubsystemDependencies[],
// MGPipeSubsystemRequires(), MGPipeSubsystemDependenciesAreSet() and
// MGPipeSubsystemDependencyWhy(). The two executable statements of the rule read those, and as
// of wave 2-D package D3 they read NOTHING ELSE - every hand-written copy is gone:
//
// CLIENT dependency check MG_Impl/Pipe/PipeFill.cpp's P4aFamilyDependencyBits /
// P4aFamilyDependenciesAreSet (bits 9-12) and P5eFamilyIsLive
// (bit 13). The local kMGPipeP4aFamilyDependencies[] is deleted;
// what stays is the P4a-specific COVERAGE static_assert, because
// that gate answers for four of these six families and a fifth P4a
// family without a row here must still be a compile error.
// SERVER consumer gate MG_Backend/DirectGLES/Managers.cpp's four
// Resolve<Family>SubsystemArm() over PipeSubsystemDependencyMissing,
// which now takes the FAMILY rather than a literal bit (bits 9-12),
// plus ResolveVertexInputSubsystemArm (bit 8) and DirectGLES.cpp's
// ResolveBufferBindingSubsystemArm (bit 13). The texture family's
// two guarded calls collapse into one, and the refusal names
// whichever required bit is actually missing instead of always the
// first, because the missing set is computed from the row.
//
// The rows moved here one commit before the readers did (R-5 landed with PipeFill.cpp and
// Managers.cpp belonging to packages in flight), and the anti-drift property never waited for
// that: MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp drives BOTH readers at every
// interesting mask and compares them against these rows - a change to either side alone reds.
// It is now a comparison of two READERS rather than of two copies, which is strictly weaker as
// a drift detector and strictly stronger as a guarantee; it is kept because the two sides still
// fold in reasons of their own (the server's own family bit, D-C3's wire-caps refusal, the
// client's consumer and wired conjuncts) and those can still disagree.
//
// THE ROWS ARE NOT TRANSITIVE, and that is load-bearing rather than an oversight: each reader
// tests the required BITS against MOBILEGL_PIPE_PUSH alone, never the other family's liveness,
// because Espryt's resolvers classify their arms from that mask alone. Making the rows
// transitive here would make the two sides disagree at some mask, which is the failure the
// table exists to prevent. PipeFill.cpp:1576-1581 argues this at length and
// TextureEmitTest.cpp:1278-1283 pins it.
//
// THE MIRROR PAIRS THAT STAY FINE are stated in MGPipe.h beside the table rather than here,
// because an absence is not a row.
// The subsystem dependency rule ("bit X requires bit Y"), stated once as rows. Preprocessor
// only (no generator reads it) and pure bit constants, so MGPipe.h may include it under
// check_include_closure.py's value-header rule. MGPipe.h builds kMGPipeSubsystemDependencies[]
// and the MGPipeSubsystem{Requires,DependenciesAreSet,DependencyWhy} helpers from it; the client
// gate (MG_Impl/Pipe/PipeFill.cpp) and Espryt's Resolve<Family>SubsystemArm read only those, and
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp checks both against the rows. Rows are not
// transitive: each reader tests the required bits against MOBILEGL_PIPE_PUSH alone, as Espryt's
// resolvers do. The bit pairs that are safe without a row are listed in MGPipe.h.
// clang-format off
+6 -18
View File
@@ -12,24 +12,12 @@
#include <MG_Pipe/MGPipe.h>
// THE VERB -> RECORD TABLE (P15). Every MGPipeVerb names the record op its emitter publishes, and
// the client's barrier decision at the validate point is that op's wait class (PipeCalls.def) and
// nothing else. Commands are recorded and replayed asynchronously by default; a verb syncs only
// because the record it emits has a wait class that says so.
//
// IT IS THE FORWARD DIRECTION. MGP_VERB_OP_LIST (FieldOwnership.def) is op -> verb, the
// direction the SERVER needs to stamp a verb boundary, and it is many-to-one (one draw_vbo row
// for twenty draw verbs, one blit row for two blit verbs). Inverting it verb-first answered
// "no row" for every verb but the representative, and the old fallback turned "no row" into
// "barriered" - a forced applier quiesce per verb (FCL: one per frame from the blit, 1.65 ms of
// GL-thread wait). Here there is no fallback in either direction: a verb with no row, or with two,
// is a compile error (the static_assert below).
//
// A verb whose emitter can publish more than one op names the one with the STRONGEST wait class
// (the fill decision is made before the record exists, so it must cover the worst record the verb
// may emit). kMGPipeNoRecord names a verb that publishes nothing: the client answers it from the
// caps mirror, so no apply can read a fill for it.
//
// Verb -> record table: each MGPipeVerb names the record op its emitter publishes, and the
// client's barrier decision at the validate point is that op's wait class (PipeCalls.def) alone.
// It is the forward direction of MGP_VERB_OP_LIST (FieldOwnership.def), which is many-to-one and
// cannot be inverted. A verb that may publish several ops names the one with the strongest wait
// class, since the fill is decided before the record exists; kMGPipeNoRecord marks a verb
// answered from the caps mirror. A missing or duplicate row is a compile error, never a default.
// X(Verb, Op)
// clang-format off
#define MGP_VERB_RECORD_OP_LIST(X) \
@@ -206,10 +206,9 @@ inline constexpr SizeT kMGPipeFieldOwnershipRowCount =
static_assert(kMGPipeFieldOwnershipRowCount == 70, "table 2's row count moved");
// An ARGUMENT-KEYED narrowing of one field. The field keeps its single row above; this
// says that one argument value of it belongs to a different class. Coverage.def:62-69 already
// rules the shape for GetBufferBindingSlot - "THE ROW STAYS ONE ROW ... the field is ONE array
// that a second row of the same name could only duplicate" - and m_pixelStore[2] is the same
// shape indexed by its own isUnpack argument.
// says that one argument value of it belongs to a different class. Coverage.def's comment on the
// GetBufferBindingSlot row already rules the shape - "The field is one array, so it stays one
// row" - and m_pixelStore[2] is the same shape indexed by its own isUnpack argument.
struct MGPipeFieldArgumentOwnership {
MGPipeInputField Field;
Uint32 Arg0;
@@ -265,8 +264,8 @@ static_assert(MGPipeEveryFieldIsClassified(),
// mask and name, so a field inside that mask would read FRESH while holding the previous
// verb's value - the one silent failure this table has. The exemptions:
// Flush not a verb at all: the verb census (BRIEF §12 C-3) found Flush is not a GLFunctionsTable
// Present FillPoints.def:21 - Present and SetSwapInterval go through BackendObject virtuals and
// SetSwapInterval the same sentence of FillPoints.def:21, and it is class C besides
// Present Present and SetSwapInterval go through BackendObject virtuals and
// SetSwapInterval not a verb for the same reason as Present, and it is class C besides
constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
switch (op) {
case MGPWireOp::Clear: return MGPipeVerb::Clear;

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