mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
11
Commits
174a428e81
...
37cb446bda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37cb446bda | ||
|
|
7bb5a9ccbe | ||
|
|
1161d489e2 | ||
|
|
92f1b9b231 | ||
|
|
265966a15d | ||
|
|
23ee8bdb49 | ||
|
|
37ecba200b | ||
|
|
7ec1448f03 | ||
|
|
211ca08b0c | ||
|
|
aabca3c171 | ||
|
|
0e3e2b7761 |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
// =====================================================================================
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
+189
-680
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) \
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user