mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
246
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5175f9ccd5 | ||
|
|
45b759e7a5 | ||
|
|
b4bbcc113a | ||
|
|
dd4afb0faf | ||
|
|
72a4cba5ad | ||
|
|
418e6f50be | ||
|
|
1ee6c8953a | ||
|
|
ec20e7001d | ||
|
|
41b4f8dfe3 | ||
|
|
a29807cc48 | ||
|
|
8c458cd594 | ||
|
|
c2c6a655ea | ||
|
|
9f60aadc1d | ||
|
|
a690032f85 | ||
|
|
173f1dd273 | ||
|
|
a38bdab4e2 | ||
|
|
6035c9d7f8 | ||
|
|
419f9941b5 | ||
|
|
72b4c91a45 | ||
|
|
ccb7b6b331 | ||
|
|
e4579ee80e | ||
|
|
0d01405cf8 | ||
|
|
dcc31e95ca | ||
|
|
4678519f99 | ||
|
|
4614abb9c6 | ||
|
|
7107d0f47c | ||
|
|
972dd811d7 | ||
|
|
f9c0e7ec43 | ||
|
|
b938c83b6f | ||
|
|
e62abff6b6 | ||
|
|
9a0a7f7608 | ||
|
|
993ce0fb76 | ||
|
|
ea38cfcb99 | ||
|
|
9c3e8ffd32 | ||
|
|
2f35014d73 | ||
|
|
8c8d460e14 | ||
|
|
b6e5c2bb39 | ||
|
|
246434bb35 | ||
|
|
41b7905000 | ||
|
|
2e03b82a67 | ||
|
|
2bebbbdcb7 | ||
|
|
500a8e462a | ||
|
|
be939e2cfe | ||
|
|
68d09bb585 | ||
|
|
5e7fb18c4a | ||
|
|
74d2daea1b | ||
|
|
7afa2f18d3 | ||
|
|
64332060fd | ||
|
|
96dca5a8ba | ||
|
|
e24e30bbec | ||
|
|
929d162594 | ||
|
|
0b12f89f3b | ||
|
|
31e6a7d6d8 | ||
|
|
29bd4d2c94 | ||
|
|
588b2277c5 | ||
|
|
1e8cdc5931 | ||
|
|
a632e26e47 | ||
|
|
ee31944bce | ||
|
|
245daca06a | ||
|
|
29d51ab98b | ||
|
|
17396216e0 | ||
|
|
a666201708 | ||
|
|
71a174a531 | ||
|
|
83a209bbd4 | ||
|
|
832620ddeb | ||
|
|
25e1a8483b | ||
|
|
2bd8f3c82d | ||
|
|
e021b4f62b | ||
|
|
a3bd7f0d7d | ||
|
|
5046b3fc89 | ||
|
|
5e5dc36e57 | ||
|
|
2bd19d5ee2 | ||
|
|
33f5e878e7 | ||
|
|
568d587ae7 | ||
|
|
602326f379 | ||
|
|
0338812cf3 | ||
|
|
09e0a83cee | ||
|
|
771e8e06a1 | ||
|
|
2d2090cf1c | ||
|
|
a11a5eb2af | ||
|
|
612dbac44f | ||
|
|
92dffb81b4 | ||
|
|
79b581984b | ||
|
|
d6e52f75c3 | ||
|
|
ac09e5b37a | ||
|
|
820d1a60b9 | ||
|
|
f0f8cab682 | ||
|
|
b2edca2728 | ||
|
|
ccde29064a | ||
|
|
bc2aad2176 | ||
|
|
5a3e9f0857 | ||
|
|
1534cf3784 | ||
|
|
712c946744 | ||
|
|
8f1eaafa79 | ||
|
|
08922b6f78 | ||
|
|
711d5c61ba | ||
|
|
5ab90dec24 | ||
|
|
4299acd9c1 | ||
|
|
953d73f111 | ||
|
|
d01439d846 | ||
|
|
c9958075e8 | ||
|
|
ae1a1c503f | ||
|
|
a02f1571f5 | ||
|
|
dca3eb868e | ||
|
|
7408bad9fd | ||
|
|
e8502a6100 | ||
|
|
9ea44389e7 | ||
|
|
17db759891 | ||
|
|
2cb44039b5 | ||
|
|
32033d6993 | ||
|
|
08192d7266 | ||
|
|
37da3c3a07 | ||
|
|
fde5fda3b5 | ||
|
|
6515c8e6ae | ||
|
|
d54ec57a5d | ||
|
|
c20e2f2b67 | ||
|
|
680ea63360 | ||
|
|
959ca30810 | ||
|
|
0bff6875b3 | ||
|
|
707bced438 | ||
|
|
83c8101a9b | ||
|
|
2a5e0195b8 | ||
|
|
afda613bd9 | ||
|
|
433f51a065 | ||
|
|
9ba5d7ba1e | ||
|
|
f0beefa85c | ||
|
|
78ff014547 | ||
|
|
3e298c9ad1 | ||
|
|
fd6b5bdbf2 | ||
|
|
8d3cd67b94 | ||
|
|
f6c7dcd1b6 | ||
|
|
4ad88214cd | ||
|
|
4412cef00c | ||
|
|
6e9943b36e | ||
|
|
0cb46fe2bd | ||
|
|
83302ca247 | ||
|
|
3c55e02797 | ||
|
|
75ea7ee2df | ||
|
|
13b380feda | ||
|
|
ed83424c75 | ||
|
|
df055eab13 | ||
|
|
511b3752c0 | ||
|
|
b28058f37c | ||
|
|
14137bc9a6 | ||
|
|
39933613ae | ||
|
|
816373ffd9 | ||
|
|
7047331a41 | ||
|
|
7ef8cb93fc | ||
|
|
b97a228cea | ||
|
|
3840cf734b | ||
|
|
68db6db7f8 | ||
|
|
cd05de504e | ||
|
|
ce24e2a734 | ||
|
|
4e44650199 | ||
|
|
9951961d9c | ||
|
|
31e370bed5 | ||
|
|
0c55560510 | ||
|
|
f11e78b0a4 | ||
|
|
d9bde13127 | ||
|
|
cc427ec4de | ||
|
|
b11bb9650a | ||
|
|
14efd6eb24 | ||
|
|
874d1ee77d | ||
|
|
b9eaa47480 | ||
|
|
42d43af25b | ||
|
|
12e6bfcf14 | ||
|
|
e6452ce948 | ||
|
|
56366331dc | ||
|
|
355c60b901 | ||
|
|
6eb0e675ad | ||
|
|
45c8f1a8be | ||
|
|
5cb826b01e | ||
|
|
e01c0ccc53 | ||
|
|
c036900d72 | ||
|
|
44c2b5cf3a | ||
|
|
738b289df8 | ||
|
|
a9778eaabe | ||
|
|
c73ae7d443 | ||
|
|
55d2af9bd1 | ||
|
|
2d690754dd | ||
|
|
7a2e256133 | ||
|
|
bb2a236d5f | ||
|
|
13d7e32b7b | ||
|
|
b1c37699b1 | ||
|
|
e5603f9a46 | ||
|
|
08d14d85ef | ||
|
|
af20dba6db | ||
|
|
a5d1136c02 | ||
|
|
d704401a56 | ||
|
|
ce9f44a24c | ||
|
|
1a012f2820 | ||
|
|
b9c137e146 | ||
|
|
e9499d38bd | ||
|
|
f1780b9000 | ||
|
|
e5c032c89e | ||
|
|
a174a06c79 | ||
|
|
46841ac706 | ||
|
|
01179c54d2 | ||
|
|
3594f03c4e | ||
|
|
43f8b47088 | ||
|
|
c74c4819fb | ||
|
|
96c544514e | ||
|
|
59191cd296 | ||
|
|
8d0ed5b82c | ||
|
|
f15b0fdf4b | ||
|
|
8f66c374aa | ||
|
|
067b186677 | ||
|
|
5d4d91fe7e | ||
|
|
bb781df527 | ||
|
|
c574043c13 | ||
|
|
3ab394e2b8 | ||
|
|
43bf97cc87 | ||
|
|
3302ee82b5 | ||
|
|
7dec32a574 | ||
|
|
dcfa5ad311 | ||
|
|
aa64c91052 | ||
|
|
fcd4ad3799 | ||
|
|
de532f55a9 | ||
|
|
3d1a866e82 | ||
|
|
df784c6752 | ||
|
|
c9dd173201 | ||
|
|
f5bd1a0412 | ||
|
|
6cb7d1b83b | ||
|
|
7c97fcfee3 | ||
|
|
caa0a7221b | ||
|
|
eb81705130 | ||
|
|
e10f5d6750 | ||
|
|
5a3c0616b7 | ||
|
|
9a8369296e | ||
|
|
149e26a79a | ||
|
|
3160c4b85b | ||
|
|
bd2092f4eb | ||
|
|
f4dbea2300 | ||
|
|
9eae98581f | ||
|
|
d7655247f7 | ||
|
|
842af23331 | ||
|
|
d1a7c5f159 | ||
|
|
ce370a3e84 | ||
|
|
bee07c3273 | ||
|
|
7c2c1456f8 | ||
|
|
ad1238bd6f | ||
|
|
a9bb99a46a | ||
|
|
02b970e9c1 | ||
|
|
eec92cd221 | ||
|
|
810850b13a | ||
|
|
9c6a8a25d8 |
+213
-13
@@ -420,13 +420,20 @@ jobs:
|
|||||||
echo "::error::nm --defined-only sees only ${defined} symbols in ${BUILD_DIR}/libMobileGL.so - it looks stripped, so the two checks below could not have failed honestly"
|
echo "::error::nm --defined-only sees only ${defined} symbols in ${BUILD_DIR}/libMobileGL.so - it looks stripped, so the two checks below could not have failed honestly"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for entry in MGPipeVerifyInputs MGPipeFillForVerb; do
|
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -q "MGPipeVerifyInputs"; then
|
||||||
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -q "${entry}"; then
|
echo "::error::libMobileGL.so defines no MGPipeVerifyInputs: -DMOBILEGL_PIPE_VERIFY=ON did not take, and every lane that consumes this artifact would run the comparator-free library and pass having compared nothing"
|
||||||
echo "::error::libMobileGL.so defines no ${entry}: -DMOBILEGL_PIPE_VERIFY=ON did not take, and every lane that consumes this artifact would run the comparator-free library and pass having compared nothing"
|
exit 1
|
||||||
exit 1
|
fi
|
||||||
fi
|
# The per-verb entry point, under EITHER of its two names. P2 renames
|
||||||
done
|
# MGPipeFillForVerb to MGPipeValidateForVerb (the body becomes the tracker's walk and
|
||||||
echo "libMobileGL.so defines MGPipeVerifyInputs and MGPipeFillForVerb (${defined} defined symbols)"
|
# the fill is one of its five steps), so this check has to accept both or it goes red on
|
||||||
|
# the rename for a reason that has nothing to do with what it tests. What it tests is
|
||||||
|
# unchanged: that the library HAS a per-verb entry point compiled in.
|
||||||
|
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -qE "MGPipeValidateForVerb|MGPipeFillForVerb"; then
|
||||||
|
echo "::error::libMobileGL.so defines neither MGPipeValidateForVerb nor MGPipeFillForVerb: there is no per-verb entry point in this artifact, so nothing fills the block the comparator compares"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "libMobileGL.so defines MGPipeVerifyInputs and a per-verb entry point (${defined} defined symbols)"
|
||||||
|
|
||||||
- name: Show ccache stats
|
- name: Show ccache stats
|
||||||
if: always()
|
if: always()
|
||||||
@@ -542,6 +549,91 @@ jobs:
|
|||||||
ctest --output-on-failure -L integration-verify --no-tests=error
|
ctest --output-on-failure -L integration-verify --no-tests=error
|
||||||
fi
|
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.
|
||||||
|
- 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)"
|
||||||
|
|
||||||
|
# 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 (ROADMAP.md:7).
|
||||||
|
# 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 ROADMAP.md:20
|
||||||
|
# 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:100 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.
|
||||||
|
- name: The handle-ABA, CSO, subsystem and texture-parameter controls (G8, G8b, G9, G10, G12)
|
||||||
|
working-directory: build-verify
|
||||||
|
env:
|
||||||
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
|
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
||||||
|
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
|
||||||
|
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
|
||||||
|
run: |
|
||||||
|
ulimit -c unlimited
|
||||||
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
|
ctest --output-on-failure -L integration-gpu \
|
||||||
|
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystem|MapPersistentRoundtrip|ObjectSubsystem|TextureParamsWithoutASamplerView|TextureUploadShape' \
|
||||||
|
--no-tests=error -j 4
|
||||||
|
|
||||||
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
|
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
|
||||||
# library opens it fopen(path, "w"), so after an ambient lane of 400-odd processes the file
|
# library opens it fopen(path, "w"), so after an ambient lane of 400-odd processes the file
|
||||||
# holds the LAST one - grepping it would say nothing about the other 405 and would red a
|
# holds the LAST one - grepping it would say nothing about the other 405 and would red a
|
||||||
@@ -698,7 +790,16 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install -y clang-20 libx11-dev
|
run: sudo apt-get update && sudo apt-get install -y clang-20 libx11-dev
|
||||||
|
|
||||||
- name: Include-closure assertions and negative control
|
- name: Include-closure assertions and negative control
|
||||||
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
|
# --expect-probes 4 (contract-v2.md 7.6): an exit code cannot tell four probes from none,
|
||||||
|
# so a --probe typo or a manifest edit that selected nothing would run zero probes and
|
||||||
|
# exit 0 - the second half of the finding that added the flag. The count is the length of
|
||||||
|
# scripts/check_include_closure.py's PROBES list and changing one means changing the other.
|
||||||
|
#
|
||||||
|
# --compiler stays clang++-20, which is what the step above installs (Debian's clang-20
|
||||||
|
# package ships /usr/bin/clang++-20). It is deliberately NOT the bare `clang++` the local
|
||||||
|
# campaign gate spells: that spelling exists because the WSL box has no clang++-20, and
|
||||||
|
# copying it here would trade a version-pinned compiler for whatever the runner has.
|
||||||
|
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all --expect-probes 4
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -1483,10 +1584,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Deliberately independent of build-linux: these are source-level gates, they take
|
# Deliberately independent of build-linux: these are source-level gates, they take
|
||||||
# seconds, and a broken build must not hide a drifted interface.
|
# seconds, and a broken build must not hide a drifted interface.
|
||||||
|
env:
|
||||||
|
# THE CURRENT PHASE's base ref, for the two G5 region gates below. It is 37da3c3a - P4a's
|
||||||
|
# base ref, INTEGRATOR-DECISIONS ID-1 - and NOT the workflow's baseline_sha input: that input
|
||||||
|
# is the SYMBOL baseline (087685d1, P1's G1 reading) and it is empty on a push, whereas these
|
||||||
|
# gates ask "did the do-not-touch list move since the phase started".
|
||||||
|
#
|
||||||
|
# IT MOVED FROM P3a's 44c2b5cf TO P4a's 37da3c3a WITH THE PHASE, and that is a deliberate
|
||||||
|
# narrowing rather than a loss: P3a's eleven functions were compared against 44c2b5cf at P3a's
|
||||||
|
# own exit and were byte-identical there, so 37da3c3a carries the same bodies (measured: the
|
||||||
|
# eleven shas at 37da3c3a are the eleven shas at 44c2b5cf, and FlushPendingRangesFrom's is
|
||||||
|
# still the sha pinned in the script at 3e298c9a). What the two gates now both answer is "did
|
||||||
|
# anything on the list move during P4a", which is the question this phase can act on.
|
||||||
|
BASELINE: "37da3c3a"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
# The G5 gate reads Managers.cpp at BASELINE with `git show`, which a depth-1 checkout
|
||||||
|
# does not have. Nothing else in this job needs history.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
|
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
|
||||||
# keeps the two interface tables, the wire records, the verify comparators, the
|
# keeps the two interface tables, the wire records, the verify comparators, the
|
||||||
@@ -1525,11 +1643,93 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
|
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
|
||||||
|
|
||||||
# Informational: the frontend mutation surface an MGPipe aggregate generation has to
|
# A GATE as of P2, which is when MG_Pipe/DirtySurface.def exists to diff the scan against
|
||||||
# cover. It becomes a gate in P2, when the mapping file exists to diff against
|
# (ROADMAP.md:18 puts the first mapping round in P2). --check fails BOTH directions: a
|
||||||
# (ROADMAP.md:18 puts the first mapping round in P2, not P1).
|
# mutator the scanner finds with no row in the def, and a row naming a mutator the scan no
|
||||||
- name: MGPipe dirty-surface report
|
# longer finds - so a deleted mutator cannot leave a stale row behind claiming coverage.
|
||||||
run: python3 scripts/gen_pipe_dirty_surface.py --summary
|
#
|
||||||
|
# --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.
|
||||||
|
- name: MGPipe dirty-surface mapping is complete (G9)
|
||||||
|
run: |
|
||||||
|
python3 scripts/gen_pipe_dirty_surface.py --check
|
||||||
|
python3 scripts/gen_pipe_dirty_surface.py --self-test
|
||||||
|
|
||||||
|
# A GATE as of P3a (G5). "pool 与延迟释放原样搬" (ROADMAP.md:19) is meant literally: the
|
||||||
|
# buffer pool, the deferred-release drain and the three persistently mapped rings move
|
||||||
|
# VERBATIM, and ARCHITECTURE.md:515 says why - their retire happens only inside Present, so
|
||||||
|
# a batching or ordering change there starves them, and nothing else in this workflow can
|
||||||
|
# see it. P3a rewrites the rest of Managers.cpp by design, so a file diff says nothing; the
|
||||||
|
# script extracts the ELEVEN named bodies and compares their hashes on their own.
|
||||||
|
#
|
||||||
|
# Eleven and not ten (ID-15): Managers.cpp carries the three-tier flush drain TWICE, once
|
||||||
|
# per preprocessor arm, and a push build compiles only FlushPendingRangesFrom while the
|
||||||
|
# untouched FlushPendingRangesNow lives in the `#else`. Hashing the pull name alone would
|
||||||
|
# protect text no shipping build compiles, so both are hashed - the pull ladder against
|
||||||
|
# BASELINE, the push ladder against a sha pinned in the script at 3e298c9a, because that
|
||||||
|
# one was born in P3a and has no body at the base ref to compare with.
|
||||||
|
#
|
||||||
|
# Scoped to the disaggregation branch and to a manual dispatch, deliberately: the question
|
||||||
|
# is "did these eleven move since P3a started", and BASELINE is P3a's base ref. On dev,
|
||||||
|
# where unrelated buffer fixes land on their own schedule, the same comparison would be
|
||||||
|
# asking a question nobody posed - it belongs with the TEMPORARY trigger lines at the top
|
||||||
|
# of this file and retires with them.
|
||||||
|
#
|
||||||
|
# --self-test is the half that keeps it honest, and it is not optional: a comparison that
|
||||||
|
# silently stopped comparing produces exactly the same green as eleven untouched bodies. It
|
||||||
|
# runs six canned controls - eleven bodies extracted, an untouched copy compared equal, an
|
||||||
|
# edit OUTSIDE them ignored, and each of the three perturbation targets (ClearBufferPool
|
||||||
|
# and BOTH flush ladders) reported BY NAME - and fails if any of them does not answer.
|
||||||
|
# Same shape as gen_pipe.py --self-test above.
|
||||||
|
- name: The buffer pool, the deferred-release drain and the rings did not move (G5)
|
||||||
|
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
run: bash scripts/p3a_untouched_regions.sh "${BASELINE}" HEAD
|
||||||
|
|
||||||
|
- name: The untouched-region gate can still fail (G5)
|
||||||
|
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
run: bash scripts/p3a_untouched_regions.sh --self-test
|
||||||
|
|
||||||
|
# A GATE AS OF P4a (G5), and a SECOND script rather than an edit to the one above. P4a extends
|
||||||
|
# the same claim to the rest of ARCHITECTURE.md:318's do-not-touch list - the unpack PBO ring's
|
||||||
|
# staging repack and its two helpers, the attachment permutation, the D24S8 sampling-emulation
|
||||||
|
# core and the format-caveat handler - which is SEVENTEEN regions across THREE files
|
||||||
|
# (BRIEF-P4A.md D-N: P3a's eleven, which P4a must not touch either, plus P4a's six). The
|
||||||
|
# parent's SOURCE_PATH is a single file, so the extension needed a per-region source path and
|
||||||
|
# a region KIND (DepthStencilSamplingReadImpl is a namespace, not a function); everything else
|
||||||
|
# about the extraction is its parent's, verbatim.
|
||||||
|
#
|
||||||
|
# Both scripts run. The parent keeps answering its own question against its own eleven, so a
|
||||||
|
# regression in either half names itself, and neither gate can be silenced by editing the
|
||||||
|
# other's list.
|
||||||
|
#
|
||||||
|
# Same feat/disaggregated-or-dispatch guard as the P3a step, for the same reason: the question
|
||||||
|
# is "did these move since the phase started", and on dev - where unrelated buffer and texture
|
||||||
|
# fixes land on their own schedule - it would be a question nobody posed. It belongs with the
|
||||||
|
# TEMPORARY trigger lines at the top of this file and retires with them.
|
||||||
|
#
|
||||||
|
# --self-test is the half that keeps it honest and is not optional: a comparison that silently
|
||||||
|
# stopped comparing produces exactly the same green as seventeen untouched regions. It runs
|
||||||
|
# three positive controls (seventeen regions extracted, an untouched copy compared equal, an
|
||||||
|
# edit OUTSIDE them invisible in all three files) and FOUR negative ones - ClearBufferPool,
|
||||||
|
# FlushPendingRangesNow, RecomputeBackendColorSlots and StageBlocksIntoUnpackRing, each
|
||||||
|
# perturbed on its own and each required to be named BY NAME - and fails if any of them does
|
||||||
|
# not answer.
|
||||||
|
- name: The unpack ring, the attachment permutation, the D24S8 core and the format caveat did not move (G5)
|
||||||
|
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
run: bash scripts/p4a_untouched_regions.sh "${BASELINE}" HEAD
|
||||||
|
|
||||||
|
- name: The P4a untouched-region gate can still fail (G5)
|
||||||
|
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
run: bash scripts/p4a_untouched_regions.sh --self-test
|
||||||
|
|
||||||
# Warning only for now: the disaggregation documents are still being written, and a
|
# Warning only for now: the disaggregation documents are still being written, and a
|
||||||
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
|
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
|
||||||
|
|||||||
@@ -21,12 +21,26 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
|
|||||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||||
# gates keep (section 10.3).
|
# gates keep (section 10.3).
|
||||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||||
|
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
|
||||||
|
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
|
||||||
|
# only make sense when the two roles share an address space. It is a SUPERSET, never a
|
||||||
|
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
|
||||||
|
# only decides whether the shims are compiled in.
|
||||||
|
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
|
||||||
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
||||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
||||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||||
|
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||||
|
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||||
|
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||||
|
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||||
|
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||||
|
# the whole migration window; it retires with the pull path itself at P13.
|
||||||
|
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||||
|
|
||||||
@@ -328,6 +342,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||||
@@ -437,6 +452,17 @@ set(SOURCE_FILES
|
|||||||
# option OFF not one file here is compiled and no include path is added.
|
# option OFF not one file here is compiled and no include path is added.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
|
||||||
|
# variable, not a forced cache write, for the reason the two implications below give: an
|
||||||
|
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
|
||||||
|
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
|
||||||
|
# with the transport shadowed off would compile a role-isolation shim over no roles.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
|
||||||
|
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
|
||||||
|
set(MOBILEGL_BUILD_DISAGGREGATED ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||||
# a checkout without the submodule must configure and build, just without the
|
# a checkout without the submodule must configure and build, just without the
|
||||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||||
@@ -454,6 +480,24 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
|
|||||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||||
# was, so the next configure - with the submodule there - honours it.
|
# was, so the next configure - with the submodule there - honours it.
|
||||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||||
|
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
|
||||||
|
# would still be defined over a build with no MG_Remote in it at all.
|
||||||
|
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
|
||||||
|
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
|
||||||
|
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
|
||||||
|
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
|
||||||
|
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
|
||||||
|
# at the CMake level from a legitimate transport-only build, which is why it is stated here
|
||||||
|
# rather than left to whoever hits the link error. Same normal-variable form as the two
|
||||||
|
# implications above.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
|
||||||
|
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
|
||||||
|
"this configure: the split path decodes into the MGPipe applier, and the "
|
||||||
|
"applier is what MOBILEGL_PIPE_PUSH compiles")
|
||||||
|
set(MOBILEGL_PIPE_PUSH ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||||
@@ -464,11 +508,31 @@ if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
|||||||
set(MOBILEGL_PIPE_PUSH ON)
|
set(MOBILEGL_PIPE_PUSH ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||||
|
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||||
|
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||||
|
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||||
|
"configure: with nothing pushed it is the only arm there is")
|
||||||
|
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MOBILEGL_PIPE_PUSH)
|
if (MOBILEGL_PIPE_PUSH)
|
||||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||||
list(APPEND SOURCE_FILES
|
list(APPEND SOURCE_FILES
|
||||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||||
|
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||||
|
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||||
|
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||||
|
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||||
|
MobileGL/MG_Pipe/PipeApply.cpp
|
||||||
|
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||||
|
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||||
|
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||||
|
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||||
|
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||||
|
# where the applier serialises, deserialises and field-compares before storing.
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -488,6 +552,28 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||||
|
# ---- P5: the three new directories ------------------------------
|
||||||
|
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
|
||||||
|
# var-tails in and out of SEG_STAGE. [w1]
|
||||||
|
# Client/ the emitting role: session, the 69-slot emit table, the
|
||||||
|
# caps mirror. [c1]
|
||||||
|
# Server/ the applying role: session, the applier bridge onto the
|
||||||
|
# existing MGPipeApply* free functions, the apply thread. [v1]
|
||||||
|
#
|
||||||
|
# Every file below lands in P5 as a HEADER plus a .cpp of named
|
||||||
|
# Fatal stubs, so that all seven P5 packages compile and link on day
|
||||||
|
# one against signatures that cannot then move under them. A stub is
|
||||||
|
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
|
||||||
|
# emitter that returns quietly is how a split lane runs monolith and
|
||||||
|
# goes green (ARCHITECTURE.md 10.3).
|
||||||
|
MobileGL/MG_Remote/CapsCodec.cpp
|
||||||
|
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||||
|
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||||
|
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||||
|
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||||
|
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||||
|
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||||
|
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -551,6 +637,9 @@ endif()
|
|||||||
if (MOBILEGL_PIPE_VERIFY)
|
if (MOBILEGL_PIPE_VERIFY)
|
||||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||||
endif()
|
endif()
|
||||||
|
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||||
|
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||||
|
|
||||||
|
|||||||
+140
-5
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
|||||||
inline const String ProjectName = "MobileGL";
|
inline const String ProjectName = "MobileGL";
|
||||||
inline const String CoreName = "MobileGL Core";
|
inline const String CoreName = "MobileGL Core";
|
||||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
|
||||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||||
inline const Uint64 CacheVersion = 0;
|
inline const Uint64 CacheVersion = 0;
|
||||||
|
|
||||||
@@ -319,10 +319,34 @@ namespace MobileGL::MG_Config {
|
|||||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||||
// 0 - the default and the only shipped value until the migration lands - is "pull
|
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||||
// everything", i.e. exactly today's behaviour. One bit of it also turns OFF
|
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||||
// client-side content addressing of CSOs, which is the negative control the CSO
|
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||||
// design is measured against. Accepts decimal or 0x-prefixed hex.
|
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a), so MOBILEGL_PIPE_PUSH=0 in the
|
||||||
|
// environment is the all-pull control and 0x1ff (kMGPipeSubsystemsMigratedAtP3a) is
|
||||||
|
// the "everything before P4a" control P4a's A/B is run against - each phase's
|
||||||
|
// constant survives as the next phase's control, which is why none of them is ever
|
||||||
|
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
|
||||||
|
// bits are listed here (MG_Pipe/MGPipe.h owns them):
|
||||||
|
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
||||||
|
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||||
|
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||||
|
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||||
|
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||||
|
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||||
|
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||||
|
// set_texture_params) - requires 0x80 AND 0x800
|
||||||
|
// (the built-in sampler CSO a set_texture_params record names is minted by
|
||||||
|
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
|
||||||
|
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
|
||||||
|
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||||
|
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||||
|
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||||
|
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
|
||||||
|
// family runs its legacy arm; it is never half-run.
|
||||||
|
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||||
|
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||||
|
// never probed. The negative control the CSO design is measured against.
|
||||||
Uint64 PipePush = 0;
|
Uint64 PipePush = 0;
|
||||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||||
// against a snapshot taken from GLContext the old way, printing the first field
|
// against a snapshot taken from GLContext the old way, printing the first field
|
||||||
@@ -349,6 +373,18 @@ namespace MobileGL::MG_Config {
|
|||||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||||
// Fatal{PipeVerifyBadKnob}.
|
// Fatal{PipeVerifyBadKnob}.
|
||||||
String PipePoisonOmit;
|
String PipePoisonOmit;
|
||||||
|
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||||
|
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||||
|
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||||
|
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||||
|
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||||
|
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||||
|
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||||
|
// block is never handed back, so that spelling collided with nothing and the control
|
||||||
|
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||||
|
// measurement and for what the control still leaves standing. Under
|
||||||
|
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||||
|
Bool PipeHandleAbaControl = false;
|
||||||
#endif
|
#endif
|
||||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||||
@@ -378,4 +414,103 @@ namespace MobileGL::MG_Config {
|
|||||||
String PipeStatsFile;
|
String PipeStatsFile;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
|
||||||
|
//
|
||||||
|
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
|
||||||
|
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
|
||||||
|
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
|
||||||
|
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
|
||||||
|
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
|
||||||
|
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
|
||||||
|
// behind their own #if.
|
||||||
|
//
|
||||||
|
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
|
||||||
|
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
|
||||||
|
// parser below does not exist to complain about it, and putting a complaint in the
|
||||||
|
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
|
||||||
|
// shape of "the split lane ran monolith and went green", so the gate against it is a
|
||||||
|
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||||
|
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
|
||||||
|
// P5 adds beside build-linux-verify).
|
||||||
|
enum class TransportMode : Uint8 {
|
||||||
|
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
|
||||||
|
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
|
||||||
|
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
|
||||||
|
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
|
||||||
|
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||||
|
};
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
|
||||||
|
// transport in is not the same as using it, and every existing lane of a build-split
|
||||||
|
// must keep running monolith unless it is asked for one.
|
||||||
|
extern TransportMode Transport;
|
||||||
|
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
|
||||||
|
extern String TransportEndpoint;
|
||||||
|
|
||||||
|
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
|
||||||
|
// for the G1 reason above and because every field here is meaningless without the
|
||||||
|
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
|
||||||
|
//
|
||||||
|
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
|
||||||
|
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
|
||||||
|
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
|
||||||
|
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
|
||||||
|
// to P6 or later.
|
||||||
|
struct IpcTable {
|
||||||
|
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
|
||||||
|
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
|
||||||
|
// SPLIT variant already carry it, and an environment variable that nothing parses is
|
||||||
|
// indistinguishable from one that is parsed and ignored.
|
||||||
|
String ServerPath;
|
||||||
|
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
|
||||||
|
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
|
||||||
|
// codec publish a max-record-bytes counter rather than assume that is enough.
|
||||||
|
Uint32 RingMb = 8;
|
||||||
|
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
|
||||||
|
// here (R-10: no chunking in P5, so nothing may exceed it).
|
||||||
|
Uint32 StageMb = 32;
|
||||||
|
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
|
||||||
|
Uint32 SpinUs = 50;
|
||||||
|
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||||
|
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||||
|
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||||
|
Uint32 PersistentBlockKb = 64;
|
||||||
|
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||||
|
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||||
|
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||||
|
// negative control has a spelling the day P11 writes it.
|
||||||
|
Uint32 AdoptTier = 2;
|
||||||
|
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
|
||||||
|
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
|
||||||
|
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
|
||||||
|
// GLContext by the client's residual fill and a free-running queue lets the server
|
||||||
|
// read a FUTURE value of them.
|
||||||
|
Uint32 VerbBarrier = 1;
|
||||||
|
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
|
||||||
|
// build, the seven sticky forwards that are otherwise exempt - from "count it in
|
||||||
|
// rsp" to Fatal (R-7.3).
|
||||||
|
Bool StrictErrors = false;
|
||||||
|
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||||
|
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||||
|
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||||
|
Bool Audit = false;
|
||||||
|
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
|
||||||
|
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
|
||||||
|
// because the resolved mask is logged by whoever starts the apply thread, and the
|
||||||
|
// string is what an operator typed.
|
||||||
|
String ServerAffinity = "auto";
|
||||||
|
};
|
||||||
|
extern IpcTable Ipc;
|
||||||
|
#else
|
||||||
|
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||||
|
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||||
|
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||||
|
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||||
|
#endif
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// For kMGPipeSubsystemsMigratedAtP4a, the push build's PipePush default (the P2 and P3a
|
||||||
|
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||||
|
// pull build's translation unit is unchanged.
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -19,6 +25,15 @@ namespace MobileGL::MG_Config {
|
|||||||
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
|
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
|
||||||
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
|
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
|
||||||
FeaturesTable Features;
|
FeaturesTable Features;
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
|
||||||
|
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
|
||||||
|
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
|
||||||
|
// and there is nothing to define.
|
||||||
|
TransportMode Transport = TransportMode::Monolith;
|
||||||
|
String TransportEndpoint;
|
||||||
|
IpcTable Ipc;
|
||||||
|
#endif
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|
||||||
namespace MobileGL::MG_ConfigLoader {
|
namespace MobileGL::MG_ConfigLoader {
|
||||||
@@ -242,7 +257,18 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||||
|
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||||
|
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||||
|
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||||
|
// subsystems off, everything P3a landed still on.
|
||||||
|
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
|
||||||
|
#else
|
||||||
|
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||||
|
// "pull everything" and that stays literally true.
|
||||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||||
|
#endif
|
||||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||||
@@ -250,6 +276,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||||
|
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||||
#endif
|
#endif
|
||||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||||
@@ -279,12 +306,106 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
#undef ENTRY
|
#undef ENTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
|
||||||
|
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
|
||||||
|
// one fallback that names what it did instead. The two prefixed forms are the only
|
||||||
|
// reason this is not literally that function's ENTRY macro.
|
||||||
|
//
|
||||||
|
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
|
||||||
|
// rather than silent, because the failure this avoids is a P6 lane that set
|
||||||
|
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
|
||||||
|
// The mode is left at Monolith so nothing half-initializes.
|
||||||
|
inline void InitTransport() {
|
||||||
|
String value;
|
||||||
|
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||||
|
String lowered = value;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
|
||||||
|
MG_Config::TransportEndpoint.clear();
|
||||||
|
if (lowered.empty() || lowered == "monolith") {
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lowered == "inproc") {
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
|
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
|
||||||
|
"ring to an apply thread");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||||
|
// rather than "unknown", which are different bugs on the operator's side.
|
||||||
|
if (lowered == "spawn" || lowered.compare(0, 5, "unix:") == 0 ||
|
||||||
|
lowered.compare(0, 5, "pipe:") == 0) {
|
||||||
|
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 implements and P5 does "
|
||||||
|
"not; staying on monolith. This run is NOT a split run.",
|
||||||
|
value.c_str());
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
|
||||||
|
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
|
||||||
|
value.c_str());
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
|
||||||
|
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
|
||||||
|
// how an A/B stops being one, and the ranges below are the diagnostics.
|
||||||
|
inline void InitIpc() {
|
||||||
|
auto& ipc = MG_Config::Ipc;
|
||||||
|
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
|
||||||
|
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
|
||||||
|
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
|
||||||
|
// create_shader_state archive already needs far more than a toy ring. The ceilings
|
||||||
|
// are sanity, not policy.
|
||||||
|
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
|
||||||
|
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
|
||||||
|
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
|
||||||
|
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||||
|
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||||
|
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||||
|
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
|
||||||
|
// point of use, which is where the "P11" in the message belongs.
|
||||||
|
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
|
||||||
|
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
|
||||||
|
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||||
|
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||||
|
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||||
|
|
||||||
|
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||||
|
// One line, on the arm where these numbers decide behaviour, because every one of
|
||||||
|
// them is a number a bug report has to quote.
|
||||||
|
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB "
|
||||||
|
"adopt-tier=%u verb-barrier=%u strict=%d audit=%d affinity='%s'",
|
||||||
|
ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier,
|
||||||
|
ipc.VerbBarrier, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||||
|
ipc.ServerAffinity.c_str());
|
||||||
|
if (ipc.VerbBarrier == 0) {
|
||||||
|
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
|
||||||
|
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
|
||||||
|
"live GLContext, so an unbarriered queue lets the server read future values");
|
||||||
|
}
|
||||||
|
if (ipc.PersistentBlockKb == 0) {
|
||||||
|
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
|
||||||
|
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Init() {
|
void Init() {
|
||||||
MGLOG_D("Loading configuration from environment variables...");
|
MGLOG_D("Loading configuration from environment variables...");
|
||||||
InitializeAcceptedEnvVariables();
|
InitializeAcceptedEnvVariables();
|
||||||
|
|
||||||
InitBackendType();
|
InitBackendType();
|
||||||
InitFeatures();
|
InitFeatures();
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
|
||||||
|
// configuration, and before the accepted-env map is destroyed just below.
|
||||||
|
InitTransport();
|
||||||
|
InitIpc();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Destroy the map since we won't need it anymore
|
// Destroy the map since we won't need it anymore
|
||||||
acceptedEnvVariablesMap.reset();
|
acceptedEnvVariablesMap.reset();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,577 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectGLES/SlotTables.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
|
||||||
|
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
|
||||||
|
//
|
||||||
|
// What changes, and why each of them is the point:
|
||||||
|
//
|
||||||
|
// * The KEY stops being a frontend heap address. It is MGPipeHandle{Slot, Gen}, minted by the
|
||||||
|
// client's MGPipeSlotAllocator off the frontend object's GetLifetimeId(). A recycled heap
|
||||||
|
// address cannot reproduce a handle, so the weak_ptr the registry carried per entry purely
|
||||||
|
// to catch that (its Entry::stateRef, used as an IDENTITY test) stops being an identity
|
||||||
|
// mechanism, and OwnerEquals / TwinLookupMemo x3 / UnitSamplerLookupMemo's owner compare all
|
||||||
|
// lose their reason to exist.
|
||||||
|
// * The lookup stops being a hash probe into an open-addressed map and becomes one bounds
|
||||||
|
// check plus one array index, so a returned BackendPtr* is NOT invalidated by the next Find
|
||||||
|
// on the table. That kills the hazard Managers.h documents at length, and with it the
|
||||||
|
// by-value copy plus second Find that SyncTextureObjectToBackend paid to survive it.
|
||||||
|
// * Slots are dense per kind, which is what lets the server side (ARCHITECTURE.md 10.1,
|
||||||
|
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
|
||||||
|
//
|
||||||
|
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
|
||||||
|
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
|
||||||
|
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
|
||||||
|
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
|
||||||
|
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
|
||||||
|
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
|
||||||
|
// goes. So:
|
||||||
|
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
|
||||||
|
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
|
||||||
|
// * a twin, and the driver storage it owns, is freed when the application lets go of the
|
||||||
|
// object rather than up to 64 creations or 1024 draw ticks later. That is what
|
||||||
|
// Managers.h's "dead gigabytes" note asked for.
|
||||||
|
//
|
||||||
|
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
|
||||||
|
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
|
||||||
|
// length of a test, and a context reset does the same in reverse - and the slot allocator
|
||||||
|
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
|
||||||
|
// resolved again by the next would find nothing to resolve. Every table therefore links itself
|
||||||
|
// into a per-table-type list at construction and out at destruction, and one notice resolves
|
||||||
|
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
|
||||||
|
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
|
||||||
|
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
|
||||||
|
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
|
||||||
|
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
|
||||||
|
//
|
||||||
|
// The weak_ptr per entry survives for exactly one reason: ForEachLive() hands the callee a
|
||||||
|
// STRONG reference to the frontend object, which the one direct-iteration site
|
||||||
|
// (ScopedDetachedTextureFramebufferAttachments) needs. It is never an identity test - that is
|
||||||
|
// what Gen is for - and it is never read to decide whether an entry is dead: a destructor that
|
||||||
|
// runs after exit() has begun has its notice dropped by InProcessTeardown(), and that twin is
|
||||||
|
// then a DELIBERATE leak (the process is exiting, the driver reclaims the object, and a twin
|
||||||
|
// destructor must not call into a driver that may already be unloaded), not something to be
|
||||||
|
// collected later.
|
||||||
|
//
|
||||||
|
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
|
||||||
|
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
|
||||||
|
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
|
||||||
|
// under a real split neither the frontend object nor its lifetime id exists on this side of
|
||||||
|
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
|
||||||
|
// belong on the client, and the backend should receive the handle in the verb payload. It is
|
||||||
|
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
|
||||||
|
// so nothing catches it automatically.
|
||||||
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
// Declared in Managers.h as well; repeated here because this header is included from it
|
||||||
|
// before that declaration, and the table below is the arming site on this arm (D13: "the
|
||||||
|
// arming site moves to the slot table's first insertion").
|
||||||
|
void EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
// What the two knobs add up to. Split out as a PURE function of them so a test can drive
|
||||||
|
// every combination without needing a process per combination.
|
||||||
|
enum class EsprytSlotArmVerdict {
|
||||||
|
Handles, // kMGPipeSubsystemEsprytSlots is set: the {slot, gen} tables run.
|
||||||
|
Legacy, // the bit is clear and the legacy address-keyed registry is reachable.
|
||||||
|
NoArm, // the bit is clear AND MOBILEGL_PIPE_LEGACY_MEMOS=0 made the legacy arm
|
||||||
|
// unreachable, so the operator asked for a configuration with no arm at all.
|
||||||
|
};
|
||||||
|
|
||||||
|
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled);
|
||||||
|
|
||||||
|
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
|
||||||
|
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
|
||||||
|
|
||||||
|
// Says, at backend bring-up, that the knobs leave no arm - and does NOT stop.
|
||||||
|
//
|
||||||
|
// The stop cannot live here, and that is the whole point of the split. Backend context
|
||||||
|
// creation runs inside eglMakeCurrent, and the integration harness pre-flights exactly that
|
||||||
|
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
|
||||||
|
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
|
||||||
|
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
|
||||||
|
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
|
||||||
|
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
|
||||||
|
// which happens in the test body where the harness reports it as a failure.
|
||||||
|
//
|
||||||
|
// The CALL SITE (InitDisplayAndContext in DirectGLES.cpp) is pinned by
|
||||||
|
// DirectGLESSlotTable.EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs
|
||||||
|
// the real bring-up entry point under the pair in a forked child: edit that site back to
|
||||||
|
// ResolveEsprytSlotTablesArm() and the case fails naming both knobs.
|
||||||
|
void DiagnoseEsprytSlotArm();
|
||||||
|
|
||||||
|
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
|
||||||
|
// left no arm at all. Cold: called exactly once per process, from the latch below - i.e. at
|
||||||
|
// the first twin lookup, which is the first moment an arm is actually needed. A process
|
||||||
|
// that never twins anything needs no arm and is not stopped.
|
||||||
|
Bool ResolveEsprytSlotTablesArm();
|
||||||
|
|
||||||
|
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
|
||||||
|
// two arms hold their twins in different containers, so flipping mid-run would strand them.
|
||||||
|
//
|
||||||
|
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
|
||||||
|
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
|
||||||
|
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
|
||||||
|
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
|
||||||
|
// branch, and the arm dispatch folds into the caller.
|
||||||
|
inline Bool EsprytSlotTablesEnabled() {
|
||||||
|
static const Bool enabled = ResolveEsprytSlotTablesArm();
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||||
|
class BackendSlotTable {
|
||||||
|
public:
|
||||||
|
using StatePtr = SharedPtr<StateObject>;
|
||||||
|
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||||
|
using BackendPtr = SharedPtr<BackendObject>;
|
||||||
|
|
||||||
|
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
|
||||||
|
// payload. Slots are dense and allocated per kind, so a million of one kind is already
|
||||||
|
// far past any application's live object count; the cap is here because the alternative
|
||||||
|
// is letting a corrupt 32-bit slot decide a vector resize. See GetOrCreate(MGPipeHandle).
|
||||||
|
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
BackendPtr backend;
|
||||||
|
// LIVENESS ONLY, and only for ForEachLive(), which locks it so the callee holds a
|
||||||
|
// strong ref. Never compared against another object to decide identity - that is
|
||||||
|
// what Gen is for - never dereferenced for its address, and never read to decide
|
||||||
|
// whether the slot is dead: death is announced, not discovered.
|
||||||
|
StateWeakPtr stateRef;
|
||||||
|
// The generation this entry's twin was built for. An entry whose Gen no longer
|
||||||
|
// matches the allocator's is a twin of the slot's PREVIOUS owner.
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Bool Live = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every constructor links the table into the per-type holder list and the destructor
|
||||||
|
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
|
||||||
|
// registry) is a holder for exactly as long as it exists. Copy and move carry the
|
||||||
|
// ENTRIES and the memo; the links are the table's own and are never copied.
|
||||||
|
BackendSlotTable() { LinkHolder(); }
|
||||||
|
BackendSlotTable(const BackendSlotTable& other):
|
||||||
|
m_slots(other.m_slots),
|
||||||
|
m_nullTwin(other.m_nullTwin),
|
||||||
|
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||||
|
m_memoHandle(other.m_memoHandle) {
|
||||||
|
LinkHolder();
|
||||||
|
}
|
||||||
|
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||||
|
m_slots(std::move(other.m_slots)),
|
||||||
|
m_nullTwin(std::move(other.m_nullTwin)),
|
||||||
|
m_memoLifetimeId(other.m_memoLifetimeId),
|
||||||
|
m_memoHandle(other.m_memoHandle) {
|
||||||
|
other.m_slots.clear();
|
||||||
|
other.ForgetHandle();
|
||||||
|
LinkHolder();
|
||||||
|
}
|
||||||
|
BackendSlotTable& operator=(const BackendSlotTable& other) {
|
||||||
|
if (this != &other) {
|
||||||
|
m_slots = other.m_slots;
|
||||||
|
m_nullTwin = other.m_nullTwin;
|
||||||
|
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||||
|
m_memoHandle = other.m_memoHandle;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
BackendSlotTable& operator=(BackendSlotTable&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
m_slots = std::move(other.m_slots);
|
||||||
|
m_nullTwin = std::move(other.m_nullTwin);
|
||||||
|
m_memoLifetimeId = other.m_memoLifetimeId;
|
||||||
|
m_memoHandle = other.m_memoHandle;
|
||||||
|
other.m_slots.clear();
|
||||||
|
other.ForgetHandle();
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
~BackendSlotTable() { UnlinkHolder(); }
|
||||||
|
|
||||||
|
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
|
||||||
|
// object's lifetime id, so two calls for the same live object always land on the same
|
||||||
|
// slot, and a successor object at the same heap address never does.
|
||||||
|
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||||
|
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
|
||||||
|
// must not trap where the release build quietly does the documented thing.
|
||||||
|
if (stateObj == nullptr) {
|
||||||
|
// The registry this replaces inserted a null KEY and handed back that entry's
|
||||||
|
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
|
||||||
|
// exactly that tolerance), so a release build never dereferenced null here.
|
||||||
|
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
|
||||||
|
// entry, so a second null call was handed the same twin the first one got.
|
||||||
|
// Resetting here instead would have destroyed it - an arm difference in the one
|
||||||
|
// path that documents relying on this. One per-table parking slot, never live,
|
||||||
|
// never handed a handle, because a null object has no identity and
|
||||||
|
// therefore cannot have a {slot, gen}.
|
||||||
|
return m_nullTwin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
|
||||||
|
// creation is the moment a driver-owned id starts needing a guarded destructor;
|
||||||
|
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
|
||||||
|
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
|
||||||
|
EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
|
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
|
||||||
|
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||||
|
"MGPipe slot space of kind %u is exhausted",
|
||||||
|
static_cast<Uint32>(kKind));
|
||||||
|
Entry& entry = EntryAt(handle.Slot);
|
||||||
|
if (entry.Live && entry.Gen != handle.Gen) {
|
||||||
|
// The slot was reclaimed and handed to a new object: the twin at it describes
|
||||||
|
// driver ids the new state object never made.
|
||||||
|
entry.backend.reset();
|
||||||
|
}
|
||||||
|
entry.Gen = handle.Gen;
|
||||||
|
entry.Live = true;
|
||||||
|
entry.stateRef = stateObj;
|
||||||
|
// No creation tick and no sweep here. The registry this replaces needed both,
|
||||||
|
// because nothing told it a texture or a renderbuffer had been DELETED and object
|
||||||
|
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
|
||||||
|
// now announces its own death from its destructor, so a dead twin's slot is already
|
||||||
|
// back before the next creation asks for one.
|
||||||
|
RememberHandle(stateObj->GetLifetimeId(), handle);
|
||||||
|
return entry.backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
|
||||||
|
// header records against itself at the top of the file.
|
||||||
|
//
|
||||||
|
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
|
||||||
|
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
|
||||||
|
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
|
||||||
|
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
|
||||||
|
// the call's payload, already minted by the side that owns minting; all this does is
|
||||||
|
// index the slot, notice a generation that no longer matches (the slot was recycled,
|
||||||
|
// so the twin at it describes driver ids the new resource never made) and hand back
|
||||||
|
// the twin pointer. FindByHandle beside it is the same shape and already existed.
|
||||||
|
//
|
||||||
|
// No StatePtr, therefore no Entry::stateRef: the weak pointer is liveness for
|
||||||
|
// ForEachLive() and a handle-keyed entry has no frontend object to weakly hold. Such
|
||||||
|
// an entry is therefore invisible to ForEachLive, which is correct - the one direct
|
||||||
|
// iteration site walks texture twins, and it is not one of these tables.
|
||||||
|
//
|
||||||
|
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
|
||||||
|
// announcement is the family's own destroy call, not the shared death notice, and the
|
||||||
|
// slot is freed by the CLIENT after that call returns.
|
||||||
|
//
|
||||||
|
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
|
||||||
|
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
|
||||||
|
// it its first caller.
|
||||||
|
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||||
|
"GetOrCreate(handle) named the reserved null handle");
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
|
||||||
|
|
||||||
|
// A slot index that ARRIVED in a payload indexes a vector this call would RESIZE,
|
||||||
|
// and nothing between the payload and here bounds it: the applier's blob gates sit
|
||||||
|
// in front of the vertex-input family, not in front of the resource family, which
|
||||||
|
// dispatches ops->Create(record.Res, ...) straight through. There is no allocator
|
||||||
|
// constant to check against on this side - the allocator is the client's - so this
|
||||||
|
// is a sanity cap and is documented as one: kMaxHandleSlot entries of one kind is
|
||||||
|
// already orders of magnitude past any real GL object count, while a corrupt 32-bit
|
||||||
|
// slot asks for a four-billion-entry resize.
|
||||||
|
if (handle.Slot >= kMaxHandleSlot) {
|
||||||
|
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
|
||||||
|
handle.Slot, kMaxHandleSlot);
|
||||||
|
return m_nullTwin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same arming as the minting overload, and for the same reason: twin creation is
|
||||||
|
// the moment a driver-owned id starts needing a guarded destructor.
|
||||||
|
EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
|
||||||
|
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
|
||||||
|
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
|
||||||
|
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
|
||||||
|
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
|
||||||
|
// a persistent map, a pooled store, released by a defaulted destructor that issues
|
||||||
|
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
|
||||||
|
// dead resource's generation, after which the incumbent's own FindByHandle refuses
|
||||||
|
// it and it is silently handed a fresh, empty twin. That is a leak AND a resource
|
||||||
|
// that loses its storage with no diagnostic, i.e. the shape commit d7655247 fixed
|
||||||
|
// and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and
|
||||||
|
// resets the twin, BACKWARD is refused - which is the same answer FindByHandle
|
||||||
|
// below already gives the same input.
|
||||||
|
Entry& entry = EntryAt(handle.Slot);
|
||||||
|
if (entry.Live && entry.Gen > handle.Gen) {
|
||||||
|
MOBILEGL_ASSERT(false,
|
||||||
|
"GetOrCreate(handle) named generation %u at slot %u, which is BEHIND "
|
||||||
|
"the live entry's %u - refusing rather than destroying the incumbent",
|
||||||
|
handle.Gen, handle.Slot, entry.Gen);
|
||||||
|
return m_nullTwin;
|
||||||
|
}
|
||||||
|
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
|
||||||
|
entry.Gen = handle.Gen;
|
||||||
|
entry.Live = true;
|
||||||
|
return entry.backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The generation of the LIVE entry at this slot, or 0 when the slot is out of range or
|
||||||
|
// holds no live entry. It exists so a caller can DIAGNOSE - in a release build, where
|
||||||
|
// MOBILEGL_ASSERT is inert - the refusal GetOrCreate(handle) above performs silently.
|
||||||
|
Uint32 LiveGenAt(Uint32 slot) const {
|
||||||
|
if (slot >= m_slots.size()) return 0;
|
||||||
|
const Entry& entry = m_slots[slot];
|
||||||
|
return entry.Live ? entry.Gen : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P3a: the death half of the overload above, for a kind whose announcement is its own
|
||||||
|
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
|
||||||
|
// than destroying it in place, because the caller may still have to decide what
|
||||||
|
// happens to the driver id it owns - Espryt pools it, deletes it, or parks it on the
|
||||||
|
// deferred-release list when no context is current on this thread - and every one of
|
||||||
|
// those outcomes has to be reached with the entry already retired, so a re-entrant
|
||||||
|
// GetOrCreate from a twin destructor cannot resurrect it.
|
||||||
|
//
|
||||||
|
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
|
||||||
|
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
|
||||||
|
// Gen bump rides the next handout, so a double free cannot skip a generation). An
|
||||||
|
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
|
||||||
|
// left alone: the successor's own GetOrCreate resets it.
|
||||||
|
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
|
||||||
|
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||||
|
if (handle.Slot >= m_slots.size()) return BackendPtr{};
|
||||||
|
Entry& entry = m_slots[handle.Slot];
|
||||||
|
if (!entry.Live || entry.Gen != handle.Gen) return BackendPtr{};
|
||||||
|
BackendPtr dead = std::move(entry.backend);
|
||||||
|
entry.backend.reset();
|
||||||
|
entry.stateRef.reset();
|
||||||
|
entry.Live = false;
|
||||||
|
return dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
|
||||||
|
// mutates the table, so the returned pointer survives any later Find on it; only a
|
||||||
|
// GetOrCreate that grows the vector can move it, and callers that hold one across a
|
||||||
|
// possible insertion still copy the BackendPtr out.
|
||||||
|
BackendPtr* Find(StateObject* stateObj) {
|
||||||
|
if (stateObj == nullptr) return nullptr;
|
||||||
|
return FindByHandle(HandleOf(stateObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackendPtr* Find(StateObject* stateObj) const {
|
||||||
|
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
|
||||||
|
if (handle.Slot >= m_slots.size()) return nullptr;
|
||||||
|
Entry& entry = m_slots[handle.Slot];
|
||||||
|
if (!entry.Live || entry.Gen != handle.Gen) return nullptr;
|
||||||
|
return &entry.backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handle this object's twin is keyed on, or the null handle. This is what a backend
|
||||||
|
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
|
||||||
|
//
|
||||||
|
// A NULL answer is never memoised. The memo is per table and the allocator is per
|
||||||
|
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
|
||||||
|
// a cached "no handle" here would then outlive the twin's creation over there, and
|
||||||
|
// nothing on this table's own acquire path would ever refresh it. A miss costs the
|
||||||
|
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
|
||||||
|
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||||
|
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
|
||||||
|
const Uint64 lifetimeId = stateObj->GetLifetimeId();
|
||||||
|
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
|
||||||
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
|
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||||
|
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2 step e2's backend half. The frontend object with this lifetime id has just been
|
||||||
|
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
|
||||||
|
// return the slot to the allocator - in that order, because the allocator forgets the
|
||||||
|
// lifetime id on Free and a holder told second could no longer resolve it.
|
||||||
|
//
|
||||||
|
// The slot is returned whether or not any holder still had a twin at it: the lifetime
|
||||||
|
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
|
||||||
|
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
|
||||||
|
// entries without freeing) would otherwise stay allocated for the life of the process.
|
||||||
|
//
|
||||||
|
// STATIC, and deliberately so: a notice is about an object, not about a table, and
|
||||||
|
// "which table holds it" is exactly the question that produced the two-holder leak.
|
||||||
|
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
|
||||||
|
// a second call for the same id answers false because the allocator no longer maps it.
|
||||||
|
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||||
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
|
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||||
|
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||||
|
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
|
||||||
|
// destructor, which is a driver call, and nothing that outlives it may be a
|
||||||
|
// reference into this holder.
|
||||||
|
BackendSlotTable* const next = holder->m_nextHolder;
|
||||||
|
holder->ReleaseTwinAt(handle);
|
||||||
|
holder = next;
|
||||||
|
}
|
||||||
|
MG_Pipe::MGPipeSlots().Free(kKind, handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many tables of this type exist right now. For the tests that pin the holder
|
||||||
|
// list; nothing on a shipping path asks.
|
||||||
|
static Uint32 HolderCount() {
|
||||||
|
Uint32 count = 0;
|
||||||
|
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr;
|
||||||
|
holder = holder->m_nextHolder) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn(const StatePtr& state, const BackendPtr& twin) over every live, still-owned entry.
|
||||||
|
// Replaces the registry's begin()/end(), whose iterator exposed the raw frontend
|
||||||
|
// address as the map key - the one place the backend read an identity it must not have.
|
||||||
|
// The state object is handed over as a STRONG reference, so the callee cannot be handed
|
||||||
|
// a dangling key the way the old iteration could.
|
||||||
|
template <typename Fn>
|
||||||
|
void ForEachLive(Fn&& fn) const {
|
||||||
|
// Index loop and a COPIED twin, not a range-for over references: fn is arbitrary
|
||||||
|
// backend code, and a nested GetOrCreate on this table would resize m_slots and
|
||||||
|
// invalidate both the iterator and any reference into the vector that outlives the
|
||||||
|
// call. The one caller today happens not to insert; that is not a property the
|
||||||
|
// walk should depend on.
|
||||||
|
for (SizeT slot = 0; slot < m_slots.size(); ++slot) {
|
||||||
|
const Entry& entry = m_slots[slot];
|
||||||
|
if (!entry.Live || !entry.backend) continue;
|
||||||
|
const StatePtr state = entry.stateRef.lock();
|
||||||
|
if (!state) continue;
|
||||||
|
const BackendPtr twin = entry.backend;
|
||||||
|
fn(state, twin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 LiveCount() const {
|
||||||
|
Uint32 count = 0;
|
||||||
|
for (const Entry& entry : m_slots) {
|
||||||
|
if (entry.Live) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
|
||||||
|
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
|
||||||
|
// every holder has let go.
|
||||||
|
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
// Forget the memo whenever it names this slot, even if this table has no entry
|
||||||
|
// there: a memo can be a handle learned from the allocator for an object another
|
||||||
|
// holder twinned, and it must not survive the slot's next handout.
|
||||||
|
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
|
||||||
|
if (handle.Slot >= m_slots.size()) return false;
|
||||||
|
// The twin's destructor is a driver call and could, in principle, re-enter
|
||||||
|
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
|
||||||
|
// destructor may be a reference into m_slots: the twin is moved out into a local,
|
||||||
|
// the entry is finished with, and only then is the local released.
|
||||||
|
BackendPtr dead;
|
||||||
|
{
|
||||||
|
Entry& entry = m_slots[handle.Slot];
|
||||||
|
if (!entry.Live || entry.Gen != handle.Gen) return false;
|
||||||
|
dead = std::move(entry.backend);
|
||||||
|
entry.backend.reset();
|
||||||
|
entry.stateRef.reset();
|
||||||
|
entry.Live = false;
|
||||||
|
}
|
||||||
|
dead.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grows the table to hold `slot`. Every caller bounds `slot` first - the minting
|
||||||
|
// overload because the allocator produced it, the handle overload against
|
||||||
|
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
|
||||||
|
// allocation size.
|
||||||
|
Entry& EntryAt(Uint32 slot) {
|
||||||
|
if (slot >= m_slots.size()) m_slots.resize(static_cast<SizeT>(slot) + 1);
|
||||||
|
return m_slots[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
|
||||||
|
m_memoLifetimeId = lifetimeId;
|
||||||
|
m_memoHandle = handle;
|
||||||
|
}
|
||||||
|
void ForgetHandle() const {
|
||||||
|
m_memoLifetimeId = 0;
|
||||||
|
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The holder list: intrusive and doubly linked, so registering and unregistering are
|
||||||
|
// two pointer writes with no allocation, and its head is a constant-initialised
|
||||||
|
// static - which is what lets the process-lifetime registry globals in Managers.cpp
|
||||||
|
// link themselves in from their own constructors with no initialisation-order
|
||||||
|
// question to answer. Single-threaded, like every table it links (the tables live and
|
||||||
|
// die on the context thread, as the notice they answer does).
|
||||||
|
void LinkHolder() {
|
||||||
|
m_prevHolder = nullptr;
|
||||||
|
m_nextHolder = s_firstHolder;
|
||||||
|
if (s_firstHolder != nullptr) s_firstHolder->m_prevHolder = this;
|
||||||
|
s_firstHolder = this;
|
||||||
|
}
|
||||||
|
void UnlinkHolder() {
|
||||||
|
if (m_prevHolder != nullptr) {
|
||||||
|
m_prevHolder->m_nextHolder = m_nextHolder;
|
||||||
|
} else {
|
||||||
|
s_firstHolder = m_nextHolder;
|
||||||
|
}
|
||||||
|
if (m_nextHolder != nullptr) m_nextHolder->m_prevHolder = m_prevHolder;
|
||||||
|
m_prevHolder = nullptr;
|
||||||
|
m_nextHolder = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline BackendSlotTable* s_firstHolder = nullptr;
|
||||||
|
BackendSlotTable* m_prevHolder = nullptr;
|
||||||
|
BackendSlotTable* m_nextHolder = nullptr;
|
||||||
|
|
||||||
|
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
|
||||||
|
Vector<Entry> m_slots;
|
||||||
|
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
|
||||||
|
BackendPtr m_nullTwin;
|
||||||
|
|
||||||
|
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
|
||||||
|
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
|
||||||
|
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
|
||||||
|
// indexing".
|
||||||
|
//
|
||||||
|
// It is one entry and therefore only helps a caller that asks for the SAME object twice
|
||||||
|
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
|
||||||
|
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
|
||||||
|
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
|
||||||
|
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
|
||||||
|
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
|
||||||
|
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
|
||||||
|
//
|
||||||
|
// It cannot serve a stale answer, by three independent arguments:
|
||||||
|
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
|
||||||
|
// heap address cannot hit this memo the way it could hit an address-keyed one;
|
||||||
|
// * a null answer is never stored, so another holder's acquire cannot be hidden by
|
||||||
|
// a "no handle" this table remembered earlier; and
|
||||||
|
// * even a hit for a slot that has since been freed and re-handed is caught, because
|
||||||
|
// the caller resolves the handle through FindByHandle, which compares Gen.
|
||||||
|
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
|
||||||
|
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
|
||||||
|
mutable Uint64 m_memoLifetimeId = 0;
|
||||||
|
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
||||||
|
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
||||||
|
// graph is unchanged (G1).
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||||
|
// {slot, gen} mint the re-keyed sites are written against.
|
||||||
|
//
|
||||||
|
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||||
|
//
|
||||||
|
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
|
||||||
|
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
|
||||||
|
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
|
||||||
|
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
|
||||||
|
//
|
||||||
|
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
|
||||||
|
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
|
||||||
|
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
|
||||||
|
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
|
||||||
|
//
|
||||||
|
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
|
||||||
|
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
|
||||||
|
// nothing outside Track H consults the legacy-memo lever at all - see
|
||||||
|
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
|
||||||
|
//
|
||||||
|
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
|
||||||
|
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
||||||
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
||||||
|
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
||||||
|
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D14's startup gate
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
|
||||||
|
// that is actually running. It answers exactly one question and it answers it before the
|
||||||
|
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
|
||||||
|
//
|
||||||
|
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
|
||||||
|
//
|
||||||
|
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
|
||||||
|
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
||||||
|
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
||||||
|
// not kill an Espryt one.
|
||||||
|
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
|
||||||
|
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
|
||||||
|
// a clear bit there simply means the client is not pushing render-state CSOs in this
|
||||||
|
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
||||||
|
// and 6 "Track H" and labels bit 0 nothing of the sort.
|
||||||
|
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
||||||
|
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
||||||
|
// branch nobody needs on the hottest path in the backend.
|
||||||
|
//
|
||||||
|
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
|
||||||
|
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
|
||||||
|
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
|
||||||
|
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
|
||||||
|
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
|
||||||
|
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
|
||||||
|
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
||||||
|
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
||||||
|
inline void MagmaPipeValidateSubsystemConfiguration() {
|
||||||
|
if (!MG_Config::Features.PipeLegacyMemos &&
|
||||||
|
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||||
|
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
||||||
|
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
||||||
|
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
||||||
|
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
|
||||||
|
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
|
||||||
|
"to measure it.",
|
||||||
|
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||||
|
MG_Pipe::kMGPipeSubsystemRenderState));
|
||||||
|
}
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
||||||
|
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||||
|
if (MG_Config::Features.PipeLegacyMemos) return;
|
||||||
|
#endif
|
||||||
|
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
||||||
|
#else
|
||||||
|
const char* const why =
|
||||||
|
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
||||||
|
#endif
|
||||||
|
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
||||||
|
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
||||||
|
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
||||||
|
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
|
||||||
|
why,
|
||||||
|
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||||
|
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
|
||||||
|
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||||
|
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||||
|
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
return MagmaPipeSubsystemOn(trackHBit);
|
||||||
|
#else
|
||||||
|
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||||
|
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||||
|
// running process can be on.
|
||||||
|
(void)trackHBit;
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||||
|
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||||
|
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||||
|
// and a control that defeats two of three guards proves nothing.
|
||||||
|
//
|
||||||
|
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||||
|
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||||
|
//
|
||||||
|
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||||
|
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||||
|
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||||
|
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||||
|
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||||
|
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||||
|
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||||
|
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||||
|
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||||
|
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||||
|
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||||
|
//
|
||||||
|
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||||
|
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||||
|
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||||
|
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||||
|
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||||
|
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||||
|
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||||
|
// which is exactly the vacuity this control exists to catch.
|
||||||
|
//
|
||||||
|
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||||
|
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||||
|
//
|
||||||
|
// * this mint has no death notification - nothing in MG_Backend/DirectVulkan consumes
|
||||||
|
// NotifyStateObjectDestroyed - so a slot returns to the free list only through
|
||||||
|
// OnFrameBoundary's age sweep (kSweepInterval 256, kRetireAgeBoundaries 1024, below);
|
||||||
|
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||||
|
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||||
|
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||||
|
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||||
|
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||||
|
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||||
|
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||||
|
// across frames by design. The two requirements are mutually exclusive, so the
|
||||||
|
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||||
|
//
|
||||||
|
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||||
|
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||||
|
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||||
|
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||||
|
// suite; it is the only place in the tree where that deletion is caught.
|
||||||
|
//
|
||||||
|
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||||
|
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||||
|
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||||
|
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||||
|
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||||
|
// "every safety net was switched off until something broke".
|
||||||
|
//
|
||||||
|
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||||
|
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||||
|
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||||
|
//
|
||||||
|
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||||
|
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||||
|
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||||
|
// RETIRED says nothing about the key that phase SHIPS.
|
||||||
|
//
|
||||||
|
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||||
|
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||||
|
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||||
|
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||||
|
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||||
|
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||||
|
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||||
|
// conclude the knob covers its kind:
|
||||||
|
//
|
||||||
|
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||||
|
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||||
|
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||||
|
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||||
|
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||||
|
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||||
|
// on this tree can produce, which would be a permanently red always-on lane.
|
||||||
|
//
|
||||||
|
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||||
|
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||||
|
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||||
|
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||||
|
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||||
|
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||||
|
return MG_Config::Features.PipeHandleAbaControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||||
|
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||||
|
//
|
||||||
|
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||||
|
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||||
|
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||||
|
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||||
|
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||||
|
// and with no `default:` there is no arm for it to read true from.
|
||||||
|
//
|
||||||
|
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||||
|
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||||
|
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||||
|
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||||
|
// and could not make anything red or green.)
|
||||||
|
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
// The two MagmaPipeIdentityTables really mints.
|
||||||
|
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||||
|
case MG_Pipe::MGPipeKind::Buffer:
|
||||||
|
return true;
|
||||||
|
// P4a's six object classes: still reached from their frontend objects on this
|
||||||
|
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||||
|
// the knob to defeat.
|
||||||
|
case MG_Pipe::MGPipeKind::Texture:
|
||||||
|
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||||
|
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||||
|
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||||
|
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||||
|
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||||
|
// ...and everything else this backend does not mint a handle for.
|
||||||
|
case MG_Pipe::MGPipeKind::None:
|
||||||
|
case MG_Pipe::MGPipeKind::Xfb:
|
||||||
|
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||||
|
case MG_Pipe::MGPipeKind::Fence:
|
||||||
|
case MG_Pipe::MGPipeKind::Query:
|
||||||
|
case MG_Pipe::MGPipeKind::Context:
|
||||||
|
case MG_Pipe::MGPipeKind::KindCount:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||||
|
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||||
|
"defeat for it");
|
||||||
|
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||||
|
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||||
|
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||||
|
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||||
|
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||||
|
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||||
|
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||||
|
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||||
|
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||||
|
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||||
|
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||||
|
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||||
|
"carries the answer into the lane");
|
||||||
|
|
||||||
|
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||||
|
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||||
|
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||||
|
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||||
|
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||||
|
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||||
|
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||||
|
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||||
|
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||||
|
//
|
||||||
|
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||||
|
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||||
|
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||||
|
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||||
|
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||||
|
|
||||||
|
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||||
|
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||||
|
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||||
|
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The {slot, gen} mint
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||||
|
// properties, and the third is the one review v2 got wrong:
|
||||||
|
//
|
||||||
|
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||||
|
// never match a live object even if the allocator hands back the same heap address
|
||||||
|
// (the ABA HandleRecycleScenario reproduces);
|
||||||
|
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||||
|
// no probe and no mix;
|
||||||
|
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||||
|
// drawn, whatever the working set size.
|
||||||
|
//
|
||||||
|
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||||
|
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||||
|
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||||
|
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||||
|
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||||
|
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||||
|
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||||
|
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||||
|
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||||
|
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||||
|
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||||
|
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||||
|
//
|
||||||
|
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||||
|
//
|
||||||
|
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||||
|
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||||
|
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||||
|
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||||
|
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||||
|
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||||
|
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||||
|
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||||
|
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||||
|
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||||
|
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||||
|
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||||
|
// plus one map node per object EVER created, for the life of the process, on a
|
||||||
|
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||||
|
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||||
|
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||||
|
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||||
|
// ~1024 frames, never to a hot one.
|
||||||
|
//
|
||||||
|
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||||
|
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||||
|
// no capacity to size off a device measurement because there is no capacity; what the
|
||||||
|
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||||
|
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||||
|
// draw - ROADMAP.md:7).
|
||||||
|
//
|
||||||
|
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||||
|
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||||
|
// clock, across two live contexts.
|
||||||
|
class MagmaPipeIdentityTable {
|
||||||
|
public:
|
||||||
|
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||||
|
|
||||||
|
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||||
|
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||||
|
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||||
|
// Objects currently holding a slot - the live working set this table tracks.
|
||||||
|
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||||
|
|
||||||
|
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||||
|
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||||
|
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||||
|
// that a zero can never be minted into a slot and then indexed with.
|
||||||
|
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||||
|
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||||
|
// that has been handed back to the free list.
|
||||||
|
if (lifetimeId == m_lastLifetimeId) {
|
||||||
|
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||||
|
return m_lastHandle;
|
||||||
|
}
|
||||||
|
Uint32 index = 0;
|
||||||
|
const auto it = m_index.find(lifetimeId);
|
||||||
|
if (it != m_index.end()) {
|
||||||
|
index = it->second;
|
||||||
|
} else {
|
||||||
|
index = ClaimSlot();
|
||||||
|
m_entries[index].LifetimeId = lifetimeId;
|
||||||
|
m_index.emplace(lifetimeId, index);
|
||||||
|
}
|
||||||
|
Entry& entry = m_entries[index];
|
||||||
|
entry.LastUse = m_boundary;
|
||||||
|
m_lastLifetimeId = lifetimeId;
|
||||||
|
m_lastIndex = index;
|
||||||
|
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||||
|
entry.Gen};
|
||||||
|
return m_lastHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||||
|
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||||
|
// slots' consumers use.
|
||||||
|
void OnFrameBoundary() {
|
||||||
|
++m_boundary;
|
||||||
|
if ((m_boundary % kSweepInterval) != 0) return;
|
||||||
|
SizeT retired = 0;
|
||||||
|
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||||
|
Entry& entry = m_entries[it->second];
|
||||||
|
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||||
|
entry.LifetimeId = 0;
|
||||||
|
m_freeSlots.push_back(it->second);
|
||||||
|
it = m_index.erase(it);
|
||||||
|
++retired;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (retired != 0) {
|
||||||
|
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||||
|
// so a front memo pointing at one would still hand out a handle the consumer
|
||||||
|
// tables would accept. Drop it.
|
||||||
|
m_lastLifetimeId = 0;
|
||||||
|
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||||
|
m_kindName, retired, LiveCount(), Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Sweep cadence and retirement age, deliberately the same numbers
|
||||||
|
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||||
|
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||||
|
// entry is still live and still correct, which is a pure waste.
|
||||||
|
static constexpr Uint64 kSweepInterval = 256;
|
||||||
|
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
Uint64 LifetimeId = 0;
|
||||||
|
Uint64 LastUse = 0;
|
||||||
|
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||||
|
// its generation, which is what makes a memo survive a reconfiguration.
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint32 ClaimSlot() {
|
||||||
|
while (!m_freeSlots.empty()) {
|
||||||
|
const Uint32 index = m_freeSlots.back();
|
||||||
|
m_freeSlots.pop_back();
|
||||||
|
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||||
|
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||||
|
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||||
|
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||||
|
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||||
|
// stale handle match a live object. It costs one slot.
|
||||||
|
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||||
|
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||||
|
"retired for good; {slot, gen} stays unique",
|
||||||
|
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++m_entries[index].Gen;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||||
|
m_entries.push_back(Entry{});
|
||||||
|
m_entries[index].Gen = 1;
|
||||||
|
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||||
|
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||||
|
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||||
|
//
|
||||||
|
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||||
|
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||||
|
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||||
|
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||||
|
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||||
|
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||||
|
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||||
|
// and it cannot read a line that was compiled away.
|
||||||
|
const SizeT minted = m_entries.size();
|
||||||
|
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||||
|
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||||
|
m_kindName, minted, LiveCount());
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* m_kindName = "";
|
||||||
|
Uint64 m_boundary = 0;
|
||||||
|
Vector<Entry> m_entries;
|
||||||
|
Vector<Uint32> m_freeSlots;
|
||||||
|
UnorderedMap<Uint64, Uint32> m_index;
|
||||||
|
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||||
|
// object's lifetime id is never 0.
|
||||||
|
Uint64 m_lastLifetimeId = 0;
|
||||||
|
Uint32 m_lastIndex = 0;
|
||||||
|
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||||
|
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||||
|
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||||
|
class MagmaPipeIdentityTables {
|
||||||
|
public:
|
||||||
|
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||||
|
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||||
|
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
|
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||||
|
: m_vaos.Acquire(lifetimeId);
|
||||||
|
}
|
||||||
|
void OnFrameBoundary() {
|
||||||
|
m_vaos.OnFrameBoundary();
|
||||||
|
m_buffers.OnFrameBoundary();
|
||||||
|
}
|
||||||
|
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||||
|
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||||
|
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||||
|
//
|
||||||
|
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||||
|
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||||
|
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||||
|
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||||
|
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||||
|
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||||
|
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||||
|
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||||
|
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||||
|
? 0u
|
||||||
|
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||||
|
//
|
||||||
|
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||||
|
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||||
|
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||||
|
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||||
|
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||||
|
// it replaces gave, without the fixed capacity.
|
||||||
|
template <typename T, Uint32 kChunkEntries = 256>
|
||||||
|
class MagmaPipeSlotTable {
|
||||||
|
public:
|
||||||
|
T& operator[](Uint32 index) {
|
||||||
|
const Uint32 chunk = index / kChunkEntries;
|
||||||
|
while (m_chunks.size() <= chunk) {
|
||||||
|
m_chunks.push_back(MakeUnique<Chunk>());
|
||||||
|
}
|
||||||
|
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||||
|
}
|
||||||
|
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Chunk {
|
||||||
|
T Entries[kChunkEntries] = {};
|
||||||
|
};
|
||||||
|
Vector<UniquePtr<Chunk>> m_chunks;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||||
|
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||||
|
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||||
|
// this code rather than a copy of it.
|
||||||
|
//
|
||||||
|
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||||
|
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||||
|
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||||
|
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||||
|
//
|
||||||
|
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||||
|
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||||
|
// slot" and "the slot was reused and Gen did not move".
|
||||||
|
//
|
||||||
|
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||||
|
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||||
|
template <typename Memos, Uint32 kChunkEntries>
|
||||||
|
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||||
|
const MG_Pipe::MGPipeHandle& handle) {
|
||||||
|
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||||
|
return table[kMagmaPipeAbaControlSlotIndex];
|
||||||
|
}
|
||||||
|
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||||
|
if (!(memos.Owner == handle)) {
|
||||||
|
memos = Memos{};
|
||||||
|
memos.Owner = handle;
|
||||||
|
}
|
||||||
|
return memos;
|
||||||
|
}
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
|
#include "MagmaPipeArms.h"
|
||||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -45,25 +46,149 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||||
// test's positions) instead of its own.
|
// test's positions) instead of its own.
|
||||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||||
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
//
|
||||||
|
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
|
||||||
|
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
|
||||||
|
// into every server-side content hash". The two are equally ABA-proof (the
|
||||||
|
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
|
||||||
|
// changes is that the key is now the identity the SERVER will be handed once
|
||||||
|
// buffers travel as handles, instead of a number only the client can mint.
|
||||||
|
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (attr.Buffer) {
|
||||||
|
// The SAME arm question the other four re-keyed sites ask, through the same
|
||||||
|
// helper: a site that decided for itself could silently key on the pre-handle
|
||||||
|
// identity while its neighbours keyed on the handle.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
|
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||||
|
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||||
|
}
|
||||||
|
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||||
|
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
|
||||||
|
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
|
||||||
|
// wearing two hats, and a control that defeated only the retired one would
|
||||||
|
// say nothing about the key P2 ships.
|
||||||
|
//
|
||||||
|
// The identity is replaced by a constant rather than by the raw
|
||||||
|
// BufferObject*, because the address is not recycled in practice and so
|
||||||
|
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
|
||||||
|
// key with NO buffer identity in it looks like - the exact defect this
|
||||||
|
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
|
||||||
|
// accepts as proof that a memoised binding still reads the buffer it was
|
||||||
|
// resolved from", and with the identity gone it accepts a binding resolved
|
||||||
|
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
|
||||||
|
// replacement VAO and gets its dead predecessor's vertex data.
|
||||||
|
bufferKey = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return XXH64_digest(m_hashState);
|
return XXH64_digest(m_hashState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||||
|
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||||
|
const MG_Pipe::MGPipeHandle handle =
|
||||||
|
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||||
|
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||||
|
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||||
|
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||||
|
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||||
|
//
|
||||||
|
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||||
|
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||||
|
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||||
|
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||||
|
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||||
|
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||||
|
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||||
|
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||||
|
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||||
|
Uint64& outHash) const {
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
const VaoBackendMemos& memos = MemosFor(vao);
|
||||||
|
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||||
|
outHash = memos.Hash;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
return vao.GetBackendHashMemo(outHash);
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
VaoBackendMemos& memos = MemosFor(vao);
|
||||||
|
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||||
|
return memos.Hash;
|
||||||
|
}
|
||||||
|
hash = ComputeHash(vao);
|
||||||
|
memos.Hash = hash;
|
||||||
|
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
if (!vao.GetBackendHashMemo(hash)) {
|
if (!vao.GetBackendHashMemo(hash)) {
|
||||||
hash = ComputeHash(vao);
|
hash = ComputeHash(vao);
|
||||||
vao.SetBackendHashMemo(hash);
|
vao.SetBackendHashMemo(hash);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao) {
|
const MG_State::GLState::VertexArrayObject& vao) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||||
|
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||||
|
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||||
|
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||||
|
// memo does not change that.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
VaoBackendMemos& memos = MemosFor(vao);
|
||||||
|
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||||
|
memos.StateEpoch == m_evictionEpoch) {
|
||||||
|
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||||
|
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
|
return *memoEntry;
|
||||||
|
}
|
||||||
|
const BackendVertexInputState& resolved =
|
||||||
|
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||||
|
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||||
|
// the reference is not worth holding across a call that can resize the table.
|
||||||
|
VaoBackendMemos& stamp = MemosFor(vao);
|
||||||
|
stamp.State = &resolved;
|
||||||
|
stamp.StateEpoch = m_evictionEpoch;
|
||||||
|
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||||
|
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||||
|
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||||
|
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||||
|
// time true, so the handle arm above always returns. Written out rather than left to
|
||||||
|
// fall off the end so the function still has a return on every path a compiler sees.
|
||||||
|
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||||
|
#else
|
||||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||||
// valid while its config version and the cache's eviction epoch both
|
// valid while its config version and the cache's eviction epoch both
|
||||||
// match - no re-hash, no map lookup.
|
// match - no re-hash, no map lookup.
|
||||||
@@ -83,6 +208,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||||
return entry;
|
return entry;
|
||||||
|
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
}
|
}
|
||||||
|
|
||||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||||
@@ -316,8 +442,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||||
// address may be reused by a future insert. Advance through the
|
// address may be reused by a future insert. Advance through the
|
||||||
// process-wide source so the value stays unique across factory
|
// process-wide source so the value stays unique across factory
|
||||||
// instances (see the member comment).
|
// instances (see the member comment). With no legacy arm the memos
|
||||||
|
// live in this factory and die with it, so a per-instance bump is
|
||||||
|
// enough - P2 D12.5.
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
m_evictionEpoch = ++s_evictionEpochSource;
|
m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
|
#else
|
||||||
|
++m_evictionEpoch;
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,12 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
||||||
|
// so the pull build gains nothing from it.
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "MagmaPipeArms.h"
|
||||||
#include "VertexInputStateBuilder.h"
|
#include "VertexInputStateBuilder.h"
|
||||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
@@ -70,8 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||||
|
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||||
|
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||||
|
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||||
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||||
|
MagmaPipeIdentityTables& identity):
|
||||||
|
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||||
|
#else
|
||||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||||
|
#endif
|
||||||
~VertexInputStateFactory() = default;
|
~VertexInputStateFactory() = default;
|
||||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||||
|
|
||||||
@@ -86,6 +100,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||||
// is unchanged. Use this on per-draw paths.
|
// is unchanged. Use this on per-draw paths.
|
||||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||||
|
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||||
|
// question ask the factory instead, because that is where the memo lives once the
|
||||||
|
// frontend object stops carrying the backend's state.
|
||||||
|
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
||||||
|
#endif
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||||
@@ -112,6 +133,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||||
|
//
|
||||||
|
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||||
|
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||||
|
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||||
|
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||||
|
// backend is in another process and its cache entry has no address a client could
|
||||||
|
// store, so the memo has to live on the side that owns the pointee.
|
||||||
|
//
|
||||||
|
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||||
|
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||||
|
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||||
|
struct VaoBackendMemos {
|
||||||
|
// Whose memos these are. The identity table can recycle a slot for a different
|
||||||
|
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||||
|
// the contents are this object's and not its predecessor's.
|
||||||
|
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
Uint64 Hash = 0;
|
||||||
|
Uint32 HashConfigVersion = ~0u;
|
||||||
|
const void* State = nullptr;
|
||||||
|
Uint64 StateEpoch = 0;
|
||||||
|
Uint32 StateConfigVersion = ~0u;
|
||||||
|
};
|
||||||
|
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||||
|
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||||
|
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||||
|
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||||
|
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||||
|
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||||
|
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||||
|
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||||
|
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||||
|
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||||
|
// someone else's.
|
||||||
|
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||||
|
#endif
|
||||||
|
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||||
@@ -137,8 +196,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||||
|
//
|
||||||
|
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||||
|
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||||
|
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||||
|
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
static inline Uint64 s_evictionEpochSource = 0;
|
static inline Uint64 s_evictionEpochSource = 0;
|
||||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||||
|
#else
|
||||||
|
Uint64 m_evictionEpoch = 1;
|
||||||
|
#endif
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -397,6 +397,101 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
static DynamicStateShadow g_dynamicStateShadow;
|
static DynamicStateShadow g_dynamicStateShadow;
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// ---- D12.3: DynamicTailKey's inputs against the P2 chunk table ----
|
||||||
|
//
|
||||||
|
// DynamicTailKey's inventory (declared above, one line per reader) is an exact,
|
||||||
|
// hand-maintained enumeration of what the six Apply* in the tail read. The P2 chunk table
|
||||||
|
// (MG_Pipe/MGPipeRenderStateSpans.h) is an independent, offsetof-derived statement of
|
||||||
|
// which bytes of RenderStateParameters are dynamic state. The two were written for
|
||||||
|
// different reasons, so making them check each other is free evidence: if a later chunk
|
||||||
|
// edit demotes or promotes one of these members, the mismatch is a BUILD BREAK here rather
|
||||||
|
// than a tail that silently stops being re-run when its input moves.
|
||||||
|
//
|
||||||
|
// The brief (P2 D12.3) expects every input to be dynamic; the tree says otherwise for
|
||||||
|
// exactly one, and the tree is right - see the ScissorTestEnabledMask note below.
|
||||||
|
//
|
||||||
|
// These assertions ARE D19's DynamicChunksCoverMagmasDynamicTailKey, in the only file this
|
||||||
|
// package owns. D19 names it as a case in MG_Test/Pipe/RenderStateSpansTest.cpp, which
|
||||||
|
// belongs to package A (C.5). INTEGRATOR: make sure the outcome is not "neither" - if
|
||||||
|
// package A did not land that case, this static_assert block is the whole gate, and if it
|
||||||
|
// did, the two are redundant on purpose and both should stay.
|
||||||
|
namespace {
|
||||||
|
// Is [begin, begin + size) covered entirely by DYNAMIC chunks?
|
||||||
|
constexpr Bool MagmaRenderStateRangeIsDynamic(SizeT begin, SizeT size) {
|
||||||
|
const SizeT end = begin + size;
|
||||||
|
for (SizeT i = 0; i < MG_Pipe::kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
const SizeT chunkBegin = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i];
|
||||||
|
const SizeT chunkEnd = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i + 1];
|
||||||
|
if (end <= chunkBegin || begin >= chunkEnd) continue; // disjoint
|
||||||
|
if (MG_Pipe::MGPipeRenderStateChunkIsPipeline(i)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
using MagmaTailRsp = RenderStateParameters;
|
||||||
|
|
||||||
|
#define MAGMA_TAIL_INPUT_IS_DYNAMIC(Member) \
|
||||||
|
static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, Member), \
|
||||||
|
sizeof(MagmaTailRsp::Member)), \
|
||||||
|
"ApplyDynamicDrawStateTail reads " #Member \
|
||||||
|
", which the P2 chunk table no longer calls dynamic state: a change to it would " \
|
||||||
|
"move the pipeline version, not the parameters version, and the tail would stop " \
|
||||||
|
"being re-run for it")
|
||||||
|
|
||||||
|
// Viewports, DepthRanges and ScissorBoxes are asserted over the WHOLE array while the
|
||||||
|
// tail reads only element 0. That is deliberately stricter than the reader needs: the
|
||||||
|
// chunk table has no per-element granularity today, so an array that is dynamic at all
|
||||||
|
// is dynamic entirely, and asserting the whole of it says so. If a later phase ever
|
||||||
|
// splits a per-viewport chunk out, this is a build break by design - narrow the assert
|
||||||
|
// to element 0 then, and say why in the same commit.
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(Viewports); // ApplyGLViewportState: Viewports[0]
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(DepthRanges); // ApplyGLViewportState: DepthRanges[0]
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(BlendColor); // ApplyBlendConstants
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetFactor); // ApplyPolygonOffsetState
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetUnits); // ApplyPolygonOffsetState
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(LineWidth); // ApplyLineWidthState
|
||||||
|
MAGMA_TAIL_INPUT_IS_DYNAMIC(ScissorBoxes); // the scissor rect: ScissorBoxes[0]
|
||||||
|
#undef MAGMA_TAIL_INPUT_IS_DYNAMIC
|
||||||
|
|
||||||
|
// ApplyStencilState reads three of the seven members of each face, and D6 splits
|
||||||
|
// StencilFaceState at sub-member granularity for exactly this reason: Ref, ValueMask
|
||||||
|
// and WriteMask are VK_DYNAMIC_STATE_STENCIL_{REFERENCE,COMPARE_MASK,WRITE_MASK}, while
|
||||||
|
// Func and the three ops are baked into the pipeline. Asserted per member, per face,
|
||||||
|
// because the split runs THROUGH the struct rather than around it.
|
||||||
|
constexpr SizeT kMagmaStencilFace1 = offsetof(MagmaTailRsp, StencilStates) + sizeof(StencilFaceState);
|
||||||
|
#define MAGMA_TAIL_STENCIL_IS_DYNAMIC(Member) \
|
||||||
|
static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, StencilStates) + \
|
||||||
|
offsetof(StencilFaceState, Member), \
|
||||||
|
sizeof(StencilFaceState::Member)), \
|
||||||
|
"ApplyStencilState reads the FRONT face's " #Member " as dynamic state"); \
|
||||||
|
static_assert(MagmaRenderStateRangeIsDynamic(kMagmaStencilFace1 + offsetof(StencilFaceState, Member), \
|
||||||
|
sizeof(StencilFaceState::Member)), \
|
||||||
|
"ApplyStencilState reads the BACK face's " #Member " as dynamic state")
|
||||||
|
|
||||||
|
MAGMA_TAIL_STENCIL_IS_DYNAMIC(Ref);
|
||||||
|
MAGMA_TAIL_STENCIL_IS_DYNAMIC(ValueMask);
|
||||||
|
MAGMA_TAIL_STENCIL_IS_DYNAMIC(WriteMask);
|
||||||
|
#undef MAGMA_TAIL_STENCIL_IS_DYNAMIC
|
||||||
|
|
||||||
|
// THE ONE INPUT THAT IS NOT DYNAMIC, and the brief's D12.3 says it should be.
|
||||||
|
// The tree wins, and it is right: the split's only rule is "a byte is pipeline state
|
||||||
|
// iff a public setter that calls BumpVersions() writes it", and ScissorTestEnabledMask
|
||||||
|
// is written by SetCapability(ScissorTest), which does. It sits in pipeline chunk P6
|
||||||
|
// with the other capability bools. The tail reads it only to decide between the
|
||||||
|
// scissor box and a full-extent rect, and it is HARMLESS there for a reason worth
|
||||||
|
// stating: a pipeline-half write moves the pipeline version, and the pipeline version
|
||||||
|
// moves only together with the parameters version (BumpVersions bumps both), so the
|
||||||
|
// tail's version gate is invalidated by it just the same. A DYNAMIC member promoted
|
||||||
|
// into the pipeline half would break that direction, which is what the asserts above
|
||||||
|
// are for; this one is pinned in the opposite direction so that DEMOTING it - which
|
||||||
|
// would be a real G7 violation - is also a build break.
|
||||||
|
static_assert(!MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, ScissorTestEnabledMask),
|
||||||
|
sizeof(MagmaTailRsp::ScissorTestEnabledMask)),
|
||||||
|
"ScissorTestEnabledMask is written by SetCapability(ScissorTest), which calls "
|
||||||
|
"BumpVersions(), so the chunk table must keep it in the pipeline half");
|
||||||
|
} // namespace
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
static void ResetDynamicStateShadow() {
|
static void ResetDynamicStateShadow() {
|
||||||
g_dynamicStateShadow = {};
|
g_dynamicStateShadow = {};
|
||||||
}
|
}
|
||||||
@@ -3015,6 +3110,13 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::Initialize() {
|
void VulkanRenderer::Initialize() {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D14, and it belongs HERE rather than on a draw: "a Track-H subsystem whose bit is
|
||||||
|
// clear is a STARTUP Fatal{PipeLegacyMemosDisabled}". Checks Magma's own bit only, and
|
||||||
|
// only once this backend is the one being brought up, so an Espryt-side bitmask cannot
|
||||||
|
// kill a Magma run and vice versa.
|
||||||
|
MagmaPipeValidateSubsystemConfiguration();
|
||||||
|
#endif
|
||||||
CreateInstance();
|
CreateInstance();
|
||||||
CreateSurface();
|
CreateSurface();
|
||||||
PickPhysicalDevice();
|
PickPhysicalDevice();
|
||||||
@@ -3194,7 +3296,12 @@ void main() {
|
|||||||
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
||||||
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
||||||
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_vertexInputStateFactory =
|
||||||
|
MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle, m_pipeIdentity);
|
||||||
|
#else
|
||||||
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
|
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
|
||||||
|
#endif
|
||||||
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
||||||
|
|
||||||
// Prime the first frame so Render() always targets an acquired swapchain image.
|
// Prime the first frame so Render() always targets an acquired swapchain image.
|
||||||
@@ -3542,6 +3649,76 @@ void main() {
|
|||||||
if (m_vaoDrawMemoTable.empty()) {
|
if (m_vaoDrawMemoTable.empty()) {
|
||||||
m_vaoDrawMemoTable.resize(kVaoDrawMemoSlotCount);
|
m_vaoDrawMemoTable.resize(kVaoDrawMemoSlotCount);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||||
|
// Negative control C (P2 brief D18), ahead of BOTH arms because it defeats the
|
||||||
|
// identity half of both keys at once: the legacy arm's (address, lifetime id) pair
|
||||||
|
// and the handle arm's {slot, gen}. Every VAO lands on one entry and the entry is
|
||||||
|
// handed back without an identity compare and WITHOUT being cleared - which is
|
||||||
|
// exactly what this table would do if a replacement object reproduced its dead
|
||||||
|
// predecessor's address, or reused its slot without the generation moving.
|
||||||
|
//
|
||||||
|
// Nothing else about the entry is relaxed: whether the resolved bindings it holds
|
||||||
|
// are then USED is still decided by TryBindResolvedVertexBindings' frame serial,
|
||||||
|
// content hash, active-attribute mask and slice epochs. That is what keeps the arm
|
||||||
|
// an assertion about identity rather than about the memo as a whole.
|
||||||
|
VaoDrawMemo& aliased = m_vaoDrawMemoTable[kMagmaPipeAbaControlSlotIndex];
|
||||||
|
aliased.vaoKey = vao;
|
||||||
|
aliased.vaoLifetimeId = vao->GetLifetimeId();
|
||||||
|
return &aliased;
|
||||||
|
}
|
||||||
|
// ---- P2 D12.4, the handle arm ----
|
||||||
|
//
|
||||||
|
// The slot PICKS the entry, and the handle DECIDES whether the entry is this VAO's -
|
||||||
|
// the same division of labour the legacy arm below gives the address and the lifetime
|
||||||
|
// id, with two differences that are both improvements:
|
||||||
|
//
|
||||||
|
// * the slot is dense from 1, so below kVaoDrawMemoSlotCount live slots the map is a
|
||||||
|
// bijection and the two-way probe never collides at all, where an address hash
|
||||||
|
// collides by the birthday rule from the first few dozen VAOs;
|
||||||
|
// * the handle is an exact identity - Gen moves whenever a slot changes owner - so
|
||||||
|
// neither a deleted VAO's successor at the same heap address nor a VAO whose slot
|
||||||
|
// was recycled can match a predecessor's entry, even byte-identically configured.
|
||||||
|
// That is what makes the lifetime-id half of the legacy compare unnecessary here.
|
||||||
|
//
|
||||||
|
// The capacity and the victim rule are deliberately the base ref's, unchanged: this is
|
||||||
|
// the one memo of the three that HAD a capacity before P2, and an entry lost to a
|
||||||
|
// collision costs exactly what it cost then (one vertex-binding re-resolve). Above
|
||||||
|
// kVaoDrawMemoSlotCount live VAOs a set of two ways serves four slots, and degrades
|
||||||
|
// from there - never worse than the address-hashed table it replaces, which was already
|
||||||
|
// colliding.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
const MG_Pipe::MGPipeHandle handle = ResolveVaoHandle(*vao);
|
||||||
|
const Uint32 index = MagmaPipeSlotIndex(handle) & (kVaoDrawMemoSlotCount - 1u);
|
||||||
|
VaoDrawMemo& first = m_vaoDrawMemoTable[index];
|
||||||
|
if (first.vaoHandle == handle) {
|
||||||
|
return &first;
|
||||||
|
}
|
||||||
|
VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u];
|
||||||
|
if (second.vaoHandle == handle) {
|
||||||
|
return &second;
|
||||||
|
}
|
||||||
|
// Miss: recycle a slot. Prefer an unclaimed one; otherwise evict the entry whose
|
||||||
|
// bindings memo is older (its VAO is the one drawn less recently).
|
||||||
|
VaoDrawMemo* victim = &first;
|
||||||
|
if (!MG_Pipe::MGPipeHandleIsNull(first.vaoHandle) &&
|
||||||
|
(MG_Pipe::MGPipeHandleIsNull(second.vaoHandle) ||
|
||||||
|
second.bindings.frameSerial < first.bindings.frameSerial)) {
|
||||||
|
victim = &second;
|
||||||
|
}
|
||||||
|
victim->vaoHandle = handle;
|
||||||
|
victim->vaoKey = vao;
|
||||||
|
victim->vaoLifetimeId = vao->GetLifetimeId();
|
||||||
|
victim->contentHash = 0;
|
||||||
|
victim->layoutFactsValid = false;
|
||||||
|
// Unmatchable until a resolve completes (same rule as the legacy arm: a bailed-out
|
||||||
|
// resolve must never leave stale contents matchable).
|
||||||
|
victim->bindings.frameSerial = 0;
|
||||||
|
victim->bindings.indexFrameSerial = 0;
|
||||||
|
victim->bindings.indexBuffer = nullptr;
|
||||||
|
return victim;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
// Multiplicative mix of the (16-byte-aligned) address; take high bits, they
|
// Multiplicative mix of the (16-byte-aligned) address; take high bits, they
|
||||||
// carry the most entropy of a multiply.
|
// carry the most entropy of a multiply.
|
||||||
const Uint64 mixed = static_cast<Uint64>(reinterpret_cast<SizeT>(vao) >> 4) * 0x9E3779B97F4A7C15ull;
|
const Uint64 mixed = static_cast<Uint64>(reinterpret_cast<SizeT>(vao) >> 4) * 0x9E3779B97F4A7C15ull;
|
||||||
@@ -3551,6 +3728,13 @@ void main() {
|
|||||||
// its own is recycled, and a slot matched on a recycled address hands the new VAO
|
// its own is recycled, and a slot matched on a recycled address hands the new VAO
|
||||||
// the dead one's resolved bindings.
|
// the dead one's resolved bindings.
|
||||||
const Uint64 lifetimeId = vao->GetLifetimeId();
|
const Uint64 lifetimeId = vao->GetLifetimeId();
|
||||||
|
// Negative control C has NO consumer here. It is answered once, ahead of both arms, by
|
||||||
|
// the early return above, so a run that reaches this line has the knob off and the
|
||||||
|
// lifetime-id half of the compare is unconditional. A fourth consumer here would be a
|
||||||
|
// second site deciding the same question - what MagmaPipeAbaControlDefeatsIdentity
|
||||||
|
// exists to prevent - and a trap: narrow that early return later and this one would
|
||||||
|
// silently return to D18's retired semantics. If it is ever narrowed, ask the accessor
|
||||||
|
// here rather than re-reading MG_Config::Features.
|
||||||
VaoDrawMemo& first = m_vaoDrawMemoTable[index];
|
VaoDrawMemo& first = m_vaoDrawMemoTable[index];
|
||||||
if (first.vaoKey == vao && first.vaoLifetimeId == lifetimeId) {
|
if (first.vaoKey == vao && first.vaoLifetimeId == lifetimeId) {
|
||||||
return &first;
|
return &first;
|
||||||
@@ -3619,7 +3803,7 @@ void main() {
|
|||||||
VaoDrawMemo* slot = nullptr;
|
VaoDrawMemo* slot = nullptr;
|
||||||
ResolvedVertexBindings* memo = nullptr;
|
ResolvedVertexBindings* memo = nullptr;
|
||||||
Uint64 vaoContentHash = 0;
|
Uint64 vaoContentHash = 0;
|
||||||
const Bool vaoHashKnown = vao.GetBackendHashMemo(vaoContentHash);
|
const Bool vaoHashKnown = VaoContentHashIfKnown(vao, vaoContentHash);
|
||||||
if (vaoHashKnown) {
|
if (vaoHashKnown) {
|
||||||
slot = LookupVaoDrawMemo(&vao);
|
slot = LookupVaoDrawMemo(&vao);
|
||||||
memo = &slot->bindings;
|
memo = &slot->bindings;
|
||||||
@@ -4818,6 +5002,7 @@ void main() {
|
|||||||
return MGB_CTX->GetRenderStateParameters().SampleMaskValue;
|
return MGB_CTX->GetRenderStateParameters().SampleMaskValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||||
VkSampleCountFlagBits rasterizationSamples) const {
|
VkSampleCountFlagBits rasterizationSamples) const {
|
||||||
// One bulk fetch instead of ~17 per-field accessor calls into MG_State: every
|
// One bulk fetch instead of ~17 per-field accessor calls into MG_State: every
|
||||||
@@ -4908,6 +5093,23 @@ void main() {
|
|||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
Uint64 VulkanRenderer::ComputePipelineSubsetStateHashFallback() const {
|
||||||
|
// The client's own hash, over the client's own definition of the pipeline subset - the
|
||||||
|
// seven pipeline chunks of the P2 chunk table, which is a strict SUPERSET of what
|
||||||
|
// ComputePipelineStateHash enumerated by hand. The render-pass facts it does not carry
|
||||||
|
// (colorAttachmentCount, the rasterization sample count, and through them the effective
|
||||||
|
// sample mask) are exactly the facts entry.renderPassHash separates, which is why the
|
||||||
|
// CSO handle can key this memo in the first place; this fallback inherits that argument
|
||||||
|
// unchanged.
|
||||||
|
//
|
||||||
|
// Only reached with no render-state CSO bound, and only in a build with no pre-handle
|
||||||
|
// arm to fall back to instead.
|
||||||
|
return MG_Pipe::MGPipeComputePipelineSubsetHash(MGB_CTX->GetRenderStateParameters());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// A program that runs a geometry shader AND captures transform feedback. Both halves are
|
// A program that runs a geometry shader AND captures transform feedback. Both halves are
|
||||||
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
||||||
@@ -4983,6 +5185,25 @@ void main() {
|
|||||||
// per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds.
|
// per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds.
|
||||||
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
||||||
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
|
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.1. Non-null means the client's render-state CSO handle is this draw's state
|
||||||
|
// key and the hash below is not computed at all; null means the pre-handle arm. The
|
||||||
|
// two arms' entries can never match each other: the handle arm stores hash 0 and a
|
||||||
|
// real handle, the legacy arm a real hash and the null handle, and the probe compares
|
||||||
|
// both components.
|
||||||
|
const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso();
|
||||||
|
// Exactly one of the two state keys is live per draw, and the ternary short-circuits,
|
||||||
|
// so a draw on the handle arm neither hashes nor touches the fallback cache.
|
||||||
|
const Uint64 pipelineStateHash =
|
||||||
|
!MG_Pipe::MGPipeHandleIsNull(renderStateCso)
|
||||||
|
? 0
|
||||||
|
: ResolveFallbackPipelineStateHash(renderStateVersion,
|
||||||
|
renderPassEntry.colorAttachmentCount,
|
||||||
|
renderPassEntry.sampleCount);
|
||||||
|
#else
|
||||||
|
// THE PULL BUILD'S TEXT, statement for statement what the base ref has: G1 admits no
|
||||||
|
// resize of this function, and a helper the compiler merely inlines is not the same
|
||||||
|
// instruction schedule.
|
||||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
|
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
|
||||||
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
|
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
|
||||||
@@ -4994,12 +5215,16 @@ void main() {
|
|||||||
m_pipelineStateHashValid = true;
|
m_pipelineStateHashValid = true;
|
||||||
}
|
}
|
||||||
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
||||||
|
#endif
|
||||||
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||||
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
||||||
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||||
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
||||||
entry.renderPassHash == renderPassHash &&
|
entry.renderPassHash == renderPassHash &&
|
||||||
entry.pipelineStateHash == pipelineStateHash &&
|
entry.pipelineStateHash == pipelineStateHash &&
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
entry.renderStateCso == renderStateCso &&
|
||||||
|
#endif
|
||||||
entry.primitiveRestartEnable == primitiveRestartEnable &&
|
entry.primitiveRestartEnable == primitiveRestartEnable &&
|
||||||
entry.transformFlags == transformFlags) {
|
entry.transformFlags == transformFlags) {
|
||||||
if (MG_Util::PipeStats::Enabled()) {
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
@@ -5668,6 +5893,9 @@ void main() {
|
|||||||
entry.vertexInputHash = vertexLayoutHash;
|
entry.vertexInputHash = vertexLayoutHash;
|
||||||
entry.renderPassHash = renderPassHash;
|
entry.renderPassHash = renderPassHash;
|
||||||
entry.pipelineStateHash = pipelineStateHash;
|
entry.pipelineStateHash = pipelineStateHash;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
entry.renderStateCso = renderStateCso;
|
||||||
|
#endif
|
||||||
entry.primitiveRestartEnable = primitiveRestartEnable;
|
entry.primitiveRestartEnable = primitiveRestartEnable;
|
||||||
entry.transformFlags = transformFlags;
|
entry.transformFlags = transformFlags;
|
||||||
entry.pipeline = pipeline;
|
entry.pipeline = pipeline;
|
||||||
@@ -5917,6 +6145,21 @@ void main() {
|
|||||||
// One compare for the whole tail: see the gate's declaration in
|
// One compare for the whole tail: see the gate's declaration in
|
||||||
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
||||||
// input the six Apply* below read.
|
// input the six Apply* below read.
|
||||||
|
//
|
||||||
|
// P2 D12.3: this read is RE-SOURCED, not re-shaped. Under MOBILEGL_PIPE_PUSH the
|
||||||
|
// accessor no longer walks into GLContext's RenderState - it returns
|
||||||
|
// PipeInputs::m_renderStateParametersVersion, which the applier publishes from
|
||||||
|
// MGPDynamicState::Version (set_dynamic_state) and MGPBindRenderState::Version
|
||||||
|
// (bind_render_state). So the gate now reads what the client PUSHED.
|
||||||
|
//
|
||||||
|
// What it does NOT do, and the P2 brief expects it to, is stop moving on a
|
||||||
|
// pipeline-only change. The tree settles that against the brief: bind_render_state
|
||||||
|
// carries m_version too and the applier publishes it, and it has to - Espryt's
|
||||||
|
// SyncRenderState uses the very same counter as its all-state change detector and G5
|
||||||
|
// forbids touching it, so a bind that rewrote the pipeline half while leaving the
|
||||||
|
// counter still would make Espryt skip re-syncing the blend state it just changed.
|
||||||
|
// The second-level DynamicTailKey compare below is therefore what actually absorbs a
|
||||||
|
// pipeline-only change, exactly as it did before P2: one key build, no vkCmd*.
|
||||||
const Uint paramsVersion = MGB_CTX->GetRenderStateParametersVersion();
|
const Uint paramsVersion = MGB_CTX->GetRenderStateParametersVersion();
|
||||||
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
|
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
|
||||||
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
|
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
|
||||||
@@ -6104,9 +6347,23 @@ void main() {
|
|||||||
// draw of a VAO-cycling stream (Minecraft chunk rendering) through the full
|
// draw of a VAO-cycling stream (Minecraft chunk rendering) through the full
|
||||||
// path, re-resolving descriptors and texture layouts nothing invalidated.
|
// path, re-resolving descriptors and texture layouts nothing invalidated.
|
||||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.4: the handle replaces the (address, lifetime id) pair here too - one
|
||||||
|
// compare instead of two, and the same identity the VAO draw memo is keyed on, so
|
||||||
|
// the two cannot disagree about whether "the VAO moved". The config version stays:
|
||||||
|
// it answers a different question (did this same object's layout change).
|
||||||
|
const Bool vaoMoved =
|
||||||
|
MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)
|
||||||
|
? (!(ResolveVaoHandle(vao) == snap.vaoHandle) ||
|
||||||
|
vao.GetConfigVersion() != snap.vaoConfigVersion)
|
||||||
|
: (static_cast<const void*>(&vao) != snap.vao ||
|
||||||
|
vao.GetLifetimeId() != snap.vaoLifetimeId ||
|
||||||
|
vao.GetConfigVersion() != snap.vaoConfigVersion);
|
||||||
|
#else
|
||||||
const Bool vaoMoved =
|
const Bool vaoMoved =
|
||||||
static_cast<const void*>(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId ||
|
static_cast<const void*>(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId ||
|
||||||
vao.GetConfigVersion() != snap.vaoConfigVersion;
|
vao.GetConfigVersion() != snap.vaoConfigVersion;
|
||||||
|
#endif
|
||||||
const auto& drawFbo =
|
const auto& drawFbo =
|
||||||
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
||||||
@@ -6194,7 +6451,7 @@ void main() {
|
|||||||
Uint64 auxMasks = 0;
|
Uint64 auxMasks = 0;
|
||||||
Bool factsKnown = false;
|
Bool factsKnown = false;
|
||||||
Uint64 contentHash = 0;
|
Uint64 contentHash = 0;
|
||||||
if (vao.GetBackendHashMemo(contentHash)) {
|
if (VaoContentHashIfKnown(vao, contentHash)) {
|
||||||
const VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao);
|
const VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao);
|
||||||
if (vaoMemo->layoutFactsValid && vaoMemo->contentHash == contentHash) {
|
if (vaoMemo->layoutFactsValid && vaoMemo->contentHash == contentHash) {
|
||||||
vaoLayoutHash = vaoMemo->layoutHash;
|
vaoLayoutHash = vaoMemo->layoutHash;
|
||||||
@@ -6211,7 +6468,7 @@ void main() {
|
|||||||
auxMasks = VertexInputStateFactory::PackVertexInputAuxMasks(
|
auxMasks = VertexInputStateFactory::PackVertexInputAuxMasks(
|
||||||
vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask);
|
vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask);
|
||||||
Uint64 stampedHash = 0;
|
Uint64 stampedHash = 0;
|
||||||
if (vao.GetBackendHashMemo(stampedHash)) {
|
if (VaoContentHashIfKnown(vao, stampedHash)) {
|
||||||
VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao);
|
VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao);
|
||||||
vaoMemo->contentHash = stampedHash;
|
vaoMemo->contentHash = stampedHash;
|
||||||
vaoMemo->layoutHash = vaoLayoutHash;
|
vaoMemo->layoutHash = vaoLayoutHash;
|
||||||
@@ -6332,6 +6589,18 @@ void main() {
|
|||||||
// what lets a per-draw GL_BLEND toggle alternate between two memo entries
|
// what lets a per-draw GL_BLEND toggle alternate between two memo entries
|
||||||
// instead of missing forever on a monotonic version. A miss falls through
|
// instead of missing forever on a monotonic version. A miss falls through
|
||||||
// to the full lookup.
|
// to the full lookup.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Same arm selector as GetOrCreatePipeline's probe (P2 D12.1); this site is the
|
||||||
|
// fast path's copy of it, and the two must key identically or the fast path would
|
||||||
|
// hand back a pipeline the full path would not have matched.
|
||||||
|
const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso();
|
||||||
|
const Uint64 pipelineStateHash =
|
||||||
|
!MG_Pipe::MGPipeHandleIsNull(renderStateCso)
|
||||||
|
? 0
|
||||||
|
: ResolveFallbackPipelineStateHash(renderStateVersion, snap.renderPassColorCount,
|
||||||
|
snap.renderPassSampleCount);
|
||||||
|
#else
|
||||||
|
// The pull build's text, statement for statement (see GetOrCreatePipeline).
|
||||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||||
m_pipelineStateHashColorCount != snap.renderPassColorCount ||
|
m_pipelineStateHashColorCount != snap.renderPassColorCount ||
|
||||||
m_pipelineStateHashSampleCount != snap.renderPassSampleCount) {
|
m_pipelineStateHashSampleCount != snap.renderPassSampleCount) {
|
||||||
@@ -6342,6 +6611,8 @@ void main() {
|
|||||||
m_pipelineStateHashSampleCount = snap.renderPassSampleCount;
|
m_pipelineStateHashSampleCount = snap.renderPassSampleCount;
|
||||||
m_pipelineStateHashValid = true;
|
m_pipelineStateHashValid = true;
|
||||||
}
|
}
|
||||||
|
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
||||||
|
#endif
|
||||||
const auto memoTransformFlags =
|
const auto memoTransformFlags =
|
||||||
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags);
|
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags);
|
||||||
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||||
@@ -6349,7 +6620,10 @@ void main() {
|
|||||||
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||||
entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash &&
|
entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash &&
|
||||||
entry.renderPassHash == snap.renderPassHash &&
|
entry.renderPassHash == snap.renderPassHash &&
|
||||||
entry.pipelineStateHash == m_pipelineStateHash &&
|
entry.pipelineStateHash == pipelineStateHash &&
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
entry.renderStateCso == renderStateCso &&
|
||||||
|
#endif
|
||||||
entry.primitiveRestartEnable == drawPrimitiveRestartEnable &&
|
entry.primitiveRestartEnable == drawPrimitiveRestartEnable &&
|
||||||
entry.transformFlags == memoTransformFlags) {
|
entry.transformFlags == memoTransformFlags) {
|
||||||
pipeline = entry.pipeline;
|
pipeline = entry.pipeline;
|
||||||
@@ -6383,6 +6657,15 @@ void main() {
|
|||||||
snap.bindGeneration = bindGeneration;
|
snap.bindGeneration = bindGeneration;
|
||||||
snap.vao = static_cast<const void*>(&vao);
|
snap.vao = static_cast<const void*>(&vao);
|
||||||
snap.vaoLifetimeId = vao.GetLifetimeId();
|
snap.vaoLifetimeId = vao.GetLifetimeId();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Guarded by the SUBSYSTEM, not only by the build switch: with bit 6 clear the field
|
||||||
|
// is dead (vaoMoved takes the address/lifetime-id branch), and minting a handle for it
|
||||||
|
// would put this package's cost inside MOBILEGL_PIPE_PUSH=0 - the all-pull control arm
|
||||||
|
// D14 defines as reproducing P1 exactly, and the arm D.4.3's T2 is measured on.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
snap.vaoHandle = ResolveVaoHandle(vao);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
snap.vaoConfigVersion = vao.GetConfigVersion();
|
snap.vaoConfigVersion = vao.GetConfigVersion();
|
||||||
snap.vaoLayoutHash = vaoLayoutHash;
|
snap.vaoLayoutHash = vaoLayoutHash;
|
||||||
snap.pipeline = pipeline;
|
snap.pipeline = pipeline;
|
||||||
@@ -6964,6 +7247,13 @@ void main() {
|
|||||||
snap.programVersion = program.GetBackendStateVersion();
|
snap.programVersion = program.GetBackendStateVersion();
|
||||||
snap.vao = &vao;
|
snap.vao = &vao;
|
||||||
snap.vaoLifetimeId = vao.GetLifetimeId();
|
snap.vaoLifetimeId = vao.GetLifetimeId();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Subsystem-guarded for the same reason as the other stamping site: the field
|
||||||
|
// is dead with bit 6 clear, and MOBILEGL_PIPE_PUSH=0 has to be P1 exactly.
|
||||||
|
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||||
|
snap.vaoHandle = ResolveVaoHandle(vao);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
snap.vaoConfigVersion = vao.GetConfigVersion();
|
snap.vaoConfigVersion = vao.GetConfigVersion();
|
||||||
snap.drawFbo = drawFbo.get();
|
snap.drawFbo = drawFbo.get();
|
||||||
snap.drawFboLifetimeId = drawFbo->GetLifetimeId();
|
snap.drawFboLifetimeId = drawFbo->GetLifetimeId();
|
||||||
@@ -12524,6 +12814,14 @@ void main() {
|
|||||||
if (m_vertexInputStateFactory) {
|
if (m_vertexInputStateFactory) {
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// Reclaim {slot, gen} for objects that have not been drawn for a long time, on the same
|
||||||
|
// cadence and the same retirement age as the entries those slots key. This is the
|
||||||
|
// stand-in for the frontend death notification P2 has no hook for, and it is what keeps
|
||||||
|
// the mint's footprint the LIVE working set rather than every object ever created
|
||||||
|
// (review v2 MAJOR 1 / MAJOR 3).
|
||||||
|
m_pipeIdentity.OnFrameBoundary();
|
||||||
|
#endif
|
||||||
if (m_samplerManager) {
|
if (m_samplerManager) {
|
||||||
m_samplerManager->OnFrameBoundary();
|
m_samplerManager->OnFrameBoundary();
|
||||||
}
|
}
|
||||||
@@ -12889,6 +13187,9 @@ void main() {
|
|||||||
InvalidateSetupDrawSnapshots();
|
InvalidateSetupDrawSnapshots();
|
||||||
}
|
}
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
m_vertexInputStateFactory->OnFrameBoundary();
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_pipeIdentity.OnFrameBoundary();
|
||||||
|
#endif
|
||||||
m_samplerManager->OnFrameBoundary();
|
m_samplerManager->OnFrameBoundary();
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "FrameContext.h"
|
#include "FrameContext.h"
|
||||||
|
#include "MagmaPipeArms.h"
|
||||||
#include "PipelineFactory.h"
|
#include "PipelineFactory.h"
|
||||||
#include "ProgramFactory.h"
|
#include "ProgramFactory.h"
|
||||||
#include "SwapchainObject.h"
|
#include "SwapchainObject.h"
|
||||||
@@ -24,7 +25,13 @@
|
|||||||
#include "MG_Util/Math/VectorTypes.h"
|
#include "MG_Util/Math/VectorTypes.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Backend/BackendObject.h>
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo
|
||||||
|
// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged.
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
@@ -820,12 +827,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 programHash = 0;
|
Uint64 programHash = 0;
|
||||||
Uint64 vertexInputHash = 0;
|
Uint64 vertexInputHash = 0;
|
||||||
Uint64 renderPassHash = 0;
|
Uint64 renderPassHash = 0;
|
||||||
// VALUE hash of the pipeline-relevant fixed-function state (see
|
// The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the
|
||||||
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
// handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see
|
||||||
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
// ComputePipelineStateHash), not the monotonic pipeline-state version: the version
|
||||||
// all entries forever even though the state alternates between two
|
// never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even
|
||||||
// values the memo already holds.
|
// though the state alternates between two values the memo already holds.
|
||||||
Uint64 pipelineStateHash = 0;
|
Uint64 pipelineStateHash = 0;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The HANDLE arm's key component, and the whole of D12.1: the CLIENT already
|
||||||
|
// hashed the pipeline subset of RenderStateParameters and minted a content-
|
||||||
|
// addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache),
|
||||||
|
// so re-hashing the same 396 bytes here was work the boundary had already done.
|
||||||
|
// Two draws share a CSO handle exactly when their pipeline bytes are equal, and
|
||||||
|
// the client's subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||||
|
// so the handle discriminates at least as finely as the hash it replaces.
|
||||||
|
//
|
||||||
|
// renderPassHash STAYS beside it and is what keeps this key complete: the CSO
|
||||||
|
// carries GL state only, while colorAttachmentCount and the rasterization sample
|
||||||
|
// count - which ComputePipelineStateHash folded in through its signature and
|
||||||
|
// through ResolveEffectiveSampleMask - are render-pass facts that the render-pass
|
||||||
|
// hash already separates.
|
||||||
|
//
|
||||||
|
// Null in an entry minted by the legacy arm, so entries of the two arms can never
|
||||||
|
// match each other: the compare below tests BOTH components.
|
||||||
|
MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
#endif
|
||||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||||
@@ -839,19 +865,142 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||||
Uint32 m_pipelineMemoCount = 0;
|
Uint32 m_pipelineMemoCount = 0;
|
||||||
Uint32 m_pipelineMemoNext = 0;
|
Uint32 m_pipelineMemoNext = 0;
|
||||||
// Hash of every fixed-function GL state the pipeline payload reads that the
|
|
||||||
// memo key's other fields (mode / program / vertex input / render pass /
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// transform flags) do not already pin down. Equal hash under an equal rest
|
// P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the
|
||||||
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
// render-state CSO this draw is keyed on, or the null handle when the pre-handle arm
|
||||||
|
// is the one that runs.
|
||||||
|
//
|
||||||
|
// Under the handle arm the memo's state key IS this handle. The client hashed those
|
||||||
|
// 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so
|
||||||
|
// recomputing an overlapping hash here was work the boundary had already done; the
|
||||||
|
// client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||||
|
// so the handle discriminates at least as finely as the hash it replaces. What the
|
||||||
|
// handle does NOT carry is the render-pass side - colorAttachmentCount and the
|
||||||
|
// rasterization sample count, which ComputePipelineStateHash folded in through its
|
||||||
|
// signature and through ResolveEffectiveSampleMask - and that is exactly why
|
||||||
|
// entry.renderPassHash stays in the key beside it.
|
||||||
|
//
|
||||||
|
// The arm is live only when the render-state subsystem is migrated in this run AND the
|
||||||
|
// client has actually bound a CSO. The second half is not belt and braces: a tree whose
|
||||||
|
// tracker does not emit create/bind_render_state yet has no handle to key on, and
|
||||||
|
// delete_render_state clears the binding (MG_Pipe/PipeApply.cpp), so the null handle is
|
||||||
|
// reachable on any tree. Keying every draw on it would alias every render state onto
|
||||||
|
// one memo entry, so a null handle means "fall back to a state hash" - never an abort,
|
||||||
|
// and never a per-draw consultation of the legacy-memo lever: bit 0 is not a Track-H
|
||||||
|
// subsystem (D14 labels only bits 5 and 6 that), and the lever's Fatal is a STARTUP
|
||||||
|
// one, in MagmaPipeValidateSubsystemConfiguration.
|
||||||
|
//
|
||||||
|
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
||||||
|
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
||||||
|
// at every shipped log level.
|
||||||
|
//
|
||||||
|
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
|
||||||
|
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
|
||||||
|
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
|
||||||
|
// this comment claimed - and this site is on the per-draw pipeline path in the very
|
||||||
|
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
|
||||||
|
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
|
||||||
|
// load of a member that is false exactly once. Single-threaded like the rest of the
|
||||||
|
// renderer, and per renderer rather than per process, which is also the right scope: a
|
||||||
|
// second context that never binds a CSO deserves to say so.
|
||||||
|
//
|
||||||
|
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
|
||||||
|
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
|
||||||
|
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
|
||||||
|
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
|
||||||
|
//
|
||||||
|
// Push-only by construction: the pull build does not compile this function at all, so
|
||||||
|
// its two callers are statement-for-statement what they were (G1).
|
||||||
|
//
|
||||||
|
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
|
||||||
|
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
|
||||||
|
// specifies. In a multi-context process this reads whatever CSO another context last
|
||||||
|
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
|
||||||
|
// consumer, so it is named here rather than left for both reviews to assume the other
|
||||||
|
// caught it.
|
||||||
|
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
||||||
|
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||||
|
return MG_Pipe::kMGPipeNullHandle;
|
||||||
|
}
|
||||||
|
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
|
||||||
|
m_pipelineCsoFallbackWarned = true;
|
||||||
|
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
||||||
|
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
||||||
|
"handle (no tracker on this build, or a draw between "
|
||||||
|
"delete_render_state and the next bind)");
|
||||||
|
}
|
||||||
|
return boundCso;
|
||||||
|
}
|
||||||
|
// Latch for the warning above. Mutable because the resolve is const and the latch is
|
||||||
|
// not part of the renderer's observable state.
|
||||||
|
mutable Bool m_pipelineCsoFallbackWarned = false;
|
||||||
|
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
||||||
|
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
||||||
|
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
||||||
|
// (version, colorAttachmentCount, sampleCount) proves the bytes are unchanged.
|
||||||
|
//
|
||||||
|
// [deviation from D12.1] The brief deletes this gate and its cached fields outright.
|
||||||
|
// They cannot go while a no-CSO draw is reachable - and it is, on any tree: a draw
|
||||||
|
// between delete_render_state and the next bind has no handle. On a tree whose tracker
|
||||||
|
// binds a CSO these five words are written once and never read again; they retire for
|
||||||
|
// real when the pull path does, at P13.
|
||||||
|
Uint64 ResolveFallbackPipelineStateHash(Uint renderStateVersion, Uint32 colorAttachmentCount,
|
||||||
|
VkSampleCountFlagBits rasterizationSamples) {
|
||||||
|
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||||
|
m_pipelineStateHashColorCount != colorAttachmentCount ||
|
||||||
|
m_pipelineStateHashSampleCount != rasterizationSamples) {
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
m_pipelineStateHash =
|
||||||
|
ComputePipelineStateHash(colorAttachmentCount, rasterizationSamples);
|
||||||
|
#else
|
||||||
|
m_pipelineStateHash = ComputePipelineSubsetStateHashFallback();
|
||||||
|
#endif
|
||||||
|
m_pipelineStateHashVersion = renderStateVersion;
|
||||||
|
m_pipelineStateHashColorCount = colorAttachmentCount;
|
||||||
|
m_pipelineStateHashSampleCount = rasterizationSamples;
|
||||||
|
m_pipelineStateHashValid = true;
|
||||||
|
}
|
||||||
|
return m_pipelineStateHash;
|
||||||
|
}
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
#if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
// The same answer as ComputePipelineStateHash, computed from the P2 chunk table
|
||||||
|
// instead of from a hand-written field list, for the build that compiles no
|
||||||
|
// pre-handle arm (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF). It is the CLIENT's own
|
||||||
|
// hash function - MGPipeComputePipelineSubsetHash over the 396 pipeline bytes - so a
|
||||||
|
// draw keyed on it and a draw keyed on a CSO handle are keyed on the same equivalence
|
||||||
|
// class of state, and the render-pass facts stay separated by renderPassHash either
|
||||||
|
// way. This is what makes the no-legacy build RUNNABLE rather than a configuration
|
||||||
|
// that aborts on the first draw that arrives without a CSO.
|
||||||
|
Uint64 ComputePipelineSubsetStateHashFallback() const;
|
||||||
|
#endif
|
||||||
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||||
|
// THE PRE-HANDLE ARM (P2 brief D12.1 / D14). Hash of every fixed-function GL state the
|
||||||
|
// pipeline payload reads that the memo key's other fields (mode / program / vertex
|
||||||
|
// input / render pass / transform flags) do not already pin down. Equal hash under an
|
||||||
|
// equal rest of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||||
// version: the version is monotonic and bumps on every pipeline-state
|
// version: the version is monotonic and bumps on every pipeline-state
|
||||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||||
|
//
|
||||||
|
// The handle arm computes none of this: the client hashed the same bytes when it
|
||||||
|
// minted the CSO, so all five cached-hash members below exist only to avoid a
|
||||||
|
// re-hash the handle arm never performs.
|
||||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||||
VkSampleCountFlagBits rasterizationSamples) const;
|
VkSampleCountFlagBits rasterizationSamples) const;
|
||||||
|
#endif
|
||||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||||
// and the pipeline-state memo word so the two cannot disagree.
|
// and the pipeline-state memo word so the two cannot disagree. NOT part of the legacy
|
||||||
|
// arm: it is a PAYLOAD computation that depends on rasterizationSamples, so it survives
|
||||||
|
// the re-key and keeps reading Multisample / SampleMask / SampleMaskValue out of the
|
||||||
|
// working block.
|
||||||
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
|
||||||
|
// ResolveFallbackPipelineStateHash's cache. Written once and never read again on a
|
||||||
|
// build whose client binds a render-state CSO; see that function for why it survives
|
||||||
|
// the re-key at all.
|
||||||
Uint m_pipelineStateHashVersion = 0;
|
Uint m_pipelineStateHashVersion = 0;
|
||||||
Uint32 m_pipelineStateHashColorCount = 0;
|
Uint32 m_pipelineStateHashColorCount = 0;
|
||||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||||
@@ -877,7 +1026,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Drops every memoized pipeline handle. Required at command-buffer
|
// Drops every memoized pipeline handle. Required at command-buffer
|
||||||
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||||
// the cached pipeline-state hash: the same boundaries can retire the GL
|
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||||
// context whose monotonic version the cache is keyed on.
|
// context whose monotonic version the cache is keyed on. The handle arm has no
|
||||||
|
// such cache to drop - a CSO handle is not derived from a monotonic version.
|
||||||
void InvalidatePipelineMemo() {
|
void InvalidatePipelineMemo() {
|
||||||
m_pipelineMemoCount = 0;
|
m_pipelineMemoCount = 0;
|
||||||
m_pipelineMemoNext = 0;
|
m_pipelineMemoNext = 0;
|
||||||
@@ -968,6 +1118,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// common shape), and "the VAO did not move" would then skip the layout
|
// common shape), and "the VAO did not move" would then skip the layout
|
||||||
// re-resolve for a different VAO.
|
// re-resolve for a different VAO.
|
||||||
Uint64 vaoLifetimeId = 0;
|
Uint64 vaoLifetimeId = 0;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.4: the handle arm's answer to the same question, and one compare rather
|
||||||
|
// than the pair above. Kept BESIDE them rather than replacing them because the
|
||||||
|
// pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot
|
||||||
|
// is a value struct, not a wire type.
|
||||||
|
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
#endif
|
||||||
Uint32 vaoConfigVersion = 0;
|
Uint32 vaoConfigVersion = 0;
|
||||||
const void* drawFbo = nullptr;
|
const void* drawFbo = nullptr;
|
||||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||||
@@ -1242,6 +1399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// - bindings revalidates per draw exactly as before (frame serial, content
|
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||||
// hash, per-binding live buffer pointers and slice epochs).
|
// hash, per-binding live buffer pointers and slice epochs).
|
||||||
struct alignas(64) VaoDrawMemo {
|
struct alignas(64) VaoDrawMemo {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an
|
||||||
|
// identity, so the pointer-plus-lifetime-id pair below stops being a key here;
|
||||||
|
// the slot also picks the table entry, so the address hash and the two-way probe
|
||||||
|
// go with it. Null in an entry that has never been claimed.
|
||||||
|
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
#endif
|
||||||
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||||
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||||
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||||
@@ -1268,8 +1432,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||||
// stable for the duration of a draw, which the EBO memo handoff
|
// stable for the duration of a draw, which the EBO memo handoff
|
||||||
// (m_currentDrawResolvedEntry) relies on.
|
// (m_currentDrawResolvedEntry) relies on.
|
||||||
|
//
|
||||||
|
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
|
||||||
|
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
|
||||||
|
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
|
||||||
|
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
|
||||||
|
// whole of review v2's MAJOR 1:
|
||||||
|
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
|
||||||
|
// table with the live VAO set is megabytes on a platform with an LMK, where the
|
||||||
|
// other two memos are 48 B and can afford it;
|
||||||
|
// * this is the ONLY one of the three memos that had a capacity before this package.
|
||||||
|
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
|
||||||
|
// cost on the base ref, so at any working-set size this table is no worse than what
|
||||||
|
// it replaces - and strictly better below capacity, where the handle is a bijection
|
||||||
|
// with the slot and the two-way probe never collides at all. The other two memos
|
||||||
|
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
|
||||||
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||||
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
|
||||||
|
// derive the same handle for the same VAO. Per renderer, never a process-global: a
|
||||||
|
// global would share one table and one reclamation clock across two live contexts and
|
||||||
|
// outlive every one of them (review v2 minor 4).
|
||||||
|
MagmaPipeIdentityTables m_pipeIdentity;
|
||||||
|
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
|
||||||
|
// first, so there is no second memo in front of it here.
|
||||||
|
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
||||||
|
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
|
||||||
|
vao.GetLifetimeId());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
||||||
|
// memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL
|
||||||
|
// build's three readers keep compiling to the very same two loads they always did -
|
||||||
|
// G1 admits no resize, and an out-of-line call here would be one.
|
||||||
|
[[gnu::always_inline]] inline Bool VaoContentHashIfKnown(
|
||||||
|
const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash);
|
||||||
|
#else
|
||||||
|
return vao.GetBackendHashMemo(outHash);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||||
// slots into an empty memo keyed on `vao`. Never returns null.
|
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||||
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||||
|
|||||||
@@ -609,6 +609,13 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
|
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
|
||||||
// the filler's per-field copies and stamps, and the verify snapshot.
|
// the filler's per-field copies and stamps, and the verify snapshot.
|
||||||
friend struct MGPipeFillAccess;
|
friend struct MGPipeFillAccess;
|
||||||
|
// The other door, and the one that exists because of what this block IS after P2:
|
||||||
|
// the server's working RenderStateParameters. MG_Pipe/PipeApply.cpp scatters
|
||||||
|
// bind_render_state's and set_dynamic_state's chunks straight into m_renderState,
|
||||||
|
// which is why DirectGLES' SyncRenderState is not one line changed. It deliberately
|
||||||
|
// does NOT stamp the poison generations - a stamp says "the filler published this
|
||||||
|
// for THIS verb", which is the walk's statement, not the applier's.
|
||||||
|
friend struct MGPipeApplyAccess;
|
||||||
|
|
||||||
// ---- identity ----
|
// ---- identity ----
|
||||||
const void* m_contextIdentity = nullptr;
|
const void* m_contextIdentity = nullptr;
|
||||||
@@ -681,7 +688,22 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||||
// variable: no .cpp is needed for the definition.
|
// variable: no .cpp is needed for the definition.
|
||||||
inline PipeInputs gPipeInputs{};
|
//
|
||||||
|
// 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.)
|
||||||
|
inline PipeInputs& gPipeInputs = *new PipeInputs();
|
||||||
|
|
||||||
// Every field has storage or is forwarded, and nothing else.
|
// Every field has storage or is forwarded, and nothing else.
|
||||||
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
||||||
|
|||||||
@@ -11,5 +11,51 @@ endif()
|
|||||||
add_executable(DriverBench DriverBench.c)
|
add_executable(DriverBench DriverBench.c)
|
||||||
target_link_libraries(DriverBench PRIVATE dl)
|
target_link_libraries(DriverBench PRIVATE dl)
|
||||||
|
|
||||||
|
# WHY EVERY ENTRY HERE CARRIES A PASS_REGULAR_EXPRESSION.
|
||||||
|
#
|
||||||
|
# DriverBench prints one CSV row per case it ran and exits 0 whatever it ran. Before this, a ctest
|
||||||
|
# entry naming a case therefore could not answer the only question it exists to ask: an argument
|
||||||
|
# matching nothing in kBenchCases selected no case, printed only the header row, and still exited
|
||||||
|
# 0. DriverBench.c now refuses an unknown case name (exit 2), which closes it at the source - but
|
||||||
|
# the entry must be able to go red for the reason it exists WITHOUT depending on that check
|
||||||
|
# staying in the binary, so each entry also requires the case's own output row to appear.
|
||||||
|
#
|
||||||
|
# The regex is what a healthy run of that case prints and nothing else does: the case name at the
|
||||||
|
# start of a line, then the frames / ops-per-frame / median-ms / ns-per-op / fps columns
|
||||||
|
# (run_case()). A rename, a drop from kBenchCases, a boot_egl() failure or
|
||||||
|
# a crash part-way through the case all remove that row and turn the entry red.
|
||||||
|
#
|
||||||
|
# Note that a PASS_REGULAR_EXPRESSION makes ctest ignore the process exit code (cmCTestRunTest:
|
||||||
|
# success is `retVal == 0 || !RequiredRegularExpressions.empty()`), which is why the row itself
|
||||||
|
# has to be the evidence rather than a companion to the rc.
|
||||||
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||||
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
|
# draw_tiny's a/ops scale with $DRIVERBENCH_DRAWS (main()), so only the shape of
|
||||||
|
# the row is pinned here, not the column values.
|
||||||
|
set_tests_properties(DriverBench PROPERTIES
|
||||||
|
LABELS benchmark
|
||||||
|
PASS_REGULAR_EXPRESSION "(^|\n)draw_tiny,[0-9]+,[0-9]+,[0-9.]+,[0-9.]+,[0-9.]+")
|
||||||
|
|
||||||
|
# The Blaze3D blend toggle, as its own entry.
|
||||||
|
#
|
||||||
|
# mc_state_toggle is glEnable(GL_BLEND) / glBlendFuncSeparate / glDrawElements /
|
||||||
|
# glDisable(GL_BLEND) / glDrawElements, 46 times - the measured vanilla-frame rate, and the exact
|
||||||
|
# shape ROADMAP.md writes down as the microbenchmark P2 owes the GO/NO-GO. It is the workload the
|
||||||
|
# whole "push at validate, not in the setter" decision was made for: a per-setter design pays for
|
||||||
|
# every toggle, and a CSO that is minted twice and then reused pays for none of them.
|
||||||
|
#
|
||||||
|
# The case has existed in kBenchCases since P0 and nothing ran it, so nothing noticed if it broke.
|
||||||
|
# Exposing it costs about 1.2 s inside an existing three-minute job, and it means the number the
|
||||||
|
# P2 report quotes comes from a case CI has been executing all along rather than from a code path
|
||||||
|
# whose first run is the day it is measured.
|
||||||
|
#
|
||||||
|
# Like the entry above, this runs against whatever $DRIVERBENCH_EGL_LIB names (the system driver
|
||||||
|
# when unset) - the ctest entry is a "does this case still run" gate, not the measurement. The
|
||||||
|
# measurement is run_driver_bench.sh against each of {native, espryt, magma}.
|
||||||
|
add_test(NAME DriverBenchStateToggle COMMAND DriverBench mc_state_toggle)
|
||||||
|
# The ops-per-frame column is pinned to 46 here, unlike the entry above: the mc_* cases are
|
||||||
|
# excluded from the $DRIVERBENCH_DRAWS scaling on purpose ("the mc_* rates are measured and must
|
||||||
|
# not move, or the numbers stop being comparable", main()), so 46 toggles per frame
|
||||||
|
# is part of what "this case still runs" means. Change the workload and this entry says so.
|
||||||
|
set_tests_properties(DriverBenchStateToggle PROPERTIES
|
||||||
|
LABELS benchmark
|
||||||
|
PASS_REGULAR_EXPRESSION "(^|\n)mc_state_toggle,[0-9]+,46,[0-9.]+,[0-9.]+,[0-9.]+")
|
||||||
|
|||||||
@@ -476,6 +476,28 @@ int main(int argc, char** argv) {
|
|||||||
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
||||||
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||||
|
|
||||||
|
/* A requested case name that matches nothing used to select nothing, print the header row and
|
||||||
|
* exit 0 - so a caller that names a case (run_driver_bench.sh, and the two ctest entries in
|
||||||
|
* CMakeLists.txt) could not tell "the case ran" from "the case has been renamed or deleted".
|
||||||
|
* Refuse it here, before any GL work, so the refusal reaches a caller that has no display
|
||||||
|
* either, and name what does exist so the fix is obvious. */
|
||||||
|
int unknownCases = 0;
|
||||||
|
for (int j = 1; j < argc; ++j) {
|
||||||
|
int known = 0;
|
||||||
|
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||||
|
if (strcmp(argv[j], kBenchCases[i].name) == 0) known = 1;
|
||||||
|
if (!known) {
|
||||||
|
fprintf(stderr, "DriverBench: no case named '%s'\n", argv[j]);
|
||||||
|
unknownCases = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unknownCases) {
|
||||||
|
fprintf(stderr, "DriverBench: the %d cases in kBenchCases are:\n", kBenchCaseCount);
|
||||||
|
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||||
|
fprintf(stderr, " %s\n", kBenchCases[i].name);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
if (boot_egl()) return 1;
|
if (boot_egl()) return 1;
|
||||||
build_resources();
|
build_resources();
|
||||||
|
|
||||||
|
|||||||
@@ -659,6 +659,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
|
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||||
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
|
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||||
@@ -681,6 +682,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
|
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||||
MGP_FILL(DrawElementsInstancedBaseInstance);
|
MGP_FILL(DrawElementsInstancedBaseInstance);
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||||
instancecount, baseinstance);
|
instancecount, baseinstance);
|
||||||
@@ -710,6 +712,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
|
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||||
MGP_FILL(DrawArraysInstancedBaseInstance);
|
MGP_FILL(DrawArraysInstancedBaseInstance);
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||||
baseinstance);
|
baseinstance);
|
||||||
|
|||||||
@@ -16,6 +16,14 @@
|
|||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.h>
|
#include <MG_Impl/Pipe/PipeFill.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a, ID-19(c). This file is the ONLY place every DSA framebuffer entry point lives, and the
|
||||||
|
// emitter it reaches is this package's own header rather than a declaration in one of the
|
||||||
|
// contract's: MG_Pipe/PipeMutation.h is the door MG_State has into the client and carries no
|
||||||
|
// framebuffer row, and MG_Impl/GLImpl and MG_Impl/Pipe are the same layer (this file already
|
||||||
|
// includes MG_Impl/Pipe/PipeFill.h for MGP_FILL).
|
||||||
|
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||||
|
#endif
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||||
@@ -613,6 +621,35 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
|
||||||
|
//
|
||||||
|
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
|
||||||
|
// ID-19 it held only the two BOUND-target records, and the emitter only ever built them
|
||||||
|
// at the validate point out of the two bindings. So glClearNamedFramebufferfv(fbo) or
|
||||||
|
// glBlitNamedFramebuffer(..., fbo, ...) on an fbo bound to NEITHER binding reached a
|
||||||
|
// backend that minted a fresh driver framebuffer with no attachments, found no record
|
||||||
|
// for it, declined, and issued the clear against it anyway: GL_INVALID_FRAMEBUFFER_-
|
||||||
|
// OPERATION and nothing cleared, where the legacy arm cleared correctly.
|
||||||
|
//
|
||||||
|
// TWO CLASSES OF SITE call this, and both are "the point at which the object is final
|
||||||
|
// for this call": the five CONSUMERS (blit and the four clears) publish immediately
|
||||||
|
// before MGP_FILL, so the record precedes the verb that hands the object over and a
|
||||||
|
// later bound-target record for the same object still wins; the ten MUTATORS (the DSA
|
||||||
|
// attachment, draw-buffer and read-buffer setters) publish immediately after the
|
||||||
|
// frontend mutation, because they have no validate point at all - FillPoints.def has no
|
||||||
|
// verb for any of them, so there is no MGP_FILL to sit in front of.
|
||||||
|
//
|
||||||
|
// A CALL THAT MOVED NOTHING IS FREE: the record's ContentHash is the emitter's own
|
||||||
|
// suppressor and it is keyed per framebuffer object, so a redundant publish emits zero
|
||||||
|
// bytes. EmitFramebufferByName picks Draw/Read/Both over Named when the object IS
|
||||||
|
// bound, so a Named record can never overwrite a bound record's Target underneath the
|
||||||
|
// binding that resolves through it.
|
||||||
|
void PipePublishFramebufferByName(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo) {
|
||||||
|
if (!fbo) return;
|
||||||
|
MG_Pipe::MGPipeFramebufferEmitterInstance().EmitFramebufferByName(*fbo);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||||
@@ -631,6 +668,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(readFramebuffer);
|
||||||
|
PipePublishFramebufferByName(drawFramebuffer);
|
||||||
|
#endif
|
||||||
MGP_FILL(BlitNamedFramebuffer);
|
MGP_FILL(BlitNamedFramebuffer);
|
||||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||||
dstY1, mask, filter);
|
dstY1, mask, filter);
|
||||||
@@ -643,6 +684,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebuffer);
|
||||||
|
#endif
|
||||||
MGP_FILL(ClearNamedFramebufferfv);
|
MGP_FILL(ClearNamedFramebufferfv);
|
||||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
@@ -654,6 +698,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebuffer);
|
||||||
|
#endif
|
||||||
MGP_FILL(ClearNamedFramebufferfi);
|
MGP_FILL(ClearNamedFramebufferfi);
|
||||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||||
}
|
}
|
||||||
@@ -665,6 +712,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebuffer);
|
||||||
|
#endif
|
||||||
MGP_FILL(ClearNamedFramebufferiv);
|
MGP_FILL(ClearNamedFramebufferiv);
|
||||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
@@ -676,6 +726,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebuffer);
|
||||||
|
#endif
|
||||||
MGP_FILL(ClearNamedFramebufferuiv);
|
MGP_FILL(ClearNamedFramebufferuiv);
|
||||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
@@ -1404,6 +1457,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (texture == 0) {
|
if (texture == 0) {
|
||||||
framebufferObject->Detach(attachmentType);
|
framebufferObject->Detach(attachmentType);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,6 +1486,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
||||||
@@ -1453,6 +1512,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (texture == 0) {
|
if (texture == 0) {
|
||||||
framebufferObject->Detach(attachmentType);
|
framebufferObject->Detach(attachmentType);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1480,6 +1542,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
||||||
@@ -1534,6 +1599,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (texture == 0) {
|
if (texture == 0) {
|
||||||
framebufferObject->Detach(attachmentType);
|
framebufferObject->Detach(attachmentType);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1636,6 +1704,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||||
/*layered=*/false);
|
/*layered=*/false);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
||||||
@@ -1705,6 +1776,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (renderbuffer == 0) {
|
if (renderbuffer == 0) {
|
||||||
framebufferObject->Detach(attachmentType);
|
framebufferObject->Detach(attachmentType);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1714,6 +1788,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!renderbufferObject) return;
|
if (!renderbufferObject) return;
|
||||||
|
|
||||||
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
||||||
@@ -1913,6 +1990,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
||||||
if (!framebufferObject) return;
|
if (!framebufferObject) return;
|
||||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
||||||
@@ -1927,6 +2007,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLenum bufs[] = {buf};
|
const GLenum bufs[] = {buf};
|
||||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
||||||
@@ -1936,6 +2019,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!framebufferObject) return;
|
if (!framebufferObject) return;
|
||||||
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
||||||
"NamedFramebufferReadBuffer_State");
|
"NamedFramebufferReadBuffer_State");
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
PipePublishFramebufferByName(framebufferObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
#include <MG_Util/Math/FixedPointConversion.h>
|
#include <MG_Util/Math/FixedPointConversion.h>
|
||||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.h>
|
#include <MG_Impl/Pipe/PipeFill.h>
|
||||||
|
// P4a, ID-18 M2. The ONE door MG_State and MG_Impl have into the client's emitters; the three
|
||||||
|
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||||
@@ -1337,6 +1340,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||||
|
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||||
|
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||||
|
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||||
|
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||||
|
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||||
|
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||||
|
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||||
|
// Wrong pixels, not a lost optimisation.
|
||||||
|
//
|
||||||
|
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||||
|
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||||
|
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||||
|
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||||
|
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||||
|
//
|
||||||
|
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||||
|
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||||
|
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||||
|
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||||
|
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||||
|
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
|
||||||
@@ -1415,6 +1442,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||||
|
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||||
|
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||||
|
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||||
|
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||||
|
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||||
|
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||||
|
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||||
|
// Wrong pixels, not a lost optimisation.
|
||||||
|
//
|
||||||
|
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||||
|
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||||
|
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||||
|
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||||
|
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||||
|
//
|
||||||
|
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||||
|
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||||
|
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||||
|
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||||
|
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||||
|
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
@@ -2105,6 +2156,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||||
|
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||||
|
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||||
|
// SetDepthStencilTextureMode; two more move fields MGPTextureParams does not carry. The
|
||||||
|
// last is SamplerObject::BumpVersion, whose own comment calls it "the one choke point
|
||||||
|
// every setter reaches" - and MGPTextureParams takes MinLod, MaxLod and LodBias off that
|
||||||
|
// object, so every glTexParameter that writes GL_TEXTURE_MIN_LOD / MAX_LOD / LOD_BIAS
|
||||||
|
// landed on state nothing watched and the applier's record kept saying MinLod = 0.
|
||||||
|
// Wrong pixels, not a lost optimisation.
|
||||||
|
//
|
||||||
|
// THE HOOK IS HERE RATHER THAN ON BumpVersion because MG_State/GLState/SamplerState is
|
||||||
|
// package C's after the tag; C.7 grants this file for exactly this class of path ("the
|
||||||
|
// grant is one call site per path"), and this switch IS the path - every arm of it
|
||||||
|
// either writes the built-in SamplerObject or writes a texture field that publishes for
|
||||||
|
// itself. Placed after the switch, so the error arms above return without emitting.
|
||||||
|
//
|
||||||
|
// IT IS ALSO ID-14's RE-EMIT HOOK. C's sampler CSO cache is content-addressed, so the
|
||||||
|
// handle MGPTextureParams::BuiltinSampler names MOVES WITH THE CONTENT; the emitter
|
||||||
|
// re-Acquires from the cache and releases the previous handle here. An over-call is
|
||||||
|
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
|
||||||
|
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
|
||||||
|
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/CompositeResolver.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// 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 MGB_CTX_IDENTITY 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramPipelineObject.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// IS THIS PROGRAM A PIPELINE COMPOSITE? A composite is the one ProgramObject in the system
|
||||||
|
// constructed with external index 0 (Core.cpp's MakeShared<ProgramObject>(0u)), and that is
|
||||||
|
// not an accident of implementation: it is deliberately not a named program, so it must not
|
||||||
|
// answer glIsProgram and must not consume a GL name, and glCreateProgram never returns 0.
|
||||||
|
//
|
||||||
|
// ASKED THIS WAY RATHER THAN CARRIED ON THE OBJECT because a Bool member on ProgramObject
|
||||||
|
// would resize the pull build's object and break G1 outright - the phase's admitted-resize
|
||||||
|
// set is empty - and a hook in Core.cpp would have to be maintained on a path that already
|
||||||
|
// states the invariant in its own comment.
|
||||||
|
inline Bool MGPipeProgramIsPipelineComposite(const MG_State::GLState::ProgramObject& program) {
|
||||||
|
return program.GetExternalIndex() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MGPipeCompositeResolver {
|
||||||
|
public:
|
||||||
|
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||||
|
using ProgramPipelineObject = MG_State::GLState::ProgramPipelineObject;
|
||||||
|
using DrawProgramSignature = ProgramPipelineObject::DrawProgramSignature;
|
||||||
|
|
||||||
|
struct Counters {
|
||||||
|
Uint64 Mints = 0; // signatures this resolver has seen minted
|
||||||
|
Uint64 Reuses = 0; // a signature that had not moved
|
||||||
|
Uint64 Releases = 0; // signature-move releases, i.e. the pipeline-cache path
|
||||||
|
// Entries dropped by Reset() because the composite's slot was already gone - the
|
||||||
|
// shape every entry of a DESTROYED CONTEXT ends in. A dropped entry is not a
|
||||||
|
// release: nothing is emitted and nothing is freed, the obligation having been
|
||||||
|
// discharged by the composite's own ~ProgramObject.
|
||||||
|
Uint64 Sweeps = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Told, at every emission, which composite the frontend handed out for which pipeline.
|
||||||
|
// Returns the handle the emitter should use, which is always the one already minted off
|
||||||
|
// the composite's own lifetime id - the resolver never mints a second identity for an
|
||||||
|
// object that has one.
|
||||||
|
//
|
||||||
|
// WHEN THE SIGNATURE MOVES the previous composite's slot is released here, through the
|
||||||
|
// one death helper and in its fixed order. That is the pipeline-cache release path; the
|
||||||
|
// composite's own destructor is the other one and the second of the two is the proven
|
||||||
|
// no-op.
|
||||||
|
MGPipeHandle Observe(Uint64 contextId, const ProgramPipelineObject& pipeline,
|
||||||
|
const ProgramObject& composite, MGPipeHandle handle) {
|
||||||
|
const DrawProgramSignature signature = pipeline.ComputeDrawProgramSignature();
|
||||||
|
const Uint pipelineName = pipeline.GetExternalIndex();
|
||||||
|
Entry* entry = Find(contextId, pipelineName);
|
||||||
|
if (entry != nullptr) {
|
||||||
|
if (entry->Signature == signature && entry->Handle == handle) {
|
||||||
|
// THE SAME COMPOSITE. Not merely "the same signature": the handle is minted
|
||||||
|
// off the composite ProgramObject's own lifetime id, so an identical handle
|
||||||
|
// IS an identical object and there is nothing to release. Live is
|
||||||
|
// deliberately NOT touched - it is the release obligation and it is still
|
||||||
|
// owed for exactly this handle.
|
||||||
|
++m_counters.Reuses;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
// A MOVED SIGNATURE ON THIS CONTEXT's OWN ENTRY, which is the only thing that
|
||||||
|
// can reach here now: another context's pipeline of the same name is not found
|
||||||
|
// above and therefore not released, its obligation staying owed to the context
|
||||||
|
// that took it.
|
||||||
|
ReleaseEntry(*entry);
|
||||||
|
} else {
|
||||||
|
m_entries.push_back(Entry{});
|
||||||
|
entry = &m_entries.back();
|
||||||
|
entry->ContextId = contextId;
|
||||||
|
entry->PipelineName = pipelineName;
|
||||||
|
}
|
||||||
|
entry->Signature = signature;
|
||||||
|
entry->Handle = handle;
|
||||||
|
entry->CompositeLifetimeId = composite.GetLifetimeId();
|
||||||
|
entry->Live = true;
|
||||||
|
++m_counters.Mints;
|
||||||
|
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.
|
||||||
|
void Reset() {
|
||||||
|
SizeT kept = 0;
|
||||||
|
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||||
|
if (!m_entries[i].Live || !MGPipeSlots().IsLive(MGPipeKind::ShaderCso, m_entries[i].Handle)) {
|
||||||
|
++m_counters.Sweeps;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kept != i) m_entries[kept] = m_entries[i];
|
||||||
|
++kept;
|
||||||
|
}
|
||||||
|
m_entries.resize(kept);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() { m_counters = Counters{}; }
|
||||||
|
|
||||||
|
// Diagnostics and unit cases only; nothing on the emission path asks. There is no
|
||||||
|
// HandleFor(name) accessor and there must not be one: the emitter takes the handle from
|
||||||
|
// the composite ProgramObject it already holds, so a lookup by name would be a second
|
||||||
|
// authority on an identity the allocator already owns.
|
||||||
|
SizeT Size() const { return m_entries.size(); }
|
||||||
|
const Counters& GetCounters() const { return m_counters; }
|
||||||
|
|
||||||
|
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.
|
||||||
|
Uint64 ContextId = 0;
|
||||||
|
Uint PipelineName = 0;
|
||||||
|
DrawProgramSignature Signature{};
|
||||||
|
MGPipeHandle Handle = kMGPipeNullHandle;
|
||||||
|
Uint64 CompositeLifetimeId = 0;
|
||||||
|
// THE RELEASE OBLIGATION. Set when this entry takes responsibility for a composite's
|
||||||
|
// slot, cleared ONLY by ReleaseEntry when that responsibility is discharged.
|
||||||
|
Bool Live = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BOTH HALVES OF THE KEY, always. An entry of another context is not this pipeline's
|
||||||
|
// entry: not found, not matched, not released.
|
||||||
|
Entry* Find(Uint64 contextId, Uint pipelineName) {
|
||||||
|
for (Entry& entry : m_entries) {
|
||||||
|
if (entry.ContextId == contextId && entry.PipelineName == pipelineName) return &entry;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseEntry(Entry& entry) {
|
||||||
|
if (!entry.Live || entry.CompositeLifetimeId == 0) return;
|
||||||
|
entry.Live = false;
|
||||||
|
MGPipeEmitShaderCsoDestroyAndFree(entry.CompositeLifetimeId);
|
||||||
|
entry.Handle = kMGPipeNullHandle;
|
||||||
|
entry.CompositeLifetimeId = 0;
|
||||||
|
++m_counters.Releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Entry> m_entries;
|
||||||
|
Counters m_counters;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline MGPipeCompositeResolver& MGPipeCompositeResolverInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason, and named in the phase's risk
|
||||||
|
// list beside the other three new client singletons: heap-constructed and intentionally
|
||||||
|
// leaked at exit, holding no frontend SharedPtr.
|
||||||
|
static MGPipeCompositeResolver* resolver = new MGPipeCompositeResolver();
|
||||||
|
return *resolver;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/CsoCache.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
// Mesa's cso_cache memcmps 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
inline constexpr SizeT kMGPipeCsoCacheCapacity = 64;
|
||||||
|
|
||||||
|
class MGPipeCsoCache {
|
||||||
|
public:
|
||||||
|
struct Counters {
|
||||||
|
Uint64 Mints = 0; // create_render_state emissions
|
||||||
|
// bind_render_state emissions, mint or reuse. Counted in Acquire because Acquire
|
||||||
|
// has exactly ONE caller (PipeFill.cpp's EmitRenderState) and that caller binds
|
||||||
|
// immediately after every call - so "acquisitions" and "binds" are the same
|
||||||
|
// number, and counting it here keeps the count from depending on an emitter
|
||||||
|
// remembering to tick it. mints/binds is the cache's hit rate and it is the
|
||||||
|
// number the CSO content-addressing negative control moves.
|
||||||
|
Uint64 Binds = 0;
|
||||||
|
Uint64 Hits = 0; // a probe that found a live entry and passed the memcmp
|
||||||
|
Uint64 Collisions = 0; // a hash hit the memcmp REJECTED - the reason it exists
|
||||||
|
Uint64 Evictions = 0; // LRU evictions, each one a delete_render_state
|
||||||
|
};
|
||||||
|
|
||||||
|
// The handle for `params`' pipeline subset. Mints and emits create_render_state on a
|
||||||
|
// miss; emits delete_render_state for whatever it evicts to make room. `payloadBytes`
|
||||||
|
// accumulates what went on the wire, for PipeStats::RecordDrawPayloadBytes.
|
||||||
|
MGPipeHandle Acquire(const RenderStateParameters& params, Uint64& payloadBytes) {
|
||||||
|
Array<Uint8, kMGPipePipelineChunkBytes> bytes;
|
||||||
|
MGPipeGatherPipelineBytes(params, bytes.data());
|
||||||
|
++m_counters.Binds;
|
||||||
|
|
||||||
|
const Bool contentAddressed =
|
||||||
|
(MG_Config::Features.PipePush & kMGPipeBehaviourNoCsoContentAddressing) == 0;
|
||||||
|
if (contentAddressed) {
|
||||||
|
const Uint64 hash = s_hashForTest != nullptr ? s_hashForTest(bytes.data())
|
||||||
|
: MGPipeHashPipelineBytes(bytes.data());
|
||||||
|
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||||
|
if (m_entries[i].Hash != hash) continue;
|
||||||
|
if (std::memcmp(m_entries[i].Bytes.data(), bytes.data(), bytes.size()) != 0) {
|
||||||
|
// A 64-bit collision between two DIFFERENT render states. Reusing the
|
||||||
|
// handle here would render one state with the other's pipeline, so the
|
||||||
|
// entry is dropped and the caller mints - correctness first, and the
|
||||||
|
// counter says how often it happened.
|
||||||
|
++m_counters.Collisions;
|
||||||
|
Evict(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_entries[i].LastUsed = ++m_clock;
|
||||||
|
++m_counters.Hits;
|
||||||
|
return m_entries[i].Cso;
|
||||||
|
}
|
||||||
|
return Mint(hash, bytes, payloadBytes);
|
||||||
|
}
|
||||||
|
// Content addressing OFF: never probe, always mint. The records still exist, so
|
||||||
|
// the arm differs from the default one in exactly one thing - whether a handle is
|
||||||
|
// reused - which is what makes it a control rather than a different design.
|
||||||
|
return Mint(0, bytes, payloadBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context teardown, a server reset, a unit test's fixture. Emits nothing: the applier
|
||||||
|
// is reset alongside, and a delete for a record that is about to be dropped anyway
|
||||||
|
// would be a wire message with no reader.
|
||||||
|
void Reset() {
|
||||||
|
for (auto& entry : m_entries) MGPipeSlots().Free(MGPipeKind::RenderStateCso, entry.Cso);
|
||||||
|
m_entries.clear();
|
||||||
|
m_clock = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() { m_counters = Counters{}; }
|
||||||
|
|
||||||
|
SizeT Size() const { return m_entries.size(); }
|
||||||
|
const Counters& GetCounters() const { return m_counters; }
|
||||||
|
|
||||||
|
// TEST SEAM, and it is here because the thing it tests cannot be reached any other
|
||||||
|
// way. A 64-bit collision between two DIFFERENT render states is silent wrong pixels
|
||||||
|
// and it is exactly what the memcmp confirm above exists to stop, so
|
||||||
|
// CsoCacheTest.HashCollisionDoesNotAliasTwoStates has to be able to make one happen.
|
||||||
|
// Null in every real build - one never-taken, perfectly-predicted branch on a path
|
||||||
|
// that runs only when the pipeline version moved, i.e. never in the steady state.
|
||||||
|
using HashForTestFn = Uint64 (*)(const void* pipelineBytes);
|
||||||
|
inline static HashForTestFn s_hashForTest = nullptr;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
Uint64 Hash = 0;
|
||||||
|
Uint64 LastUsed = 0;
|
||||||
|
MGPipeHandle Cso = kMGPipeNullHandle;
|
||||||
|
Array<Uint8, kMGPipePipelineChunkBytes> Bytes{};
|
||||||
|
};
|
||||||
|
|
||||||
|
MGPipeHandle Mint(Uint64 hash, const Array<Uint8, kMGPipePipelineChunkBytes>& bytes,
|
||||||
|
Uint64& payloadBytes) {
|
||||||
|
if (m_entries.size() >= kMGPipeCsoCacheCapacity) {
|
||||||
|
SizeT victim = 0;
|
||||||
|
for (SizeT i = 1; i < m_entries.size(); ++i) {
|
||||||
|
if (m_entries[i].LastUsed < m_entries[victim].LastUsed) victim = i;
|
||||||
|
}
|
||||||
|
Evict(victim);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MGPipeHandle cso = MGPipeSlots().Allocate(MGPipeKind::RenderStateCso);
|
||||||
|
MGPRenderStateDesc desc{};
|
||||||
|
desc.Cso = cso;
|
||||||
|
desc.BaseCso = kMGPipeNullHandle;
|
||||||
|
// A brand-new CSO names every pipeline chunk; the incremental form against a
|
||||||
|
// BaseCso is what the applier's assertion allows and P3 will use once a CSO is
|
||||||
|
// minted from a neighbour rather than from nothing.
|
||||||
|
desc.ChunkMask = kAllPipelineChunks;
|
||||||
|
desc.Blob.Size = kMGPipePipelineChunkBytes;
|
||||||
|
MGPipeApplyCreateRenderState(desc, bytes.data());
|
||||||
|
payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes;
|
||||||
|
|
||||||
|
Entry entry;
|
||||||
|
entry.Hash = hash;
|
||||||
|
entry.LastUsed = ++m_clock;
|
||||||
|
entry.Cso = cso;
|
||||||
|
entry.Bytes = bytes;
|
||||||
|
m_entries.push_back(entry);
|
||||||
|
|
||||||
|
++m_counters.Mints;
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoMints, 1);
|
||||||
|
}
|
||||||
|
return cso;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Evict(SizeT index) {
|
||||||
|
MGPHandleOnly handle{};
|
||||||
|
handle.Handle = m_entries[index].Cso;
|
||||||
|
handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso);
|
||||||
|
MGPipeApplyDeleteRenderState(handle);
|
||||||
|
MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso);
|
||||||
|
m_entries[index] = m_entries.back();
|
||||||
|
m_entries.pop_back();
|
||||||
|
++m_counters.Evictions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr Uint32 kAllPipelineChunks =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1);
|
||||||
|
|
||||||
|
Vector<Entry> m_entries;
|
||||||
|
Uint64 m_clock = 0;
|
||||||
|
Counters m_counters;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The monolith's one cache, held beside the tracker. A Vector scan rather than a hash
|
||||||
|
// map on purpose: 64 entries of Uint64 is a handful of cache lines, it is probed only
|
||||||
|
// when the pipeline version moved, and it keeps the eviction order in the same array as
|
||||||
|
// the content - a map would need a second structure to answer "which is oldest".
|
||||||
|
inline MGPipeCsoCache& MGPipeCsoCacheInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||||
|
// rule covers every MGPipe process singleton, not only the ones on today's death
|
||||||
|
// paths.
|
||||||
|
static MGPipeCsoCache* cache = new MGPipeCsoCache();
|
||||||
|
return *cache;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/FramebufferEmit.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
|
#include <MG_Impl/Pipe/Tracker.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <xxhash.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// TURNING IT ON RETIRES NO PULL. GetFramebufferBindingSlot is the family's one
|
||||||
|
// Coverage.def emitted row and PipeFill.cpp's EmittedCallSuppliesTheWholeField answers
|
||||||
|
// FALSE for it, with the reason: the field's storage is a BindingSlot<FramebufferObject> -
|
||||||
|
// a frontend heap reference - and the call that supplies it carries eight-byte {slot, gen}
|
||||||
|
// handles and a fully resolved descriptor. So this bit switches the EMISSION on and the
|
||||||
|
// residual fill keeps writing the mirror, which is what keeps the verify lane at zero
|
||||||
|
// divergence.
|
||||||
|
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = kMGPipeSubsystemFramebuffer;
|
||||||
|
|
||||||
|
inline Bool MGPipeFramebufferSubsystemEnabled() {
|
||||||
|
return (kMGPipeWiredFramebufferSubsystem & kMGPipeSubsystemFramebuffer) != 0 &&
|
||||||
|
(MG_Config::Features.PipePush & kMGPipeSubsystemFramebuffer) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-C1: the MGPSurface builder, one pure function, one statement per field
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPSurface::Kind's three constants ARE THE CONTRACT'S (ID-12 DV-4, c0c):
|
||||||
|
// kMGPipeSurfaceKindNone / ...Texture / ...Renderbuffer live in MG_Pipe/MGPipeTypes.h under
|
||||||
|
// exactly these names with the same MGPipeKind derivation and the same static_assert. This
|
||||||
|
// package's copies were a redefinition in the same namespace and are deleted.
|
||||||
|
|
||||||
|
// The upload target an attachment names, RESOLVED: an attachment made through an entry
|
||||||
|
// point that carries no face token stores TextureUploadTarget::Unknown, and the record goes
|
||||||
|
// out fully resolved - nothing in it may require a lookup on the far side.
|
||||||
|
//
|
||||||
|
// THE FALLBACK IS ONLY LEGAL FOR A SINGLE-TARGET TEXTURE (m1), and v1's was not. The
|
||||||
|
// precedent it copied - FramebufferAttachmentObject::GetSize - needs an EXTENT, which is
|
||||||
|
// identical across a cube map's six faces; face IDENTITY is not, so
|
||||||
|
// `glFramebufferTexture(GL_COLOR_ATTACHMENT0, cube, 0)` resolved to targets[0] and the
|
||||||
|
// record ASSERTED CubeMapPositiveX for a layered attachment that names all six. A texture
|
||||||
|
// with exactly one upload target has a [0] that IS the truth; anything else keeps Unknown,
|
||||||
|
// which is the value the field already carries for "this attachment names no single face"
|
||||||
|
// and which Layered = 1 tells the reader to ignore.
|
||||||
|
inline MobileGL::TextureUploadTarget MGPipeResolveAttachmentUploadTarget(
|
||||||
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||||
|
MobileGL::TextureUploadTarget resolved = attachment.GetTextureUploadTarget();
|
||||||
|
if (resolved != MobileGL::TextureUploadTarget::Unknown) return resolved;
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
if (!texture) return MobileGL::TextureUploadTarget::Unknown;
|
||||||
|
const auto& targets = texture->GetUploadTargets();
|
||||||
|
return targets.size() == 1 ? targets[0] : MobileGL::TextureUploadTarget::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONE PURE FUNCTION, ONE STATEMENT PER FIELD, and that shape is a gate requirement rather
|
||||||
|
// than taste: G7's scripted control stops this conversion copying exactly one member
|
||||||
|
// (MGPSurface::Layered) and expects the framebuffer suite to go red NAMING that field. A
|
||||||
|
// loop or a memcpy would make the control unanswerable.
|
||||||
|
//
|
||||||
|
// `res` is handed in because resolving it needs the slot allocator and this function stays
|
||||||
|
// pure; `internalFormat` is INLINE in the record on purpose, so the four cross-object masks
|
||||||
|
// fall out at push time with no lookup on the far side.
|
||||||
|
// THE EMPTY POINT IS THE ZERO-INITIALISED RECORD EXCEPT FOR ITS TWO TARGET FIELDS. Both
|
||||||
|
// are Uint16 enumerations whose zero is a REAL value - TextureTarget::Texture1D and
|
||||||
|
// TextureUploadTarget::Texture1D - so a reader that forgot to gate on Kind would read a
|
||||||
|
// plausible wrong answer rather than a nonsense one. Unknown (0xFFFF) is what the contract
|
||||||
|
// spells for TextureTarget (kMGPipeSurfaceNoTextureTarget) and m6 applies the same rule to
|
||||||
|
// UploadTarget, which shares the collision ID-12 DV-3 ruled on for MGPSubData::Target.
|
||||||
|
inline MGPSurface MGPipeEmptySurface() {
|
||||||
|
MGPSurface surface{};
|
||||||
|
surface.UploadTarget = static_cast<Uint16>(MobileGL::TextureUploadTarget::Unknown);
|
||||||
|
surface.TextureTarget = kMGPipeSurfaceNoTextureTarget;
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline MGPSurface MGPipeBuildSurface(const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||||
|
MGPipeHandle res) {
|
||||||
|
MGPSurface surface = MGPipeEmptySurface();
|
||||||
|
if (attachment.IsEmpty()) return surface;
|
||||||
|
surface.Res = res;
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
surface.Kind = kMGPipeSurfaceKindTexture;
|
||||||
|
surface.InternalFormat = static_cast<Uint32>(texture->GetFormat());
|
||||||
|
surface.Layered = attachment.IsLayered() ? 1 : 0;
|
||||||
|
surface.Level = static_cast<Uint16>(std::max<Int>(attachment.GetTextureLevel(), 0));
|
||||||
|
surface.Layer = static_cast<Uint32>(std::max<Int>(attachment.GetTextureLayer(), 0));
|
||||||
|
surface.UploadTarget = static_cast<Uint16>(MGPipeResolveAttachmentUploadTarget(attachment));
|
||||||
|
// ID-12 DV-5: the field that WAS Pad0, and the size did not move. The four
|
||||||
|
// cross-object masks all reduce to (format, TEXTURE TARGET) -
|
||||||
|
// ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha - and no
|
||||||
|
// TextureUploadTarget -> TextureTarget inverse exists anywhere in the tree, so
|
||||||
|
// without this the inline InternalFormat cannot make them fall out at push time and
|
||||||
|
// the backend keeps reading the frontend attachment objects.
|
||||||
|
surface.TextureTarget = static_cast<Uint16>(texture->GetTarget());
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
surface.Kind = kMGPipeSurfaceKindRenderbuffer;
|
||||||
|
surface.InternalFormat = static_cast<Uint32>(renderbuffer->GetInternalFormat());
|
||||||
|
surface.Layered = 0;
|
||||||
|
surface.Level = 0;
|
||||||
|
surface.Layer = 0;
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGPFramebufferState::DrawBuffers[i]: an index INTO THIS RECORD'S OWN Color[] array, and
|
||||||
|
// -1 for NONE, which is the field's documented convention read literally.
|
||||||
|
//
|
||||||
|
// THE FOUR DEFAULT-FRAMEBUFFER TOKENS map to 0, and that is a deliberate narrowing rather
|
||||||
|
// than an oversight: a default framebuffer has one colour surface, this record carries it
|
||||||
|
// in Color[0] (see MGPipeBuildFramebufferState), and IsDefault is what tells the server
|
||||||
|
// which framebuffer it is looking at. The distinction the narrowing loses is FRONT versus
|
||||||
|
// BACK and LEFT versus RIGHT, which MobileGL's frontend never gives a default framebuffer
|
||||||
|
// in the first place - FramebufferObject's constructor seeds BackLeft and nothing writes
|
||||||
|
// another. A phase that needs stereo has to widen the field, not re-encode this one.
|
||||||
|
inline Int8 MGPipeDrawBufferIndex(MobileGL::FramebufferAttachmentType buffer) {
|
||||||
|
using MobileGL::FramebufferAttachmentType;
|
||||||
|
if (buffer == FramebufferAttachmentType::None) return -1;
|
||||||
|
if (buffer >= FramebufferAttachmentType::Color0 && buffer <= FramebufferAttachmentType::ColorMax) {
|
||||||
|
return static_cast<Int8>(static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// m2: A DRAW-BUFFER TOKEN CAN NAME A COLOUR POINT THE RECORD CANNOT CARRY, and D-C3's
|
||||||
|
// refusal loop only ever scanned ATTACHMENTS. `glDrawBuffers(1, {GL_COLOR_ATTACHMENT10})`
|
||||||
|
// with nothing attached at 10 is legal state - draw-incomplete, but legal - and the index
|
||||||
|
// above would have written 10 into a record whose Color[] is 8 wide, so the server would
|
||||||
|
// index out of its own storage or invent a bound the record does not carry. Truncating
|
||||||
|
// silently is the bug class this phase is closing, so the record is refused exactly as an
|
||||||
|
// over-wide attachment is.
|
||||||
|
inline Bool MGPipeDrawBufferIsInsideTheWireWidth(MobileGL::FramebufferAttachmentType buffer) {
|
||||||
|
using MobileGL::FramebufferAttachmentType;
|
||||||
|
if (buffer < FramebufferAttachmentType::Color0 || buffer > FramebufferAttachmentType::ColorMax) {
|
||||||
|
return true; // None and the four default-framebuffer tokens; neither indexes Color[]
|
||||||
|
}
|
||||||
|
return static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0) <
|
||||||
|
static_cast<Int>(kMGPipeMaxColorAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-C4: ContentHash, and the one input it must not swallow
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
inline void MGPipeCopySurfaceForHash(MGPSurface& dst, const MGPSurface& src) {
|
||||||
|
dst.Res = src.Res;
|
||||||
|
dst.InternalFormat = src.InternalFormat;
|
||||||
|
dst.Kind = src.Kind;
|
||||||
|
dst.Layered = src.Layered;
|
||||||
|
dst.Level = src.Level;
|
||||||
|
dst.Layer = src.Layer;
|
||||||
|
dst.UploadTarget = src.UploadTarget;
|
||||||
|
// MANDATORY, not optional: TextureTarget is a PipeFields.def row now, so a
|
||||||
|
// field-wise copy that skipped it would suppress a record whose only moved field is
|
||||||
|
// the attachment's texture target - and that field decides three of the four
|
||||||
|
// cross-object masks.
|
||||||
|
dst.TextureTarget = src.TextureTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Uint64 MGPipeFramebufferStateContentHash(const MGPFramebufferState& state) {
|
||||||
|
MGPFramebufferState staging{};
|
||||||
|
staging.Fbo = state.Fbo;
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
MGPipeCopySurfaceForHash(staging.Color[i], state.Color[i]);
|
||||||
|
}
|
||||||
|
MGPipeCopySurfaceForHash(staging.Depth, state.Depth);
|
||||||
|
MGPipeCopySurfaceForHash(staging.Stencil, state.Stencil);
|
||||||
|
MGPipeCopySurfaceForHash(staging.ReadSurface, state.ReadSurface);
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
staging.DrawBuffers[i] = state.DrawBuffers[i];
|
||||||
|
}
|
||||||
|
staging.Width = state.Width;
|
||||||
|
staging.Height = state.Height;
|
||||||
|
staging.Layers = state.Layers;
|
||||||
|
staging.Samples = state.Samples;
|
||||||
|
staging.FixedSampleLocations = state.FixedSampleLocations;
|
||||||
|
staging.IsDefault = state.IsDefault;
|
||||||
|
staging.Complete = state.Complete;
|
||||||
|
staging.Target = state.Target;
|
||||||
|
// staging.ContentHash stays 0 - that is the whole point.
|
||||||
|
return XXH64(&staging, sizeof(staging), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The emitter
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MGPipeFramebufferEmitter {
|
||||||
|
public:
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||||
|
using FramebufferAttachmentType = MobileGL::FramebufferAttachmentType;
|
||||||
|
|
||||||
|
// The handle for `fbo`. kMGPipeDefaultFramebuffer ({0,1}) for the default framebuffer,
|
||||||
|
// which is what retires the four pDefaultFramebufferInfo->defaultFBO identity
|
||||||
|
// comparisons into an ordinary handle compare; a client-minted {slot, gen} otherwise.
|
||||||
|
//
|
||||||
|
// Minted, never gated: a framebuffer handle is CLIENT state and costs one free-list pop.
|
||||||
|
static MGPipeHandle HandleFor(const FramebufferObject& fbo) {
|
||||||
|
if (fbo.IsDefaultFramebuffer()) return kMGPipeDefaultFramebuffer;
|
||||||
|
return MGPipeSlots().Acquire(MGPipeKind::Framebuffer, fbo.GetLifetimeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||||
|
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||||
|
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
|
||||||
|
const auto& drawFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).GetBoundObject();
|
||||||
|
const auto& readFbo = ctx.GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).GetBoundObject();
|
||||||
|
if (!drawFbo && !readFbo) return 0;
|
||||||
|
|
||||||
|
// ONE OBJECT BOUND TO BOTH TARGETS IS ONE RECORD WITH Target = Both, and that is
|
||||||
|
// not an optimisation: Espryt's "same FBO as draw" skip is the habitat of the
|
||||||
|
// read-buffer defect class, and a record that says which target it describes turns
|
||||||
|
// "apply the draw buffers only for the draw target" from call-site discipline into
|
||||||
|
// a one-line test on the far side.
|
||||||
|
const Bool shared = drawFbo && readFbo && drawFbo.get() == readFbo.get();
|
||||||
|
|
||||||
|
MGPFramebufferState drawState{};
|
||||||
|
MGPFramebufferState readState{};
|
||||||
|
Bool drawOk = false;
|
||||||
|
Bool readOk = false;
|
||||||
|
if (shared) {
|
||||||
|
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
|
||||||
|
} else {
|
||||||
|
if (drawFbo) {
|
||||||
|
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
|
||||||
|
}
|
||||||
|
if (readFbo) {
|
||||||
|
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!drawOk && !readOk) return 0;
|
||||||
|
|
||||||
|
// THE SUPPRESSOR SLOT IS FED THE COMBINED ANSWER and the per-target latches decide
|
||||||
|
// which of the two records actually goes out. The slot exists so that
|
||||||
|
// InvalidateAll() on a fresh context reaches this family like every other, and so
|
||||||
|
// that "nothing moved" costs one compare rather than two.
|
||||||
|
const Uint64 drawHash = drawOk ? drawState.ContentHash : 0;
|
||||||
|
const Uint64 readHash = readOk ? readState.ContentHash : 0;
|
||||||
|
const Uint64 combined =
|
||||||
|
MGPipeMixShutter(MGPipeMixShutter(drawHash, readHash), shared ? 1u : 0u);
|
||||||
|
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetFramebufferState,
|
||||||
|
combined)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
if (shared) {
|
||||||
|
if (drawOk && (drawHash != m_lastEmitted[kDraw] || drawHash != m_lastEmitted[kRead])) {
|
||||||
|
bytes += Emit(drawState);
|
||||||
|
m_lastEmitted[kDraw] = drawHash;
|
||||||
|
m_lastEmitted[kRead] = drawHash;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
if (drawOk && drawHash != m_lastEmitted[kDraw]) {
|
||||||
|
bytes += Emit(drawState);
|
||||||
|
m_lastEmitted[kDraw] = drawHash;
|
||||||
|
}
|
||||||
|
if (readOk && readHash != m_lastEmitted[kRead]) {
|
||||||
|
bytes += Emit(readState);
|
||||||
|
m_lastEmitted[kRead] = readHash;
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
Uint64 EmitFramebufferByName(const FramebufferObject& fbo) {
|
||||||
|
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
|
||||||
|
MGPipeFramebufferTarget target = MGPipeFramebufferTarget::Named;
|
||||||
|
const Bool boundToDraw = IsBoundTo(fbo, MobileGL::FramebufferTarget::Draw);
|
||||||
|
const Bool boundToRead = IsBoundTo(fbo, MobileGL::FramebufferTarget::Read);
|
||||||
|
if (boundToDraw && boundToRead) {
|
||||||
|
target = MGPipeFramebufferTarget::Both;
|
||||||
|
} else if (boundToDraw) {
|
||||||
|
target = MGPipeFramebufferTarget::Draw;
|
||||||
|
} else if (boundToRead) {
|
||||||
|
target = MGPipeFramebufferTarget::Read;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPFramebufferState state{};
|
||||||
|
if (!BuildFramebufferState(fbo, target, state)) return 0;
|
||||||
|
|
||||||
|
// THE SUPPRESSOR IS KEYED BY THE FRAMEBUFFER THE RECORD NAMES, never by one global
|
||||||
|
// slot (MGPipeTypes.h states the rule): 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. Target is a ContentHash input, so the
|
||||||
|
// second half holds by construction; the per-object table is what buys the first.
|
||||||
|
// The two BOUND latches stay what they are - "does the server's draw/read binding
|
||||||
|
// already hold this record" - and a bound-target emission from here consults them,
|
||||||
|
// because a rebind of an unchanged object must still move the binding.
|
||||||
|
if (target == MGPipeFramebufferTarget::Named) {
|
||||||
|
NamedEntry& entry = NamedEntryFor(state.Fbo);
|
||||||
|
if (entry.Has && entry.Gen == state.Fbo.Gen && entry.LastHash == state.ContentHash) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const Uint64 bytes = Emit(state);
|
||||||
|
entry.Has = true;
|
||||||
|
entry.Gen = state.Fbo.Gen;
|
||||||
|
entry.LastHash = state.ContentHash;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
if (target == MGPipeFramebufferTarget::Both) {
|
||||||
|
if (state.ContentHash == m_lastEmitted[kDraw] && state.ContentHash == m_lastEmitted[kRead]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const Uint64 bytes = Emit(state);
|
||||||
|
m_lastEmitted[kDraw] = state.ContentHash;
|
||||||
|
m_lastEmitted[kRead] = state.ContentHash;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
const SizeT slot = target == MGPipeFramebufferTarget::Read ? kRead : kDraw;
|
||||||
|
if (state.ContentHash == m_lastEmitted[slot]) return 0;
|
||||||
|
const Uint64 bytes = Emit(state);
|
||||||
|
m_lastEmitted[slot] = state.ContentHash;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
||||||
|
// same objects, so "what was emitted" costs no copy. ----
|
||||||
|
// ---- the death half (P4a final review C-2) ----
|
||||||
|
//
|
||||||
|
// Called by the contract's death helper before the slot is freed (there is no wire
|
||||||
|
// delete for this kind, D-I2, so this is the only client-side thing a framebuffer's
|
||||||
|
// death has to do). The per-object Named latch is the entry: a recycled handle's Gen
|
||||||
|
// already refuses the stale latch, so this is hygiene rather than a fix - the rule
|
||||||
|
// (ID-8) is that whatever mints a handle retires everything it keeps under it at the
|
||||||
|
// death, and every P4a kind takes the same shape. Gen-keyed for a late notice.
|
||||||
|
void NoteFramebufferDied(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return;
|
||||||
|
if (m_named[slot].Gen == handle.Gen) m_named[slot] = NamedEntry{};
|
||||||
|
}
|
||||||
|
// "Does this emitter hold a Named-record latch for this handle at its generation."
|
||||||
|
Bool NamedRecordIsLatched(MGPipeHandle handle) const {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return false;
|
||||||
|
return m_named[slot].Has && m_named[slot].Gen == handle.Gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
||||||
|
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
||||||
|
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||||
|
Uint64 EmissionCount() const { return m_emissions; }
|
||||||
|
Uint64 RefusedCount() const { return m_refusals; }
|
||||||
|
|
||||||
|
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||||
|
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
|
||||||
|
// ReadFramebuffer working state, so these mirrors have to go with them or the first
|
||||||
|
// emission after a make-current would be suppressed as unchanged and the server would
|
||||||
|
// draw into the previous context's framebuffer. The suppressor slot is invalidated by
|
||||||
|
// the validate point's own InvalidateAll(), beside this call.
|
||||||
|
void Reset() {
|
||||||
|
m_lastEmitted[kDraw] = 0;
|
||||||
|
m_lastEmitted[kRead] = 0;
|
||||||
|
// The per-object latch goes too, and the safe direction is why: MGPipeApplierReset
|
||||||
|
// keeps FramebufferRecords standing (they are object state, ID-19(b)) but
|
||||||
|
// ReleaseObjectRecords clears the whole table, and this emitter cannot tell the two
|
||||||
|
// scopes apart from here. Keeping a latch across a table that may have been dropped
|
||||||
|
// would suppress the one record that had to go out; dropping it costs one extra
|
||||||
|
// 304-byte record per named framebuffer after a context switch.
|
||||||
|
m_named.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() { m_emissions = m_refusals = 0; }
|
||||||
|
|
||||||
|
void ResetForTest() {
|
||||||
|
Reset();
|
||||||
|
ResetCounters();
|
||||||
|
m_lastDraw = MGPFramebufferState{};
|
||||||
|
m_lastRead = MGPFramebufferState{};
|
||||||
|
m_lastNamed = MGPFramebufferState{};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr SizeT kDraw = 0;
|
||||||
|
static constexpr SizeT kRead = 1;
|
||||||
|
|
||||||
|
Uint64 Emit(const MGPFramebufferState& state) {
|
||||||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Named)) {
|
||||||
|
m_lastNamed = state;
|
||||||
|
} else if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read)) {
|
||||||
|
m_lastRead = state;
|
||||||
|
} else {
|
||||||
|
m_lastDraw = state;
|
||||||
|
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
|
||||||
|
}
|
||||||
|
MGPipeApplySetFramebufferState(state);
|
||||||
|
++m_emissions;
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
|
||||||
|
}
|
||||||
|
return sizeof(MGPFramebufferState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by `fbo` - and every
|
||||||
|
// field in it is a property of THAT object. Target is the only binding-specific one.
|
||||||
|
//
|
||||||
|
// ReadSurface IS RESOLVED FROM THIS FRAMEBUFFER'S OWN READ BUFFER UNDER EVERY TARGET,
|
||||||
|
// Named included (c0e / MGPipeTypes.h). v1 resolved a Draw record's ReadSurface from
|
||||||
|
// the READ-bound object, which was D-C2's letter and muddled in substance: the record
|
||||||
|
// then described a surface that is not part of the framebuffer its own Fbo names, and a
|
||||||
|
// glReadBuffer on the read FBO moved the DRAW record's ContentHash and forced a
|
||||||
|
// redundant draw emission. Resolving it per object is what makes the
|
||||||
|
// read-buffer-shared-FBO defect class unrepresentable rather than merely fixed - the
|
||||||
|
// record carries a surface, not an index, and no field of it refers to "whatever is
|
||||||
|
// bound".
|
||||||
|
Bool BuildFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
|
||||||
|
MGPFramebufferState& out) {
|
||||||
|
// D-C3, THE CLIENT HALF OF THE BRING-UP REFUSAL. The wire array is 8 wide and
|
||||||
|
// GetDynamicParameters().MaxColorAttachments is the driver's raw ES cap, not
|
||||||
|
// clamped to 8 on the GLES path. An attachment point at or above the wire width
|
||||||
|
// cannot be carried at all, so the record is REFUSED and the legacy arm runs -
|
||||||
|
// truncating it silently is exactly the bug class this phase is closing. The
|
||||||
|
// backend half of the same refusal (bit 9 declined at its first lookup, with one
|
||||||
|
// ERROR naming the cap) rides ResolveFramebufferSubsystemArm.
|
||||||
|
for (Int point = static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||||
|
static_cast<Int>(kMGPipeMaxColorAttachments);
|
||||||
|
point <= static_cast<Int>(FramebufferAttachmentType::ColorMax); ++point) {
|
||||||
|
if (fbo.GetAttachment(static_cast<FramebufferAttachmentType>(point)).IsEmpty()) continue;
|
||||||
|
MGLOG_E_ONCE("MGPipe: framebuffer %u has an attachment at colour point %d, which is at or "
|
||||||
|
"above the wire width of %u - set_framebuffer_state is refused rather than "
|
||||||
|
"truncated and the legacy arm runs",
|
||||||
|
fbo.GetExternalIndex(),
|
||||||
|
point - static_cast<Int>(FramebufferAttachmentType::Color0),
|
||||||
|
static_cast<Uint>(kMGPipeMaxColorAttachments));
|
||||||
|
++m_refusals;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// m2, THE SAME REFUSAL ONE FIELD OVER. A draw-buffer token may name a colour point
|
||||||
|
// at or above the wire width with nothing attached there, which the loop above
|
||||||
|
// cannot see; MGPipeDrawBufferIndex would then write 8..31 into an 8-wide array.
|
||||||
|
{
|
||||||
|
const auto& tokens = fbo.GetDrawBuffers();
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
if (MGPipeDrawBufferIsInsideTheWireWidth(tokens[i])) continue;
|
||||||
|
MGLOG_E_ONCE("MGPipe: framebuffer %u names colour point %d in draw buffer %u, which is "
|
||||||
|
"at or above the wire width of %u - set_framebuffer_state is refused "
|
||||||
|
"rather than truncated and the legacy arm runs",
|
||||||
|
fbo.GetExternalIndex(),
|
||||||
|
static_cast<Int>(tokens[i]) -
|
||||||
|
static_cast<Int>(FramebufferAttachmentType::Color0),
|
||||||
|
static_cast<Uint>(i), static_cast<Uint>(kMGPipeMaxColorAttachments));
|
||||||
|
++m_refusals;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = MGPFramebufferState{};
|
||||||
|
out.Fbo = HandleFor(fbo);
|
||||||
|
out.Target = static_cast<Uint8>(target);
|
||||||
|
out.IsDefault = fbo.IsDefaultFramebuffer() ? 1 : 0;
|
||||||
|
|
||||||
|
// THE COLOUR POINTS. A default framebuffer keeps its one colour surface under
|
||||||
|
// BackLeft rather than under Color0, and the record has exactly one place to put
|
||||||
|
// it: Color[0], which is also the index MGPipeDrawBufferIndex maps that token to,
|
||||||
|
// so the array and the draw-buffer indices agree by construction.
|
||||||
|
if (out.IsDefault != 0) {
|
||||||
|
out.Color[0] = SurfaceOf(fbo, FramebufferAttachmentType::BackLeft);
|
||||||
|
} else {
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
out.Color[i] = SurfaceOf(fbo, static_cast<FramebufferAttachmentType>(
|
||||||
|
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||||
|
static_cast<Int>(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Depth = SurfaceOf(fbo, FramebufferAttachmentType::Depth);
|
||||||
|
out.Stencil = SurfaceOf(fbo, FramebufferAttachmentType::Stencil);
|
||||||
|
out.ReadSurface = SurfaceOf(fbo, fbo.GetReadBuffer());
|
||||||
|
|
||||||
|
const auto& drawBuffers = fbo.GetDrawBuffers();
|
||||||
|
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||||
|
out.DrawBuffers[i] = MGPipeDrawBufferIndex(drawBuffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
FillGeometry(fbo, out);
|
||||||
|
// Complete is FramebufferObject::CheckCompleteness(), the FRONTEND-ONLY answer, and
|
||||||
|
// never glCheckFramebufferStatus's: that entry point additionally consults the
|
||||||
|
// backend's probed format-capability cache, and a client emitting it would be
|
||||||
|
// reading the backend from the client side - the exact coupling this boundary
|
||||||
|
// exists to remove. glCheckFramebufferStatus keeps answering from the frontend
|
||||||
|
// exactly as it does today.
|
||||||
|
out.Complete = fbo.CheckCompleteness() ? 1 : 0;
|
||||||
|
out.ContentHash = MGPipeFramebufferStateContentHash(out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
|
||||||
|
if (MG_State::pGLContext == nullptr) return false;
|
||||||
|
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
|
||||||
|
return bound && bound.get() == &fbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NamedEntry {
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Uint64 LastHash = 0;
|
||||||
|
Bool Has = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
NamedEntry& NamedEntryFor(MGPipeHandle fbo) {
|
||||||
|
const SizeT slot = fbo.Slot;
|
||||||
|
if (slot >= m_named.size()) m_named.resize(slot + 1);
|
||||||
|
return m_named[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPSurface SurfaceOf(const FramebufferObject& fbo, FramebufferAttachmentType type) {
|
||||||
|
if (type == FramebufferAttachmentType::None || type == FramebufferAttachmentType::Unknown) {
|
||||||
|
return MGPipeEmptySurface();
|
||||||
|
}
|
||||||
|
const auto& attachment = fbo.GetAttachment(type);
|
||||||
|
if (attachment.IsEmpty()) return MGPipeEmptySurface();
|
||||||
|
MGPipeTextureEmitter& textures = MGPipeTextureEmitterInstance();
|
||||||
|
// D-A4's two producers: an attachment point is what sets RENDER_TARGET and
|
||||||
|
// DEPTH_STENCIL, the two sticky bind bits nothing set before P4a. Sticky and ORed,
|
||||||
|
// so a texture that was ever a colour attachment keeps saying so, and the mask is
|
||||||
|
// republished on the resource's next respecify.
|
||||||
|
const Uint16 bit = (type == FramebufferAttachmentType::Depth ||
|
||||||
|
type == FramebufferAttachmentType::Stencil)
|
||||||
|
? static_cast<Uint16>(kMGPipeBindDepthStencil)
|
||||||
|
: static_cast<Uint16>(kMGPipeBindRenderTarget);
|
||||||
|
MGPipeHandle res = kMGPipeNullHandle;
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
res = textures.AcquireTexture(texture->GetLifetimeId(), texture.get());
|
||||||
|
textures.NoteTextureBoundAs(res, bit);
|
||||||
|
} else if (attachment.IsRenderbuffer()) {
|
||||||
|
const auto& renderbuffer = attachment.GetRenderbuffer();
|
||||||
|
res = textures.AcquireRenderbuffer(renderbuffer->GetLifetimeId());
|
||||||
|
textures.NoteRenderbufferBoundAs(res, bit);
|
||||||
|
}
|
||||||
|
return MGPipeBuildSurface(attachment, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The attachments' common extent, and the ARB_framebuffer_no_attachments defaults when
|
||||||
|
// there is no attachment at all (GL 4.6 core table 23.24 - the shape a framebuffer with
|
||||||
|
// no attachments rasterizes at).
|
||||||
|
static void FillGeometry(const FramebufferObject& fbo, MGPFramebufferState& out) {
|
||||||
|
Bool found = false;
|
||||||
|
for (const auto& attachment : fbo.GetAllAttachmentObjects()) {
|
||||||
|
if (attachment.IsEmpty()) continue;
|
||||||
|
const IntVec3 size = attachment.GetSize();
|
||||||
|
if (!found) {
|
||||||
|
out.Width = static_cast<Uint16>(std::clamp<Int>(size.x(), 0, 0xFFFF));
|
||||||
|
out.Height = static_cast<Uint16>(std::clamp<Int>(size.y(), 0, 0xFFFF));
|
||||||
|
out.Layers = static_cast<Uint16>(
|
||||||
|
attachment.IsLayered() ? std::clamp<Int>(size.z(), 1, 0xFFFF) : 1);
|
||||||
|
if (attachment.IsTexture()) {
|
||||||
|
const auto& texture = attachment.GetTexture();
|
||||||
|
out.Samples = static_cast<Uint16>(std::max<Int>(texture->GetSamples(), 0));
|
||||||
|
out.FixedSampleLocations = texture->HasFixedSampleLocations() ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
out.Samples = static_cast<Uint16>(
|
||||||
|
std::max<Int>(attachment.GetRenderbuffer()->GetSamples(), 0));
|
||||||
|
out.FixedSampleLocations = 1;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) return;
|
||||||
|
out.Width = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultWidth(), 0, 0xFFFF));
|
||||||
|
out.Height = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultHeight(), 0, 0xFFFF));
|
||||||
|
out.Layers = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultLayers(), 0, 0xFFFF));
|
||||||
|
out.Samples = static_cast<Uint16>(std::clamp<Int>(fbo.GetDefaultSamples(), 0, 0xFFFF));
|
||||||
|
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<Uint64, 2> m_lastEmitted{};
|
||||||
|
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
|
||||||
|
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
|
||||||
|
// (D-I2), so a successor simply overwrites its predecessor's entry.
|
||||||
|
Vector<NamedEntry> m_named;
|
||||||
|
MGPFramebufferState m_lastDraw{};
|
||||||
|
MGPFramebufferState m_lastRead{};
|
||||||
|
MGPFramebufferState m_lastNamed{};
|
||||||
|
Uint64 m_emissions = 0;
|
||||||
|
Uint64 m_refusals = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||||
|
// rule covers every MGPipe process singleton, not only the ones a frontend destructor
|
||||||
|
// reaches today, and it is what keeps exit() out of a torn-down pipe.
|
||||||
|
static MGPipeFramebufferEmitter* emitter = new MGPipeFramebufferEmitter();
|
||||||
|
return *emitter;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/ImageEmit.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||||
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <xxhash.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// D-G3. Over the tail with Start and Count mixed in, the same shape the two sampler sets
|
||||||
|
// use - and it covers InternalFormat and Access because those are live glBindImageTexture
|
||||||
|
// state that the format-less image bake keys on, not decoration.
|
||||||
|
inline Uint64 MGPipeShaderImageSetContentHash(const MGPImageView* entries, Uint32 start, Uint32 count) {
|
||||||
|
Uint64 hash = XXH64(entries, static_cast<SizeT>(count) * sizeof(MGPImageView), 0);
|
||||||
|
hash = MGPipeMixShutter(hash, start);
|
||||||
|
hash = MGPipeMixShutter(hash, count);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MGPipeImageEmitter {
|
||||||
|
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 the frontend has none and 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; TextureState::NoteUnitTouched is the TEXTURE-unit path and
|
||||||
|
// glBindImageTexture does not reach it. Adding a counter to TextureState would edit
|
||||||
|
// another package's file and resize the pull build's object, which G1 forbids outright.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||||
|
const auto& program = ctx.GetProgramForDraw();
|
||||||
|
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(program.get());
|
||||||
|
const Uint32 programWindow =
|
||||||
|
resolution.MaxImageUnit < 0 ? 0u : static_cast<Uint32>(resolution.MaxImageUnit) + 1u;
|
||||||
|
if (programWindow > m_window) m_window = programWindow;
|
||||||
|
const Uint32 count = m_window < kMGPipeMaxImageUnits ? m_window : kMGPipeMaxImageUnits;
|
||||||
|
// PROPERTY 1, and it is one integer test on every draw of every application that
|
||||||
|
// never binds an image.
|
||||||
|
if (count == 0) return 0;
|
||||||
|
|
||||||
|
for (Uint32 unit = 0; unit < count; ++unit) {
|
||||||
|
const auto& binding = ctx.GetImageTextureBinding(static_cast<Int>(unit));
|
||||||
|
MGPImageView& entry = m_entries[unit];
|
||||||
|
entry = MGPImageView{};
|
||||||
|
entry.Unit = unit;
|
||||||
|
entry.Res = binding.Texture ? MGPipeSlots().Acquire(MGPipeKind::Texture,
|
||||||
|
binding.Texture->GetLifetimeId())
|
||||||
|
: kMGPipeNullHandle;
|
||||||
|
// D-A4: a texture named in an emitted MGPImageView is SHADER-IMAGE-bound from
|
||||||
|
// then on - the bit ImageBindableHint is derived from. The bind itself noted it
|
||||||
|
// first (TextureState.h, so the hint precedes the first sync); this is the
|
||||||
|
// letter of the rule and a one-compare early-out once the bit is set.
|
||||||
|
if (!MGPipeHandleIsNull(entry.Res)) {
|
||||||
|
MGPipeNoteTextureBoundAs(entry.Res, static_cast<Uint32>(kMGPipeBindShaderImage));
|
||||||
|
}
|
||||||
|
// THE APPLICATION's format and access, verbatim. The bind-format recast and the
|
||||||
|
// buffer-texture split view are server-side and stay there; so does
|
||||||
|
// SupportsLayeredImageBinding's rule, which asks the BACKEND target after
|
||||||
|
// MapToBackendTextureTarget and forces layer to 0 for a non-layerable one -
|
||||||
|
// Adreno took a stray layer index literally. A client that pre-applied any of
|
||||||
|
// that would be answering a driver question from the wrong side.
|
||||||
|
entry.InternalFormat = static_cast<Uint32>(binding.Format);
|
||||||
|
entry.Layer = static_cast<Uint32>(binding.Layer);
|
||||||
|
entry.Level = static_cast<Uint16>(binding.Level);
|
||||||
|
entry.Layered = binding.Layered != GL_FALSE ? 1 : 0;
|
||||||
|
entry.Access = static_cast<Uint8>(MGPipeEncodeImageAccess(binding.Access));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 hash = MGPipeShaderImageSetContentHash(m_entries.data(), 0, count);
|
||||||
|
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetShaderImages, hash)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_lastImages = MGPShaderImages{};
|
||||||
|
m_lastImages.Start = 0;
|
||||||
|
m_lastImages.Count = count;
|
||||||
|
m_lastImages.ContentHash = hash;
|
||||||
|
MGPipeApplySetShaderImages(m_lastImages, m_entries.data());
|
||||||
|
++m_imageSets;
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1);
|
||||||
|
}
|
||||||
|
return sizeof(MGPShaderImages) + static_cast<Uint64>(count) * sizeof(MGPImageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The validate point's FreshlyPrimed arm. A fresh context is a fresh set of image
|
||||||
|
// bindings, so the sticky window starts over; the suppressor slot this set latches is
|
||||||
|
// invalidated beside this call. There is no record half here at all - set_shader_images
|
||||||
|
// is pure working state and mints no object of its own.
|
||||||
|
void Reset() { m_window = 0; }
|
||||||
|
|
||||||
|
void ResetCounters() { m_imageSets = 0; }
|
||||||
|
|
||||||
|
const MGPShaderImages& LastShaderImages() const { return m_lastImages; }
|
||||||
|
const Array<MGPImageView, kMGPipeMaxImageUnits>& LastImageViews() const { return m_entries; }
|
||||||
|
Uint64 ImageSetCount() const { return m_imageSets; }
|
||||||
|
Uint32 Window() const { return m_window; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// GL_READ_ONLY / GL_WRITE_ONLY / GL_READ_WRITE folded into the one byte the wire
|
||||||
|
// carries. A value the enum does not name would otherwise truncate silently into a
|
||||||
|
// Uint8, which is the class of bug the descriptors exist to close.
|
||||||
|
static Uint32 MGPipeEncodeImageAccess(GLenum access) {
|
||||||
|
switch (access) {
|
||||||
|
case GL_READ_ONLY:
|
||||||
|
return 0;
|
||||||
|
case GL_WRITE_ONLY:
|
||||||
|
return 1;
|
||||||
|
case GL_READ_WRITE:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
MOBILEGL_ASSERT(false, "glBindImageTexture access 0x%x is not one of the three GL names",
|
||||||
|
static_cast<Uint>(access));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<MGPImageView, kMGPipeMaxImageUnits> m_entries{};
|
||||||
|
MGPShaderImages m_lastImages{};
|
||||||
|
Uint32 m_window = 0;
|
||||||
|
Uint64 m_imageSets = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline MGPipeImageEmitter& MGPipeImageEmitterInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||||
|
// intentionally leaked at exit, like every other MGPipe process singleton.
|
||||||
|
static MGPipeImageEmitter* emitter = new MGPipeImageEmitter();
|
||||||
|
return *emitter;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,21 @@
|
|||||||
namespace MobileGL::MG_Pipe {
|
namespace MobileGL::MG_Pipe {
|
||||||
struct PipeInputs;
|
struct PipeInputs;
|
||||||
|
|
||||||
// PipeFill.cpp. Bumps the per-verb serial, records the verb and the context identity,
|
// PipeFill.cpp. THE VALIDATE POINT (ARCHITECTURE.md 5.1, P2 brief D1). In order:
|
||||||
// and copies every field in the verb class's may-read mask (kMGPipeClassFieldMask) out
|
// 1. bump the per-verb serial, record the verb and the context identity;
|
||||||
// of the live GLContext, stamping each with the new serial. In a verify build it then
|
// 2. run the tracker's DIRTY WALK for this verb's class (MG_Impl/Pipe/Tracker.h);
|
||||||
// runs the entry compare against a second snapshot (P1 brief D8).
|
// 3. EMIT, for each set dirty bit whose subsystem bit is on in the runtime
|
||||||
void MGPipeFillForVerb(MGPipeVerb verb);
|
// MOBILEGL_PIPE_PUSH bitmask, the P2 call that carries it;
|
||||||
|
// 4. run the P1 residual fill for every field an emitted call did NOT supply,
|
||||||
|
// stamping each with the new serial exactly as before;
|
||||||
|
// 5. in a verify build, the entry compare against a second snapshot (P1 brief D8) -
|
||||||
|
// which stops being a tautology the moment step 3 supplies a field step 4 skips.
|
||||||
|
//
|
||||||
|
// It was MGPipeFillForVerb through P1, when steps 2 and 3 did not exist. The macro
|
||||||
|
// spelling, the 83 call sites and the verb enum are unchanged: the dispatch is
|
||||||
|
// kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor*
|
||||||
|
// entry points with one call site per verb instead of nine.
|
||||||
|
void MGPipeValidateForVerb(MGPipeVerb verb);
|
||||||
|
|
||||||
// Ends the verb in flight without starting another: bumps the serial, so every field the
|
// Ends the verb in flight without starting another: bumps the serial, so every field the
|
||||||
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
||||||
@@ -33,6 +43,63 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
||||||
void MGPipeLeaveVerb();
|
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 TABLE IS IN PipeFill.cpp, ONCE: bit 9 requires bit 10, bit 10 requires bits 7 and
|
||||||
|
// 11, bit 11 requires bit 10, bit 12 depends on nothing - the client mirror, bit for bit, of
|
||||||
|
// the four `Resolve<Family>SubsystemArm()` refusals in DirectGLES/Managers.cpp.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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
|
||||||
|
// (TrackerWalk.ABaseInstanceSurvivesTheFirstWalkOnAFreshContext).
|
||||||
|
Uint32 MGPipePendingBaseInstance();
|
||||||
|
|
||||||
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
|
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
|
||||||
// the value - of `field` at `verb`, so that verb's read of it is
|
// the value - of `field` at `verb`, so that verb's read of it is
|
||||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
|
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
|
||||||
@@ -41,6 +108,28 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// Fatal{PipeVerifyBadKnob}.
|
// Fatal{PipeVerifyBadKnob}.
|
||||||
void MGPipeSetPoisonOmission(const char* verb, const char* field);
|
void MGPipeSetPoisonOmission(const char* verb, const char* field);
|
||||||
|
|
||||||
|
// PipeFill.cpp. How many times set_vertex_attrib_defaults' applier failed to reproduce
|
||||||
|
// the value the call carried, so the client wrote the mirror itself
|
||||||
|
// (EmitVertexAttribDefaults). It is the ONE observable of that repair: the window it
|
||||||
|
// covers is a verb whose class does not read m_currentVertexAttribute, where reading the
|
||||||
|
// storage to check it would be the poison violation the fill table exists to forbid. So
|
||||||
|
// TrackerShippedEmitter asserts on this counter instead, and the day package A's applier
|
||||||
|
// switches on MGPAttribValue::ValueClass the counter stops moving.
|
||||||
|
//
|
||||||
|
// Not hot-path instrumentation: it is incremented only inside the repair branch, which
|
||||||
|
// runs only when the call actually went out, which is only when an attribute default
|
||||||
|
// moved.
|
||||||
|
Uint64 MGPipeVertexAttribDefaultRepairCount();
|
||||||
|
|
||||||
|
// PipeFill.cpp. The header of the last set_vertex_attrib_defaults that actually went out
|
||||||
|
// - Mask, and Count == 0 for "none ever did", since a call naming no attribute is not
|
||||||
|
// emitted. Two properties of this call have no other observable, because reading
|
||||||
|
// m_currentVertexAttribute back at a verb whose class does not carry it is the poison
|
||||||
|
// violation the fill table exists to forbid: that a FRESH CONTEXT republishes all 32
|
||||||
|
// (the server's mirror still holds the previous context's defaults), and that one moved
|
||||||
|
// attribute publishes exactly one. Eight bytes, written only when a call goes out.
|
||||||
|
MGPVertexAttribDefaults MGPipeVertexAttribDefaultsLastHeader();
|
||||||
|
|
||||||
#if MOBILEGL_PIPE_VERIFY
|
#if MOBILEGL_PIPE_VERIFY
|
||||||
// PipeFill.cpp. The second arm of the comparator (P1 brief D8, ARCHITECTURE.md 13.2-2):
|
// PipeFill.cpp. The second arm of the comparator (P1 brief D8, ARCHITECTURE.md 13.2-2):
|
||||||
// fills `snapshot` from the live GLContext the old way, for every field in `mask`. This
|
// fills `snapshot` from the live GLContext the old way, for every field in `mask`. This
|
||||||
@@ -49,7 +138,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
|
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
|
||||||
#endif
|
#endif
|
||||||
} // namespace MobileGL::MG_Pipe
|
} // namespace MobileGL::MG_Pipe
|
||||||
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
|
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeValidateForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
|
||||||
|
// P3a D-H2.1. One line immediately ABOVE the MGP_FILL of a draw entry point that takes a
|
||||||
|
// baseinstance, carrying the argument RAW. It has to be a macro for MGP_FILL's reason: the
|
||||||
|
// three call sites are compiled in the pull build too, where MGPipeSetPendingBaseInstance is
|
||||||
|
// neither declared nor defined.
|
||||||
|
#define MGP_SET_BASE_INSTANCE(BaseInstance) \
|
||||||
|
::MobileGL::MG_Pipe::MGPipeSetPendingBaseInstance(static_cast<::MobileGL::Uint32>(BaseInstance))
|
||||||
#else
|
#else
|
||||||
#define MGP_FILL(Verb) ((void)0)
|
#define MGP_FILL(Verb) ((void)0)
|
||||||
|
#define MGP_SET_BASE_INSTANCE(BaseInstance) ((void)0)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/ProgramEmit.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/CompositeResolver.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// WIRED. create/bind/delete_shader_state, set_draw_program, set_dispatch_program and
|
||||||
|
// set_global_constants all have bodies, so this family contributes its bit to
|
||||||
|
// kMGPipeWiredSubsystems.
|
||||||
|
//
|
||||||
|
// AND SINCE c0b THAT CONSTANT REALLY IS PART OF THE EMISSION GATE, so the note that used to
|
||||||
|
// say otherwise here was true only against the contract commit: the validate point's
|
||||||
|
// `wants()` asks the subsystem mapping, the operator's MOBILEGL_PIPE_PUSH mask, THIS
|
||||||
|
// CONSTANT and the dirty bit, and the birth hooks' `FamilyIsLive` asks the same pair one
|
||||||
|
// level in. It is also a compile-time contract - while it is non-zero PipeFill.cpp's
|
||||||
|
// `if constexpr` seam instantiates the forward to EmitShaderCso below, so a missing entry
|
||||||
|
// point is a build error here rather than at the merge. The RUNTIME A/B that switches the
|
||||||
|
// family off is still the mask. See SamplerEmit.h's twin note.
|
||||||
|
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = kMGPipeSubsystemPrograms;
|
||||||
|
|
||||||
|
// D-H6. ~0u is the BACKENDS' "never uploaded" sentinel for a global-constants version, and
|
||||||
|
// ProgramObject::MarkUBOContentDirty skips it on the wrap for exactly that reason. The
|
||||||
|
// client must never put it on the wire either: a server that received it would read its own
|
||||||
|
// record as "nothing has ever been uploaded here" and re-upload for ever.
|
||||||
|
inline constexpr Uint32 kMGPipeGlobalConstantsNeverUploaded = ~Uint32{0};
|
||||||
|
|
||||||
|
inline constexpr Bool MGPipeGlobalConstantsVersionIsEmittable(Uint32 version) {
|
||||||
|
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.
|
||||||
|
inline Uint32 MGPipeStageMaskOf(const MG_State::GLState::ProgramObject& program) {
|
||||||
|
Uint32 mask = 0;
|
||||||
|
for (const ShaderStage stage : program.GetLinkedShaderStages()) {
|
||||||
|
if (stage == ShaderStage::Unknown) continue;
|
||||||
|
mask |= Uint32{1} << static_cast<Uint32>(stage);
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MGPipeProgramEmitter {
|
||||||
|
public:
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||||
|
|
||||||
|
// create_shader_state (re-issued on the SAME handle whenever the link version moves -
|
||||||
|
// Gen moves only on slot reuse), then bind_shader_state and set_draw_program /
|
||||||
|
// set_dispatch_program. Two program calls because the frontend has two joins and two
|
||||||
|
// PipeInputs slots.
|
||||||
|
//
|
||||||
|
// BOTH JOINS HAPPEN HERE and both are the verb's own: GetProgramForDraw flattens a
|
||||||
|
// bound pipeline into its composite and GetProgramForDispatch answers the compute
|
||||||
|
// question, and with a plain glUseProgram they are the same object, so the ordinary
|
||||||
|
// frame pays one join it was going to pay anyway.
|
||||||
|
Uint64 EmitShaderState(GLContext& ctx) {
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
const auto& drawProgram = ctx.GetProgramForDraw();
|
||||||
|
const auto& dispatchProgram = ctx.GetProgramForDispatch();
|
||||||
|
|
||||||
|
const MGPipeHandle drawCso =
|
||||||
|
drawProgram ? AcquireShaderCso(*drawProgram, bytes) : kMGPipeNullHandle;
|
||||||
|
// THE COMPOSITE'S SECOND RELEASE PATH is spoken here, not in a destructor: when the
|
||||||
|
// bound pipeline's draw-program signature moves, the resolver releases the slot the
|
||||||
|
// previous composite held. Whichever of the two paths runs second - this one or the
|
||||||
|
// composite ProgramObject's own ~ProgramObject - is a proven no-op, because the slot
|
||||||
|
// allocator refuses a slot that is not live at that generation.
|
||||||
|
if (drawProgram && MGPipeProgramIsPipelineComposite(*drawProgram)) {
|
||||||
|
if (const auto& pipeline = ctx.GetBoundProgramPipeline()) {
|
||||||
|
// THE CONTEXT IS PART OF THE RESOLVER's KEY and this is the only place that
|
||||||
|
// supplies it: the resolver is a process singleton and a pipeline's GL name
|
||||||
|
// is per context, so without it a make-current between two contexts holding
|
||||||
|
// one pipeline name released the other context's LIVE composite.
|
||||||
|
// GetTextureContextId() is the tree's never-reused per-context id, the same
|
||||||
|
// one PipeInputs carries and the backends' per-context memos key on.
|
||||||
|
MGPipeCompositeResolverInstance().Observe(ctx.GetTextureContextId(), *pipeline,
|
||||||
|
*drawProgram, drawCso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const MGPipeHandle dispatchCso =
|
||||||
|
dispatchProgram ? (dispatchProgram == drawProgram ? drawCso
|
||||||
|
: AcquireShaderCso(*dispatchProgram, bytes))
|
||||||
|
: kMGPipeNullHandle;
|
||||||
|
|
||||||
|
// THE BOUND CSO IS THE DRAW ONE WHEN THERE IS ONE. bind_shader_state names what
|
||||||
|
// glUseProgram selected, and when a program pipeline is bound instead that is the
|
||||||
|
// composite; a compute-only pipeline has no draw program at all, and then the
|
||||||
|
// dispatch program is the only thing bound. A null handle is legal here and means
|
||||||
|
// exactly "nothing bound".
|
||||||
|
const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso;
|
||||||
|
if (boundCso != m_boundCso) {
|
||||||
|
MGPipeApplyBindShaderState(HandleOnly(boundCso));
|
||||||
|
m_boundCso = boundCso;
|
||||||
|
++m_binds;
|
||||||
|
bytes += sizeof(MGPHandleOnly);
|
||||||
|
}
|
||||||
|
if (drawCso != m_drawCso) {
|
||||||
|
MGPipeApplySetDrawProgram(HandleOnly(drawCso));
|
||||||
|
m_drawCso = drawCso;
|
||||||
|
++m_drawSets;
|
||||||
|
bytes += sizeof(MGPHandleOnly);
|
||||||
|
}
|
||||||
|
if (dispatchCso != m_dispatchCso) {
|
||||||
|
MGPipeApplySetDispatchProgram(HandleOnly(dispatchCso));
|
||||||
|
m_dispatchCso = dispatchCso;
|
||||||
|
++m_dispatchSets;
|
||||||
|
bytes += sizeof(MGPHandleOnly);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_global_constants: the DEFAULT UNIFORM BLOCK only, keyed (ShaderCso, Version) and
|
||||||
|
// at most once per program per frame. Version is GetUBOContentVersion() and must never
|
||||||
|
// be ~0u, which is the backends' "never uploaded" sentinel - the wrap skips it.
|
||||||
|
//
|
||||||
|
// NAMED uniform blocks are NOT this call's: set_shader_buffers(Uniform) is a later
|
||||||
|
// phase's and BindCurrentProgramWithResources' named-UBO block is untouched. What
|
||||||
|
// travels here is globalUboScratch, the link phase's CPU array, which has no GL name
|
||||||
|
// and no BufferObject behind it.
|
||||||
|
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||||
|
const auto& program = ctx.GetProgramForDraw();
|
||||||
|
if (!program) return 0;
|
||||||
|
const Uint32 version = program->GetUBOContentVersion();
|
||||||
|
// THE SENTINEL IS NEVER EMITTED. A server that received ~0u would read its own
|
||||||
|
// record as "never uploaded" and re-upload every frame for ever.
|
||||||
|
if (!MGPipeGlobalConstantsVersionIsEmittable(version)) return 0;
|
||||||
|
const Uint size = program->GetUBOSize();
|
||||||
|
if (size == 0) return 0;
|
||||||
|
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
const MGPipeHandle cso = AcquireShaderCso(*program, bytes);
|
||||||
|
// (ShaderCso, Version) IS the key, so the latch is the key: an unchanged pair means
|
||||||
|
// the server already holds these bytes and re-sending them would move the record's
|
||||||
|
// serial for nothing.
|
||||||
|
if (cso == m_constantsCso && version == m_constantsVersion) return bytes;
|
||||||
|
|
||||||
|
m_lastConstants = MGPGlobalConstants{};
|
||||||
|
m_lastConstants.ShaderCso = cso;
|
||||||
|
m_lastConstants.Version = version;
|
||||||
|
// THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which
|
||||||
|
// is what a monolith emission is - and the bytes ride beside it as a companion
|
||||||
|
// pointer. Offset carries the staging address for diagnostics only; nothing reads
|
||||||
|
// it as a length.
|
||||||
|
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||||
|
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||||
|
m_lastConstants.Blob.Size = 0;
|
||||||
|
MGPipeApplySetGlobalConstants(m_lastConstants, program->GetUBOData());
|
||||||
|
m_constantsCso = cso;
|
||||||
|
m_constantsVersion = version;
|
||||||
|
++m_constantSets;
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
|
||||||
|
}
|
||||||
|
return bytes + sizeof(MGPGlobalConstants) + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D-H4's re-issue rule, and it is the CreateVertexElements shape one for one: the
|
||||||
|
// record goes out again on the SAME handle whenever the link version moves, which is
|
||||||
|
// legal because MGPipeHandle::Gen increments only on slot reuse and never on a
|
||||||
|
// respecify. A program that relinks is the same GL object and the server's twin table
|
||||||
|
// must not be asked to mint a second one.
|
||||||
|
MGPipeHandle AcquireShaderCso(const ProgramObject& program, Uint64& payloadBytes) {
|
||||||
|
const MGPipeHandle handle = AcquireShaderCsoHandle(program);
|
||||||
|
if (MGPipeHandleIsNull(handle)) return handle;
|
||||||
|
Latch& latch = LatchFor(handle);
|
||||||
|
|
||||||
|
const Uint32 linkVersion = program.GetLinkVersion();
|
||||||
|
if (latch.RecordLive && latch.RecordGen == handle.Gen && latch.LinkVersion == linkVersion) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& link = program.GetLinkReflection();
|
||||||
|
const auto& spirv = program.GetSpirvReflection();
|
||||||
|
|
||||||
|
m_lastDesc = MGPProgramDesc{};
|
||||||
|
m_lastDesc.Cso = handle;
|
||||||
|
m_lastDesc.StageMask = MGPipeStageMaskOf(program);
|
||||||
|
m_lastDesc.GlobalUboSize = static_cast<Uint32>(program.GetUBOSize());
|
||||||
|
m_lastDesc.ReservedNumSamplesOffset = static_cast<Uint32>(spirv.reservedNumSamplesOffset);
|
||||||
|
m_lastDesc.SpirvStatus = spirv.spirvStatus ? 1 : 0;
|
||||||
|
m_lastDesc.NativeFloat64 = spirv.nativeFloat64 ? 1 : 0;
|
||||||
|
m_lastDesc.PointSizeDemoted = spirv.pointSizeDemoted ? 1 : 0;
|
||||||
|
m_lastDesc.EnableSpirvValidation = spirv.enableSpirvValidation ? 1 : 0;
|
||||||
|
|
||||||
|
// ONE BLOB REF PER MODULE, IN THE LINKED-SHADER-SNAPSHOT'S ORDER, which is the
|
||||||
|
// order GetGeneratedSpirv() is indexed in - so Spirv[i] and StageMask agree because
|
||||||
|
// they came out of the same snapshot. Every one of them declares Size 0 (the one
|
||||||
|
// Blob rule); Offset carries the module's staging address so a reader can see which
|
||||||
|
// slots are occupied without the record pretending to declare a length it does not
|
||||||
|
// own.
|
||||||
|
//
|
||||||
|
// A COUNTED REFUSAL AND NOT AN ASSERTION (D-J3). MOBILEGL_ASSERT compiles out at
|
||||||
|
// INFO, which is all three gate builds and every shipped build, so an assert here
|
||||||
|
// would leave the truncation below completely silent in exactly the builds that
|
||||||
|
// run - which is the idiom D-J3 exists to forbid. generatedSpirv cannot exceed six
|
||||||
|
// stages today, so this is a guard against a seventh; truncation is the safe
|
||||||
|
// direction and the counter is what makes it visible.
|
||||||
|
const SizeT moduleCount = spirv.generatedSpirv.size();
|
||||||
|
if (moduleCount > 6) ++m_moduleTruncations;
|
||||||
|
for (SizeT i = 0; i < moduleCount && i < 6; ++i) {
|
||||||
|
m_lastDesc.Spirv[i].Seg = kMGHostSpanSegNone;
|
||||||
|
m_lastDesc.Spirv[i].Offset = reinterpret_cast<Uint64>(spirv.generatedSpirv[i].data());
|
||||||
|
m_lastDesc.Spirv[i].Size = 0;
|
||||||
|
}
|
||||||
|
m_lastDesc.Reflection.Seg = kMGHostSpanSegNone;
|
||||||
|
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
|
||||||
|
m_lastDesc.Reflection.Size = 0;
|
||||||
|
|
||||||
|
MGPipeApplyCreateShaderState(m_lastDesc, &link, &spirv);
|
||||||
|
// THE CREATE WENT OUT, so the publication latch is taken here and nowhere else
|
||||||
|
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
|
||||||
|
// delete_shader_state can never go out - for an ordinary program or for a
|
||||||
|
// composite, both of which take that one helper.
|
||||||
|
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||||
|
++m_creates;
|
||||||
|
payloadBytes += sizeof(MGPProgramDesc);
|
||||||
|
|
||||||
|
// A RE-ISSUED create_shader_state CLEARS THE APPLIER's DEFAULT UNIFORM BLOCK (wire
|
||||||
|
// W6), so the (Cso, Version) latch that suppresses set_global_constants has to go
|
||||||
|
// with it or the block is never re-sent. The case the design worries about is a
|
||||||
|
// FAILED relink of a bound program - GL keeps the previous executable and its
|
||||||
|
// uniforms running - and the general one is any future re-issue trigger that does
|
||||||
|
// not happen to move the content version, of which a recycled slot is one.
|
||||||
|
// Invalidated rather than re-emitted here, because this function has no business
|
||||||
|
// deciding when the constants go out: the next EmitGlobalConstants sees an
|
||||||
|
// unlatched key and sends them.
|
||||||
|
if (m_constantsCso == handle) {
|
||||||
|
m_constantsCso = kMGPipeNullHandle;
|
||||||
|
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.RecordLive = true;
|
||||||
|
latch.RecordGen = handle.Gen;
|
||||||
|
latch.LinkVersion = linkVersion;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- THE CONTRACT ENTRY POINT THIS FAMILY OWES (contract-v2 §3.4) ----
|
||||||
|
//
|
||||||
|
// PipeFill.cpp's MGPipeEmitShaderCsoCreate forwards here through the `if constexpr`
|
||||||
|
// seam keyed on kMGPipeWiredProgramSubsystem, so while that constant is non-zero this
|
||||||
|
// must exist and be spelled exactly like this. A thin wrapper on purpose:
|
||||||
|
// AcquireShaderCso above IS this family's handle rule - identity-addressed per
|
||||||
|
// ProgramObject, the composite band entered through the one door, the re-issue on the
|
||||||
|
// same handle and the publication - and a second copy of any of it here would be a
|
||||||
|
// second authority.
|
||||||
|
//
|
||||||
|
// THE HOOK HAS ALREADY APPLIED BOTH GATES (the operator's mask and the wired constant),
|
||||||
|
// so this body applies none of its own. The byte count is discarded: a birth is not a
|
||||||
|
// validate-point emission and has no payload budget to report into.
|
||||||
|
void EmitShaderCso(ProgramObject& program) {
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
AcquireShaderCso(program, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The emitter's OWN record memo - "have I already published a create_shader_state at
|
||||||
|
// this slot, for this generation, at this link version".
|
||||||
|
//
|
||||||
|
// IT IS NOT WHAT THE DEATH PATH ASKS, and that changed at c0b (contract-v2 §3.1/D17):
|
||||||
|
// MGPipeEmitShaderCsoDestroyAndFree reads A's publication latch, which is one answer
|
||||||
|
// per {kind, slot, gen} that all six death helpers share. This stays because the
|
||||||
|
// VERSION-FIRST SKIP needs it - it is the same latch AcquireShaderCso consults before
|
||||||
|
// it builds a descriptor - and because a unit case reads it.
|
||||||
|
//
|
||||||
|
// THE COMPOSITE BAND IS INDEXED SEPARATELY, for the allocator's own reason: the band
|
||||||
|
// base is 983040, so a slot-indexed vector would allocate ~983k latches for one program
|
||||||
|
// pipeline. Both spaces stay dense against their own high-water mark.
|
||||||
|
Bool RecordIsPublished(MGPipeHandle handle) const {
|
||||||
|
if (MGPipeHandleIsNull(handle)) return false;
|
||||||
|
const Vector<Latch>& table = TableOf(handle);
|
||||||
|
const SizeT slot = SlotIndexOf(handle);
|
||||||
|
if (slot >= table.size()) return false;
|
||||||
|
const Latch& latch = table[slot];
|
||||||
|
return latch.RecordLive && latch.RecordGen == handle.Gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The memo's other half, and the bound-mirror clearing beside it.
|
||||||
|
//
|
||||||
|
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the death path
|
||||||
|
// reads the contract's latch for the wire delete and then forwards here, before the
|
||||||
|
// slot is freed, so a dead handle no longer reads as published in this memo between
|
||||||
|
// the death and the recycle and the three bound mirrors never name a dead program.
|
||||||
|
// Gen-keyed, so a late notice for a slot already handed out again clears nothing of
|
||||||
|
// the successor's.
|
||||||
|
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||||
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
|
Vector<Latch>& table = TableOf(handle);
|
||||||
|
const SizeT slot = SlotIndexOf(handle);
|
||||||
|
if (slot < table.size() && table[slot].RecordGen == handle.Gen) {
|
||||||
|
table[slot] = Latch{};
|
||||||
|
}
|
||||||
|
if (m_boundCso == handle) m_boundCso = kMGPipeNullHandle;
|
||||||
|
if (m_drawCso == handle) m_drawCso = kMGPipeNullHandle;
|
||||||
|
if (m_dispatchCso == handle) m_dispatchCso = kMGPipeNullHandle;
|
||||||
|
if (m_constantsCso == handle) {
|
||||||
|
m_constantsCso = kMGPipeNullHandle;
|
||||||
|
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The validate point's FreshlyPrimed arm. MGPipeApplierReset clears DrawProgram,
|
||||||
|
// DispatchProgram and BoundShaderCso - all three are per-context WORKING STATE - so
|
||||||
|
// the three mirrors here go with them, or the first emission after a make-current
|
||||||
|
// would be suppressed as unchanged and the server would draw with the previous
|
||||||
|
// context's program bound.
|
||||||
|
//
|
||||||
|
// The RECORD half stays, and that is the rule rather than an oversight: the applier
|
||||||
|
// keeps its shader-CSO records across a make-current because a program lives in a share
|
||||||
|
// group, and re-publishing one would move its Serial for nothing. The global-constants
|
||||||
|
// key goes with the working state because its record's bytes are per (Cso, Version) and
|
||||||
|
// a fresh server has not been told them.
|
||||||
|
void Reset() {
|
||||||
|
m_boundCso = kMGPipeNullHandle;
|
||||||
|
m_drawCso = kMGPipeNullHandle;
|
||||||
|
m_dispatchCso = kMGPipeNullHandle;
|
||||||
|
m_constantsCso = kMGPipeNullHandle;
|
||||||
|
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||||
|
// The composite memo's freshness goes with them - and only its freshness. Its
|
||||||
|
// ENTRIES name composites whose frontend objects outlive the context switch, so
|
||||||
|
// releasing them here would emit a delete for a live program.
|
||||||
|
MGPipeCompositeResolverInstance().Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() {
|
||||||
|
m_creates = m_binds = m_drawSets = m_dispatchSets = m_constantSets = 0;
|
||||||
|
m_moduleTruncations = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- what a unit case reads ----
|
||||||
|
const MGPProgramDesc& LastProgramDesc() const { return m_lastDesc; }
|
||||||
|
const MGPGlobalConstants& LastGlobalConstants() const { return m_lastConstants; }
|
||||||
|
// THE (Cso, Version) KEY set_global_constants is suppressed against. Exposed so a case
|
||||||
|
// can pin that a re-issued create_shader_state invalidates it - the applier clears the
|
||||||
|
// block on the re-issue (wire W6), so a latch that survived it would never re-send.
|
||||||
|
MGPipeHandle GlobalConstantsCso() const { return m_constantsCso; }
|
||||||
|
Uint32 GlobalConstantsVersion() const { return m_constantsVersion; }
|
||||||
|
// D-J3's counted refusal: programs whose linked snapshot carried more modules than
|
||||||
|
// MGPProgramDesc::Spirv[] can name, and whose tail was therefore dropped.
|
||||||
|
Uint64 TruncatedModuleCount() const { return m_moduleTruncations; }
|
||||||
|
MGPipeHandle BoundCso() const { return m_boundCso; }
|
||||||
|
MGPipeHandle DrawCso() const { return m_drawCso; }
|
||||||
|
MGPipeHandle DispatchCso() const { return m_dispatchCso; }
|
||||||
|
Uint64 CreateCount() const { return m_creates; }
|
||||||
|
Uint64 BindCount() const { return m_binds; }
|
||||||
|
Uint64 DrawProgramSetCount() const { return m_drawSets; }
|
||||||
|
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
|
||||||
|
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// THE ONE PLACE THE BAND CAN ENTER. An ordinary program's slot comes from the ordinary
|
||||||
|
// allocator door keyed on its lifetime id. CompositeResolver.h widens this to send a
|
||||||
|
// pipeline composite through MGPipeSlotAllocator::AllocateComposite instead, and
|
||||||
|
// nothing else about the emission changes - the server never learns a composite is a
|
||||||
|
// composite.
|
||||||
|
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
|
||||||
|
const Uint64 lifetimeId = program.GetLifetimeId();
|
||||||
|
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||||
|
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||||
|
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
|
||||||
|
// an ordinary ShaderCso handle in every other respect - the same kind, the same
|
||||||
|
// {slot, gen} rules, the same Free, the same death helper. Keying it on its own
|
||||||
|
// lifetime id rather than on the pipeline's signature is what makes ~ProgramObject
|
||||||
|
// able to release it at all, and it is why two pipelines that happen to have the
|
||||||
|
// same signature keep their own composite: sharing one handle between two frontend
|
||||||
|
// objects would let the first one's death free a slot the second still names.
|
||||||
|
return MGPipeProgramIsPipelineComposite(program)
|
||||||
|
? MGPipeSlots().AllocateComposite(lifetimeId)
|
||||||
|
: MGPipeSlots().AllocateFor(MGPipeKind::ShaderCso, lifetimeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Latch {
|
||||||
|
Bool RecordLive = false;
|
||||||
|
Uint32 RecordGen = 0;
|
||||||
|
Uint32 LinkVersion = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
|
||||||
|
// bites a second time: the composite band starts at slot 983040, so folding a composite
|
||||||
|
// into the ordinary slot-indexed vector would allocate ~983k latches - and grow them
|
||||||
|
// again on every future push_back - for a single program pipeline. Both spaces stay
|
||||||
|
// dense against their own high-water mark, which is exactly what the allocator does one
|
||||||
|
// level down.
|
||||||
|
Vector<Latch>& TableOf(MGPipeHandle handle) {
|
||||||
|
return MGPipeIsCompositeShaderSlot(handle.Slot) ? m_compositeLatch : m_latch;
|
||||||
|
}
|
||||||
|
const Vector<Latch>& TableOf(MGPipeHandle handle) const {
|
||||||
|
return MGPipeIsCompositeShaderSlot(handle.Slot) ? m_compositeLatch : m_latch;
|
||||||
|
}
|
||||||
|
static SizeT SlotIndexOf(MGPipeHandle handle) {
|
||||||
|
return MGPipeIsCompositeShaderSlot(handle.Slot)
|
||||||
|
? static_cast<SizeT>(handle.Slot - kMGPipeShaderCsoCompositeSlotBase)
|
||||||
|
: static_cast<SizeT>(handle.Slot);
|
||||||
|
}
|
||||||
|
Latch& LatchFor(MGPipeHandle handle) {
|
||||||
|
Vector<Latch>& table = TableOf(handle);
|
||||||
|
const SizeT slot = SlotIndexOf(handle);
|
||||||
|
if (slot >= table.size()) table.resize(slot + 1);
|
||||||
|
return table[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
|
||||||
|
MGPHandleOnly only{};
|
||||||
|
only.Handle = handle;
|
||||||
|
only.Kind = static_cast<Uint32>(MGPipeKind::ShaderCso);
|
||||||
|
return only;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPProgramDesc m_lastDesc{};
|
||||||
|
MGPGlobalConstants m_lastConstants{};
|
||||||
|
|
||||||
|
Vector<Latch> m_latch;
|
||||||
|
Vector<Latch> m_compositeLatch;
|
||||||
|
MGPipeHandle m_boundCso = kMGPipeNullHandle;
|
||||||
|
MGPipeHandle m_drawCso = kMGPipeNullHandle;
|
||||||
|
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
|
||||||
|
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
|
||||||
|
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||||
|
|
||||||
|
Uint64 m_creates = 0;
|
||||||
|
Uint64 m_binds = 0;
|
||||||
|
Uint64 m_drawSets = 0;
|
||||||
|
Uint64 m_dispatchSets = 0;
|
||||||
|
Uint64 m_constantSets = 0;
|
||||||
|
Uint64 m_moduleTruncations = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline MGPipeProgramEmitter& MGPipeProgramEmitterInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||||
|
// intentionally leaked at exit, and it MUST NOT hold a frontend SharedPtr - that is
|
||||||
|
// the exit-order rule, stated over every MGPipe process singleton rather than over the
|
||||||
|
// ones a destructor reaches today.
|
||||||
|
static MGPipeProgramEmitter* emitter = new MGPipeProgramEmitter();
|
||||||
|
return *emitter;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/ResourceTracker.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-A3: BindMask
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPResourceDesc::BindMask's twelve bits MOVED TO MG_Pipe/MGPipeTypes.h AT P4a, beside
|
||||||
|
// the field, exactly as the note that stood here said they would when a second producer
|
||||||
|
// appeared: P4a's texture family sets kMGPipeBindSampler / kMGPipeBindShaderImage /
|
||||||
|
// kMGPipeBindRenderTarget / kMGPipeBindDepthStencil, the four bits nothing set before.
|
||||||
|
// No alias is written for them because none is possible or needed - both files are
|
||||||
|
// namespace MobileGL::MG_Pipe and this one includes that header, so every spelling below
|
||||||
|
// and in package B's code is unchanged.
|
||||||
|
//
|
||||||
|
// What stays here is the BUFFER half of the mapping, which is this file's own: the
|
||||||
|
// BufferTarget table, its sentinel and its completeness assert.
|
||||||
|
|
||||||
|
// A sentinel the table below returns for an enumerator it does not name. It is NOT a
|
||||||
|
// legal mask value: every enumerator must be listed, including the ones that map to no
|
||||||
|
// bit at all, so that ADDING a BufferTarget is a build break here rather than a bit
|
||||||
|
// that silently stops being published.
|
||||||
|
inline constexpr Uint32 kMGPipeBindUnmapped = 0x10000u;
|
||||||
|
|
||||||
|
// The one table. No `default:` arm on purpose - that is what makes the static_assert
|
||||||
|
// below able to see an unnamed enumerator.
|
||||||
|
constexpr Uint32 MGPipeBindMaskForBufferTarget(BufferTarget target) {
|
||||||
|
switch (target) {
|
||||||
|
case BufferTarget::Vertex:
|
||||||
|
return kMGPipeBindVertex;
|
||||||
|
// GL_ELEMENT_ARRAY_BUFFER is the VAO's element slot: the same bind is both "this
|
||||||
|
// resource is an index buffer" and "the server may need its bytes on its own side".
|
||||||
|
case BufferTarget::Index:
|
||||||
|
return kMGPipeBindIndex | kMGPipeBindElementArray;
|
||||||
|
case BufferTarget::Uniform:
|
||||||
|
return kMGPipeBindConstant;
|
||||||
|
case BufferTarget::ShaderStorage:
|
||||||
|
return kMGPipeBindShaderBuffer;
|
||||||
|
case BufferTarget::DispatchIndirect:
|
||||||
|
case BufferTarget::DrawIndirect:
|
||||||
|
case BufferTarget::Parameter:
|
||||||
|
return kMGPipeBindIndirect;
|
||||||
|
// A texture buffer's backing store is SAMPLED through the texture that names it.
|
||||||
|
case BufferTarget::Texture:
|
||||||
|
return kMGPipeBindSampler;
|
||||||
|
case BufferTarget::TransformFeedback:
|
||||||
|
return kMGPipeBindStreamOutput;
|
||||||
|
case BufferTarget::AtomicCounter:
|
||||||
|
return kMGPipeBindAtomic;
|
||||||
|
// TRANSFER AND QUERY TARGETS, which the bind mask deliberately does not name: none
|
||||||
|
// of them is a pipeline binding, none of them makes the server keep anything, and
|
||||||
|
// a bit set for them would only widen what a split server mirrors. Listed rather
|
||||||
|
// than defaulted, so the completeness assert still sees them.
|
||||||
|
case BufferTarget::CopyRead:
|
||||||
|
case BufferTarget::CopyWrite:
|
||||||
|
case BufferTarget::PixelPack:
|
||||||
|
case BufferTarget::PixelUnpack:
|
||||||
|
case BufferTarget::Query:
|
||||||
|
return kMGPipeBindNone;
|
||||||
|
case BufferTarget::BufferTargetCount:
|
||||||
|
case BufferTarget::Unknown:
|
||||||
|
return kMGPipeBindNone;
|
||||||
|
}
|
||||||
|
return kMGPipeBindUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Bool MGPipeEveryBufferTargetIsMapped() {
|
||||||
|
for (SizeT i = 0; i < static_cast<SizeT>(BufferTarget::BufferTargetCount); ++i) {
|
||||||
|
if (MGPipeBindMaskForBufferTarget(static_cast<BufferTarget>(i)) == kMGPipeBindUnmapped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(MGPipeEveryBufferTargetIsMapped(),
|
||||||
|
"a BufferTarget enumerator has no MGPResourceDesc::BindMask row: add it to "
|
||||||
|
"MGPipeBindMaskForBufferTarget, including a deliberate kMGPipeBindNone, or the "
|
||||||
|
"resource it is bound to stops publishing that binding (D-A3, P8 expectation 1)");
|
||||||
|
static_assert(MGPipeBindMaskForBufferTarget(BufferTarget::Index) & kMGPipeBindElementArray,
|
||||||
|
"the ELEMENT_ARRAY bit is the index host mirror's switch (ARCHITECTURE.md 10.3)");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The discriminators MGPResourceDesc / MGPSubData carry for a BUFFER
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// P4a MINTED THE FIRST LIST: MGPipeTypes.h now carries enum MGPipeResourceTarget beside
|
||||||
|
// the field, and kMGPipeResourceTargetBuffer moved there with it - the narrowed
|
||||||
|
// resource_respecify ack predicate lives in that header and has to name the buffer target
|
||||||
|
// explicitly, and it may not reach into MG_Impl to do so. The second discriminator is the
|
||||||
|
// frontend enum, named rather than open-coded, and stays here because only this file
|
||||||
|
// produces it.
|
||||||
|
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
||||||
|
static_cast<Uint8>(MobileGL::TextureStorageType::Buffer);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-A2: the payload builders. Pure, so a unit case can assert field by field.
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The descriptor for `buffer`. `storageDefined` is false for the create that the
|
||||||
|
// constructor emits - storage is defined lazily by the first respecify and a backend
|
||||||
|
// tolerates a resource that has none - and true for every respecify.
|
||||||
|
inline MGPResourceDesc MGPipeBuildResourceDesc(const MG_State::GLState::BufferObject& buffer,
|
||||||
|
MGPipeHandle handle, Uint16 bindMask,
|
||||||
|
Bool storageDefined) {
|
||||||
|
MGPResourceDesc desc{};
|
||||||
|
desc.Resource = handle;
|
||||||
|
desc.Target = static_cast<Uint8>(kMGPipeResourceTargetBuffer);
|
||||||
|
desc.StorageKind = kMGPipeResourceStorageKindBuffer;
|
||||||
|
desc.BindMask = bindMask;
|
||||||
|
if (storageDefined) {
|
||||||
|
// MGPResourceDesc::Width is a Uint32 and that is the CONTRACT's shape, not this
|
||||||
|
// package's, so a store of 4 GiB or more cannot be declared at all. Truncating it
|
||||||
|
// silently is the one answer that must not happen: the applier's range gate would
|
||||||
|
// then refuse the first legal write past the truncated extent as
|
||||||
|
// Fatal{ProtocolCorruption} and name a corruption that is really a narrowing here.
|
||||||
|
// So it is said out loud, once, in every build - the assertion compiles out at
|
||||||
|
// INFO, which is what all three gate builds are.
|
||||||
|
if (buffer.GetSize() > static_cast<SizeT>(0xFFFFFFFFull)) {
|
||||||
|
MGLOG_E_ONCE("MGPipe: buffer %u declares a store of %llu bytes, which does not fit "
|
||||||
|
"MGPResourceDesc::Width - the descriptor's extent is narrowed and every "
|
||||||
|
"write past 4 GiB will be refused by the applier's range gate",
|
||||||
|
buffer.GetExternalIndex(),
|
||||||
|
static_cast<unsigned long long>(buffer.GetSize()));
|
||||||
|
MOBILEGL_ASSERT(false, "MGPResourceDesc::Width cannot carry this buffer's size");
|
||||||
|
}
|
||||||
|
desc.Width = static_cast<Uint32>(buffer.GetSize());
|
||||||
|
desc.Usage = static_cast<Uint32>(buffer.GetUsage());
|
||||||
|
desc.StorageFlags = static_cast<Uint32>(buffer.GetStorageFlags());
|
||||||
|
desc.Immutable = buffer.IsImmutableStorage() ? 1 : 0;
|
||||||
|
desc.HasDefinedContent = buffer.HasDefinedContent() ? 1 : 0;
|
||||||
|
}
|
||||||
|
// Diagnostics only: a GL name is never an identity, never a memo key and never part
|
||||||
|
// of a content hash (ARCHITECTURE.md 4.2.1).
|
||||||
|
desc.GlNameForDiag = static_cast<Uint32>(buffer.GetExternalIndex());
|
||||||
|
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.
|
||||||
|
inline Bool MGPipeBuildSubDataRecord(MGPipeHandle res, Uint64 offset, Uint64 size, MGPSubData& out,
|
||||||
|
Bool sourceIsVerbatimLevelShadow) {
|
||||||
|
out = MGPSubData{};
|
||||||
|
out.Res = res;
|
||||||
|
out.Target = kMGPipeResourceTargetBuffer;
|
||||||
|
out.SourceIsVerbatimLevelShadow = sourceIsVerbatimLevelShadow ? 1 : 0;
|
||||||
|
if (!MGPipeSetSubDataBufferRange(out, offset, size)) return false;
|
||||||
|
out.Blob.Seg = kMGHostSpanSegNone;
|
||||||
|
out.Blob.Size = size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONE record's destination box caps the offset at 2^31-1 and the size at 2^32-1
|
||||||
|
// (MGPipeTypes.h), so a range beyond either has to be split. The pieces are CONTIGUOUS
|
||||||
|
// and in ASCENDING order, and both properties are load-bearing rather than tidy:
|
||||||
|
// splitting a content write into overlapping or reordered pieces would change what the
|
||||||
|
// backend's queue-and-drain sees, and the Mali WAR-stall fix depends on that queue being
|
||||||
|
// exactly the writes the application made.
|
||||||
|
inline constexpr Uint64 kMGPipeSubDataMaxRecordOffset = 0x7FFFFFFFull;
|
||||||
|
inline constexpr Uint64 kMGPipeSubDataMaxRecordSize = 0xFFFFFFFFull;
|
||||||
|
|
||||||
|
// WITH THE RECORD'S OWN BOUND THE SPLIT IS NOT REACHABLE, and saying so is better than a
|
||||||
|
// loop that reads as if it were: a second piece starts at least 2^32-1 bytes past the
|
||||||
|
// first, which is already past the OFFSET cap, so a range too big for one record is
|
||||||
|
// REFUSED rather than split. The offset cap cannot be split away at all - every piece of
|
||||||
|
// a range that starts past 2^31-1 starts past it too - and a silent truncation is the one
|
||||||
|
// answer that must not happen, so the walk emits nothing and its caller says so once.
|
||||||
|
//
|
||||||
|
// `maxChunk` exists because the record's bound is not the tight one for long: a transport
|
||||||
|
// segment is far smaller (tens of MiB), and that is where this walk starts producing real
|
||||||
|
// splits. It is a parameter now, and exercised at a reachable value by the unit gate, so
|
||||||
|
// that lowering it is one argument rather than a new code path written under pressure.
|
||||||
|
template <class Fn>
|
||||||
|
inline Bool MGPipeForEachSubDataRecordRange(Uint64 offset, Uint64 size, Fn&& piece,
|
||||||
|
Uint64 maxChunk = kMGPipeSubDataMaxRecordSize) {
|
||||||
|
if (offset > kMGPipeSubDataMaxRecordOffset) return false;
|
||||||
|
if (size == 0) return true;
|
||||||
|
if (maxChunk == 0) return false;
|
||||||
|
// Every piece has to be encodable BEFORE any of them is emitted: a half-emitted range
|
||||||
|
// is a partial content write the backend would land as if it were the whole one.
|
||||||
|
const Uint64 chunkCap = maxChunk < kMGPipeSubDataMaxRecordSize ? maxChunk : kMGPipeSubDataMaxRecordSize;
|
||||||
|
for (Uint64 at = offset; at < offset + size; at += chunkCap) {
|
||||||
|
if (at > kMGPipeSubDataMaxRecordOffset) return false;
|
||||||
|
}
|
||||||
|
for (Uint64 at = offset, left = size; left > 0;) {
|
||||||
|
const Uint64 chunk = left > chunkCap ? chunkCap : left;
|
||||||
|
piece(at, chunk);
|
||||||
|
at += chunk;
|
||||||
|
left -= chunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The tracker: handles, the inverse, the sticky mask, the reverse channel
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MGPipeResourceTracker {
|
||||||
|
public:
|
||||||
|
using BufferObject = MG_State::GLState::BufferObject;
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
|
||||||
|
// The handle for `buffer`, minted on first use. Minting is NOT gated on a backend
|
||||||
|
// having registered MGPipeResourceOps: the handle is CLIENT state and
|
||||||
|
// set_vertex_buffers names it whether or not the resource family is switched on, so
|
||||||
|
// gating it would make the vertex-input subsystem emit null handles whenever the
|
||||||
|
// resource subsystem is off. Only the CALLS are gated (D-A1).
|
||||||
|
MGPipeHandle Acquire(BufferObject& buffer) {
|
||||||
|
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
||||||
|
m_bySlot[slot].Object = &buffer;
|
||||||
|
m_bySlot[slot].Gen = handle.Gen;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handle a buffer already has, or the null handle. Never mints - the emission
|
||||||
|
// path calls Acquire, the query paths call this.
|
||||||
|
MGPipeHandle Find(const BufferObject& buffer) const {
|
||||||
|
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
|
||||||
|
// create the constructor emits and the destroy the destructor emits, and a readback
|
||||||
|
// is only ever issued for a live, bound buffer. A WeakPtr would be wrong - the
|
||||||
|
// object does not own itself through a SharedPtr at those two moments. The Gen
|
||||||
|
// compare is what refuses a stale handle rather than resolving it to whatever now
|
||||||
|
// occupies the slot.
|
||||||
|
BufferObject* Resolve(MGPipeHandle handle) const {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_bySlot.size()) return nullptr;
|
||||||
|
const Entry& entry = m_bySlot[slot];
|
||||||
|
if (entry.Object == nullptr || entry.Gen != handle.Gen) return nullptr;
|
||||||
|
if (MGPipeSlots().GenOfSlot(MGPipeKind::Buffer, handle.Slot) != handle.Gen) return nullptr;
|
||||||
|
return entry.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drops the inverse entry and the sticky mask. The CALLER frees the slot afterwards,
|
||||||
|
// in that order (D-L): MGPipeSlotAllocator::Free erases the lifetimeId -> slot
|
||||||
|
// mapping, so anything that has to resolve the handle must do it first.
|
||||||
|
void Retire(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_bySlot.size()) return;
|
||||||
|
m_bySlot[slot] = Entry{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- D-L: was resource_create actually EMITTED for this slot? ----
|
||||||
|
//
|
||||||
|
// The create is gated at its call site (BufferObject's constructor) and the destroy
|
||||||
|
// is gated inside MGPipeEmitResourceDestroyAndFree, so the two ask the SAME question
|
||||||
|
// at two different moments. A buffer constructed while a backend's table was
|
||||||
|
// registered and destroyed after UnregisterBufferBackendOps() would take the second
|
||||||
|
// answer, free its slot, and leave the applier's record Live - on a slot the
|
||||||
|
// allocator is about to hand out again, with the backend's twin (a driver buffer id)
|
||||||
|
// still attached to it. So the answer is LATCHED at the create and the destroy uses
|
||||||
|
// the latched one; the two are then a pair by construction rather than by the
|
||||||
|
// registration outliving every buffer.
|
||||||
|
void NotePublished(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_bySlot.size()) return;
|
||||||
|
m_bySlot[slot].Published = true;
|
||||||
|
}
|
||||||
|
Bool WasPublished(MGPipeHandle handle) const {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
return slot < m_bySlot.size() && m_bySlot[slot].Published;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
|
||||||
|
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
|
||||||
|
// keeps saying so.
|
||||||
|
Uint16 BindMask(MGPipeHandle handle) const {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
return slot < m_bySlot.size() ? m_bySlot[slot].BindMask : Uint16{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
// OR one target's bit into a handle's sticky mask, without looking at the context at
|
||||||
|
// all. This is what closes the sampling window for the two bits anything keys on:
|
||||||
|
// the vertex-input emitters resolve, at EVERY draw, exactly the attribute buffers and
|
||||||
|
// the element-slot buffer, so any buffer ever DRAWN FROM carries its ARRAY_BUFFER /
|
||||||
|
// ELEMENT_ARRAY bit for the rest of its life whether or not it happened to be bound
|
||||||
|
// at a storage op. It grows the table rather than dropping the note: it is called
|
||||||
|
// from the validate point, which is GL-thread by construction, and a slot outside the
|
||||||
|
// table is a buffer whose mint this process has not seen (a unit fixture's
|
||||||
|
// ResetForTest, in practice).
|
||||||
|
void NoteBoundAs(MGPipeHandle handle, BufferTarget target) {
|
||||||
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
|
||||||
|
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.
|
||||||
|
Uint16 RefreshBindMask(GLContext& ctx, const BufferObject& buffer, MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_bySlot.size()) return 0;
|
||||||
|
Entry& entry = m_bySlot[slot];
|
||||||
|
const Uint64 epoch = BindEpoch(ctx);
|
||||||
|
if (epoch == m_bindEpoch && entry.BindMaskEpoch == epoch) return entry.BindMask;
|
||||||
|
m_bindEpoch = epoch;
|
||||||
|
entry.BindMaskEpoch = epoch;
|
||||||
|
Uint16 mask = entry.BindMask;
|
||||||
|
for (const auto target : MG_State::GLState::GlobalBufferTargets) {
|
||||||
|
if (ctx.GetBufferBindingSlot(target).GetBoundObject().get() == &buffer) {
|
||||||
|
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto target : MG_State::GLState::BufferBindPointTargets) {
|
||||||
|
const SizeT touched = ctx.GetTouchedBufferBindingPointCount(target);
|
||||||
|
for (SizeT i = 0; i < touched; ++i) {
|
||||||
|
if (ctx.GetBufferBindingPoint(target, static_cast<Uint>(i)).GetBoundObject().get() == &buffer) {
|
||||||
|
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The index slot is the BOUND VAO's, not BufferState's, so it is not in
|
||||||
|
// GlobalBufferTargets and GetBufferBindingSlot(Index) asserts without a VAO.
|
||||||
|
if (const auto& vao = ctx.GetBoundVertexArray()) {
|
||||||
|
if (vao->GetIndexBufferBindingSlot().GetBoundObject().get() == &buffer) {
|
||||||
|
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(BufferTarget::Index));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
|
||||||
|
if (vao->GetAttribute(static_cast<Uint>(i)).Buffer.get() == &buffer) {
|
||||||
|
mask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(BufferTarget::Vertex));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.BindMask = mask;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the two observables a unit case reads (see the header comment) ----
|
||||||
|
const MGPResourceDesc& LastDesc() const { return m_lastDesc; }
|
||||||
|
Uint64 CreateCount() const { return m_creates; }
|
||||||
|
Uint64 RespecifyCount() const { return m_respecifies; }
|
||||||
|
Uint64 DestroyCount() const { return m_destroys; }
|
||||||
|
Uint64 MapPersistentCount() const { return m_mapPersistents; }
|
||||||
|
|
||||||
|
void NoteDesc(const MGPResourceDesc& desc, Bool isCreate) {
|
||||||
|
m_lastDesc = desc;
|
||||||
|
if (isCreate) {
|
||||||
|
++m_creates;
|
||||||
|
} else {
|
||||||
|
++m_respecifies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
void ResetForTest() {
|
||||||
|
m_bySlot.clear();
|
||||||
|
m_bindEpoch = 0;
|
||||||
|
m_lastDesc = MGPResourceDesc{};
|
||||||
|
m_creates = m_respecifies = m_destroys = m_mapPersistents = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
BufferObject* Object = nullptr;
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Uint16 BindMask = 0;
|
||||||
|
Bool Published = false;
|
||||||
|
Uint64 BindMaskEpoch = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Has any buffer binding moved since the last scan": the sum of the binding-slot
|
||||||
|
// versions, which BindingSlot bumps only on a real change. A collision costs one
|
||||||
|
// skipped rescan of ONE buffer's mask, and the mask is re-scanned at the next
|
||||||
|
// emission whose epoch differs, so it can delay a bit by one storage op and never
|
||||||
|
// drop one - the same over-fire-is-free / under-fire-is-fatal direction every
|
||||||
|
// shutter in Tracker.h takes.
|
||||||
|
//
|
||||||
|
// IT DOES NOT SEE THE 84x4 INDEXED BINDING POINTS, and that is sound only because
|
||||||
|
// BindBufferBase_State / BindBufferRange_State also bind the GENERIC slot for the
|
||||||
|
// same target (GL_Buffer.cpp:1531 says why), so an indexed bind always moves one of
|
||||||
|
// the versions summed here. If that ever stops being true, the CONSTANT /
|
||||||
|
// SHADER_BUFFER / ATOMIC / STREAM_OUTPUT bits start being missed silently and the
|
||||||
|
// repair is to fold GetTouchedBufferBindingPointCount into the epoch.
|
||||||
|
static Uint64 BindEpoch(GLContext& ctx) {
|
||||||
|
Uint64 epoch = 1;
|
||||||
|
for (const auto target : MG_State::GLState::GlobalBufferTargets) {
|
||||||
|
epoch += ctx.GetBufferBindingSlot(target).GetVersion();
|
||||||
|
epoch *= 3;
|
||||||
|
}
|
||||||
|
if (const auto& vao = ctx.GetBoundVertexArray()) {
|
||||||
|
epoch += vao->GetIndexBufferBindingSlot().GetVersion();
|
||||||
|
epoch = MGPipeMixShutterValue(epoch, vao->GetLifetimeId());
|
||||||
|
epoch = MGPipeMixShutterValue(epoch, vao->GetConfigVersion());
|
||||||
|
}
|
||||||
|
return epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same mix Tracker.h's composite shutters use. Spelled here rather than
|
||||||
|
// included so this header does not depend on the tracker.
|
||||||
|
static constexpr Uint64 MGPipeMixShutterValue(Uint64 accumulator, Uint64 value) {
|
||||||
|
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Entry> m_bySlot;
|
||||||
|
Uint64 m_bindEpoch = 0;
|
||||||
|
MGPResourceDesc m_lastDesc{};
|
||||||
|
Uint64 m_creates = 0;
|
||||||
|
Uint64 m_respecifies = 0;
|
||||||
|
Uint64 m_destroys = 0;
|
||||||
|
Uint64 m_mapPersistents = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The monolith's one resource tracker, beside the state tracker, the CSO cache and the
|
||||||
|
// set-hash suppressor.
|
||||||
|
inline MGPipeResourceTracker& MGPipeResourceTrackerInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeSlots()' reason (SlotAllocator.cpp): ~BufferObject reads
|
||||||
|
// and writes this tracker, and the objects that own the last reference to a
|
||||||
|
// BufferObject outlive every function-local static.
|
||||||
|
static MGPipeResourceTracker* tracker = new MGPipeResourceTracker();
|
||||||
|
return *tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// 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).
|
||||||
|
inline void MGPipeClientOnBufferWriteback(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes) {
|
||||||
|
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
MGLOG_E_ONCE("MGPipe: OnBufferWriteback for a handle {%u,%u} that resolves to no buffer",
|
||||||
|
res.Slot, res.Gen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bytes.Seg != kMGHostSpanSegNone) {
|
||||||
|
MGLOG_E_ONCE("MGPipe: OnBufferWriteback carried a transport segment (%u); P3a is monolith only",
|
||||||
|
bytes.Seg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Monolith: Seg is kMGHostSpanSegNone and Offset IS the address of the backend's
|
||||||
|
// mapped bytes (MGPipeTypes.h says so in as many words). Under a transport the
|
||||||
|
// segment resolves first, and that is the phase's edit, not this one's.
|
||||||
|
buffer->WritebackFromBackend(
|
||||||
|
DataPtr{reinterpret_cast<void*>(static_cast<std::uintptr_t>(bytes.Offset)),
|
||||||
|
static_cast<SizeT>(bytes.Size)},
|
||||||
|
static_cast<SizeT>(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A draw or dispatch wrote these ranges. ARCHITECTURE.md 7.1 calls this a NARROWING
|
||||||
|
// channel - the client builds a conservative pending set at its own emission points and
|
||||||
|
// the callback only ever removes from it - so P3a's implementation marks exactly what
|
||||||
|
// the three Espryt MarkGpuWritten sites mark today and the observable behaviour is
|
||||||
|
// unchanged. The narrowing itself is P8/P9's.
|
||||||
|
inline void MGPipeClientOnGpuWritten(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges) {
|
||||||
|
// THE SHAPE IS A CONTRACT POINT, not a formality: the announcement is ONE range
|
||||||
|
// covering kMGPipeWholeBuffer, deliberately not ZERO ranges, because zero will mean
|
||||||
|
// "a fully narrowed set - nothing is dirty" at P8/P9. Marking the whole buffer
|
||||||
|
// written for a zero-range announcement would be the narrowing channel run backwards,
|
||||||
|
// so the shape is asserted here rather than assumed.
|
||||||
|
MOBILEGL_ASSERT(rangeCount == 1 && ranges != nullptr,
|
||||||
|
"OnGpuWritten {slot=%u, gen=%u}: P3a announces exactly one whole-buffer range, "
|
||||||
|
"not %u",
|
||||||
|
res.Slot, res.Gen, static_cast<Uint>(rangeCount));
|
||||||
|
(void)ranges;
|
||||||
|
if (rangeCount == 0) return;
|
||||||
|
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
// Loud, like its sibling above: a backend announcing a write against a handle
|
||||||
|
// this client cannot resolve is a dropped MarkGpuWritten, and a dropped
|
||||||
|
// MarkGpuWritten is a stale shadow read back as if it were current.
|
||||||
|
MGLOG_E_ONCE("MGPipe: OnGpuWritten for a handle {%u,%u} that resolves to no buffer", res.Slot,
|
||||||
|
res.Gen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buffer->MarkGpuWritten();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installed once, and never over an entry a backend already claimed: these two are the
|
||||||
|
// CLIENT's implementations of a backend -> frontend callback, so the backend installs
|
||||||
|
// the rest of the table and these two answer for it.
|
||||||
|
inline void MGPipeInstallClientResourceCallbacks() {
|
||||||
|
if (gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
||||||
|
gMGPipeCallbacks.OnBufferWriteback = &MGPipeClientOnBufferWriteback;
|
||||||
|
}
|
||||||
|
if (gMGPipeCallbacks.OnGpuWritten == nullptr) {
|
||||||
|
gMGPipeCallbacks.OnGpuWritten = &MGPipeClientOnGpuWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/SetHashSuppressor.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list), PLUS
|
||||||
|
// SetFramebufferState, which is not kVarTail at all: MGPFramebufferState carries a
|
||||||
|
// ContentHash for TWO jobs - the server's render-pass memo key and the client's emission
|
||||||
|
// suppressor - and the second one needs a slot here like any other. The enum is
|
||||||
|
// CLIENT-ONLY and is not a wire opcode, so appending before Count is safe.
|
||||||
|
enum class MGPipeSuppressorSlot : Uint32 {
|
||||||
|
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
||||||
|
// P4a - WIRED. The three unit sets' suppressors are not optional and were never a
|
||||||
|
// later phase's: MGPipeTypes.h makes the pattern mandatory for every kVarTail set_*,
|
||||||
|
// because GetTextureBindGeneration() bumps on a REDUNDANT rebind - MC 26.2 rebinds the
|
||||||
|
// same sampler at every texture-unit switch - so an unsuppressed set is a
|
||||||
|
// several-hundred-byte variable-length record per batch, which is the exact regression
|
||||||
|
// the design names. What P3b/P4b owns is the ~175-line BACKEND debounce these replace
|
||||||
|
// (UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and the two
|
||||||
|
// g_*SyncList tables); P4a wires the carrier, P3b/P4b deletes the backend copy.
|
||||||
|
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||||
|
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||||
|
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||||
|
SetShaderBuffers, // P4b
|
||||||
|
SetStreamOutputTargets, // P4b
|
||||||
|
SetVertexAttribDefaults, // P2 - the one consumer that is wired
|
||||||
|
SetFramebufferState, // P4a - wired
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr SizeT kMGPipeSuppressorSlotCount = static_cast<SizeT>(MGPipeSuppressorSlot::Count);
|
||||||
|
|
||||||
|
class MGPipeSetHashSuppressor {
|
||||||
|
public:
|
||||||
|
// True when `contentHash` differs from what this slot last emitted, and LATCHES it.
|
||||||
|
// False means the resolved set has not moved and the call must not go out.
|
||||||
|
Bool ShouldEmit(MGPipeSuppressorSlot slot, Uint64 contentHash) {
|
||||||
|
const Uint64 latched = contentHash == 0 ? 1 : contentHash;
|
||||||
|
const SizeT index = static_cast<SizeT>(slot);
|
||||||
|
if (m_lastEmitted[index] == latched) return false;
|
||||||
|
m_lastEmitted[index] = latched;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A context change or a server reset: what the server has is no longer what this
|
||||||
|
// slot last emitted, so the next resolved set must go out whatever it hashes to.
|
||||||
|
void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast<SizeT>(slot)] = 0; }
|
||||||
|
|
||||||
|
void InvalidateAll() {
|
||||||
|
for (SizeT i = 0; i < kMGPipeSuppressorSlotCount; ++i) m_lastEmitted[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 == "never emitted". Exposed for the unit test, which is what pins that the
|
||||||
|
// reserved value really is reserved.
|
||||||
|
Uint64 LastEmitted(MGPipeSuppressorSlot slot) const {
|
||||||
|
return m_lastEmitted[static_cast<SizeT>(slot)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Array<Uint64, kMGPipeSuppressorSlotCount> m_lastEmitted{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The monolith's one suppressor, beside the tracker and the CSO cache.
|
||||||
|
inline MGPipeSetHashSuppressor& MGPipeSetHashSuppressorInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||||
|
// rule covers every MGPipe process singleton, not only the ones on today's death
|
||||||
|
// paths.
|
||||||
|
static MGPipeSetHashSuppressor* suppressor = new MGPipeSetHashSuppressor();
|
||||||
|
return *suppressor;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// SlotAllocator.h. Compiled only under MOBILEGL_PIPE_PUSH.
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
namespace {
|
||||||
|
// The ShaderCso band the ordinary allocator must never enter: the top 1/16 of the
|
||||||
|
// ShaderCso slot space is reserved for PROGRAM PIPELINE COMPOSITES, which are minted
|
||||||
|
// client-side out of the stage programs bound to a pipeline object. Reserving a band
|
||||||
|
// rather than a flag keeps the composite resolver's lifetime bookkeeping out of here
|
||||||
|
// (MGPipeHandles.h, ARCHITECTURE.md 5.6.3).
|
||||||
|
Bool SlotIsAllocatable(MGPipeKind kind, Uint32 slot) {
|
||||||
|
if (slot < kMGPipeFirstAllocatableSlot) return false;
|
||||||
|
if (kind != MGPipeKind::ShaderCso) return true;
|
||||||
|
return slot < kMGPipeShaderCsoCompositeSlotBase;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
MGPipeSlotAllocator::KindState& MGPipeSlotAllocator::StateOf(MGPipeKind kind) {
|
||||||
|
const SizeT index = static_cast<SizeT>(kind);
|
||||||
|
MOBILEGL_ASSERT(index < kKindCount, "MGPipeKind %zu out of range", index);
|
||||||
|
return m_kinds[index < kKindCount ? index : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MGPipeSlotAllocator::KindState& MGPipeSlotAllocator::StateOf(MGPipeKind kind) const {
|
||||||
|
const SizeT index = static_cast<SizeT>(kind);
|
||||||
|
MOBILEGL_ASSERT(index < kKindCount, "MGPipeKind %zu out of range", index);
|
||||||
|
return m_kinds[index < kKindCount ? index : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeSlotAllocator::SlotState* MGPipeSlotAllocator::EntryOf(KindState& state, MGPipeKind kind,
|
||||||
|
Uint32 slot) {
|
||||||
|
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(slot)) {
|
||||||
|
const SizeT index = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||||
|
if (index >= state.BandSlots.size()) return nullptr;
|
||||||
|
return &state.BandSlots[index];
|
||||||
|
}
|
||||||
|
if (slot >= state.Slots.size()) return nullptr;
|
||||||
|
return &state.Slots[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MGPipeSlotAllocator::SlotState*
|
||||||
|
MGPipeSlotAllocator::EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot) {
|
||||||
|
return EntryOf(const_cast<KindState&>(state), kind, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::Allocate(MGPipeKind kind) {
|
||||||
|
KindState& state = StateOf(kind);
|
||||||
|
if (state.Slots.empty()) {
|
||||||
|
// Slot 0 exists so the vector is slot-indexed, and is never handed out.
|
||||||
|
state.Slots.resize(kMGPipeFirstAllocatableSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 slot = 0;
|
||||||
|
Bool reused = false;
|
||||||
|
while (!state.FreeList.empty()) {
|
||||||
|
const Uint32 candidate = state.FreeList.back();
|
||||||
|
state.FreeList.pop_back();
|
||||||
|
if (!SlotIsAllocatable(kind, candidate)) continue;
|
||||||
|
slot = candidate;
|
||||||
|
reused = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reused) {
|
||||||
|
slot = static_cast<Uint32>(state.Slots.size());
|
||||||
|
MOBILEGL_ASSERT(SlotIsAllocatable(kind, slot),
|
||||||
|
"MGPipe slot space of kind %u is exhausted at slot %u",
|
||||||
|
static_cast<Uint32>(kind), slot);
|
||||||
|
if (!SlotIsAllocatable(kind, slot)) return kMGPipeNullHandle;
|
||||||
|
state.Slots.emplace_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotState& entry = state.Slots[slot];
|
||||||
|
if (entry.EverHandedOut) {
|
||||||
|
// The one place Gen may move. 2^32 recycles of ONE slot is ~50 days of continuous
|
||||||
|
// churn at one recycle per frame at 1000 fps, which is why the bound is asserted
|
||||||
|
// in a debug allocator rather than defended in release.
|
||||||
|
MOBILEGL_ASSERT(entry.Gen != ~Uint32{0},
|
||||||
|
"MGPipe handle generation wrapped on kind %u slot %u; {slot, gen} is "
|
||||||
|
"no longer unique",
|
||||||
|
static_cast<Uint32>(kind), slot);
|
||||||
|
++entry.Gen;
|
||||||
|
}
|
||||||
|
entry.EverHandedOut = true;
|
||||||
|
entry.Live = true;
|
||||||
|
entry.LifetimeId = 0;
|
||||||
|
++state.LiveCount;
|
||||||
|
return MGPipeHandle{slot, entry.Gen};
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::AllocateFor(MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
|
const MGPipeHandle handle = Allocate(kind);
|
||||||
|
if (MGPipeHandleIsNull(handle)) return handle;
|
||||||
|
KindState& state = StateOf(kind);
|
||||||
|
state.Slots[handle.Slot].LifetimeId = lifetimeId;
|
||||||
|
if (lifetimeId != 0) {
|
||||||
|
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
|
||||||
|
"lifetime id %llu already owns a slot of kind %u",
|
||||||
|
static_cast<unsigned long long>(lifetimeId), static_cast<Uint32>(kind));
|
||||||
|
state.ByLifetimeId[lifetimeId] = handle.Slot;
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::AllocateComposite(Uint64 lifetimeId) {
|
||||||
|
// P4a, D-H7. The mirror image of Allocate() above, restricted to the band that one
|
||||||
|
// refuses, and kept in a table of its own so both spaces stay DENSE: the band's base
|
||||||
|
// is 983040, and minting one composite into the slot-indexed vector would allocate
|
||||||
|
// ~23 MB of SlotState for a single program pipeline.
|
||||||
|
KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||||
|
|
||||||
|
Uint32 slot = 0;
|
||||||
|
Bool reused = false;
|
||||||
|
if (!state.BandFreeList.empty()) {
|
||||||
|
slot = state.BandFreeList.back();
|
||||||
|
state.BandFreeList.pop_back();
|
||||||
|
reused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reused) {
|
||||||
|
const SizeT next = kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size();
|
||||||
|
slot = static_cast<Uint32>(next);
|
||||||
|
// The band's own exhaustion assert, mirroring Allocate()'s: a composite that
|
||||||
|
// cannot be minted is a NAMED failure, not a silent fall-through into the ordinary
|
||||||
|
// program slots, which is exactly what reserving a band rather than setting a flag
|
||||||
|
// buys.
|
||||||
|
MOBILEGL_ASSERT(next < kMGPipeShaderCsoSlotLimit,
|
||||||
|
"the MGPipe ShaderCso COMPOSITE band is exhausted at slot %zu; a "
|
||||||
|
"program-pipeline composite cannot be minted and must not take an "
|
||||||
|
"ordinary program's slot",
|
||||||
|
next);
|
||||||
|
if (next >= kMGPipeShaderCsoSlotLimit) return kMGPipeNullHandle;
|
||||||
|
state.BandSlots.emplace_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotState* entry = EntryOf(state, MGPipeKind::ShaderCso, slot);
|
||||||
|
if (entry == nullptr) return kMGPipeNullHandle;
|
||||||
|
if (entry->EverHandedOut) {
|
||||||
|
MOBILEGL_ASSERT(entry->Gen != ~Uint32{0},
|
||||||
|
"MGPipe handle generation wrapped on the ShaderCso composite band, "
|
||||||
|
"slot %u; {slot, gen} is no longer unique",
|
||||||
|
slot);
|
||||||
|
++entry->Gen;
|
||||||
|
}
|
||||||
|
entry->EverHandedOut = true;
|
||||||
|
entry->Live = true;
|
||||||
|
entry->LifetimeId = lifetimeId;
|
||||||
|
++state.LiveCount;
|
||||||
|
// The band's share of LiveCount, so CompositeLiveCount() can answer without a walk.
|
||||||
|
++state.BandLiveCount;
|
||||||
|
if (lifetimeId != 0) {
|
||||||
|
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
|
||||||
|
"lifetime id %llu already owns a ShaderCso slot",
|
||||||
|
static_cast<unsigned long long>(lifetimeId));
|
||||||
|
state.ByLifetimeId[lifetimeId] = slot;
|
||||||
|
}
|
||||||
|
return MGPipeHandle{slot, entry->Gen};
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
|
||||||
|
if (lifetimeId == 0) return kMGPipeNullHandle;
|
||||||
|
const KindState& state = StateOf(kind);
|
||||||
|
const auto it = state.ByLifetimeId.find(lifetimeId);
|
||||||
|
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
|
||||||
|
const SlotState* entry = EntryOf(state, kind, it->second);
|
||||||
|
if (entry == nullptr || !entry->Live) return kMGPipeNullHandle;
|
||||||
|
return MGPipeHandle{it->second, entry->Gen};
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
|
const MGPipeHandle existing = FindByLifetimeId(kind, lifetimeId);
|
||||||
|
if (!MGPipeHandleIsNull(existing)) return existing;
|
||||||
|
return AllocateFor(kind, lifetimeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
|
||||||
|
KindState& state = StateOf(kind);
|
||||||
|
SlotState* entry = EntryOf(state, kind, handle.Slot);
|
||||||
|
if (entry == nullptr) return;
|
||||||
|
// A stale handle must not free the slot its successor now owns - that is the whole
|
||||||
|
// reason the generation is in the key. It is also what makes the SECOND of a
|
||||||
|
// composite's two independent release paths a proven no-op.
|
||||||
|
if (!entry->Live || entry->Gen != handle.Gen) return;
|
||||||
|
if (entry->LifetimeId != 0) {
|
||||||
|
const auto it = state.ByLifetimeId.find(entry->LifetimeId);
|
||||||
|
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
|
||||||
|
state.ByLifetimeId.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry->Live = false;
|
||||||
|
entry->LifetimeId = 0;
|
||||||
|
--state.LiveCount;
|
||||||
|
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(handle.Slot)) {
|
||||||
|
--state.BandLiveCount;
|
||||||
|
state.BandFreeList.push_back(handle.Slot);
|
||||||
|
} else {
|
||||||
|
state.FreeList.push_back(handle.Slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool MGPipeSlotAllocator::IsLive(MGPipeKind kind, MGPipeHandle handle) const {
|
||||||
|
const SlotState* entry = EntryOf(StateOf(kind), kind, handle.Slot);
|
||||||
|
return entry != nullptr && entry->Live && entry->Gen == handle.Gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||||
|
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||||
|
return entry != nullptr ? entry->Gen : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||||
|
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||||
|
return entry != nullptr ? entry->LifetimeId : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
|
||||||
|
// THE ORDINARY SPACE ONLY, and the band is reported by CompositeHighWater() below.
|
||||||
|
// Folding the two would pin this at ~983k from the first composite mint onward and
|
||||||
|
// take the ordinary space's "the high-water mark did not move" assertion away for the
|
||||||
|
// rest of the process - the assertion that catches a dense table that never shrinks,
|
||||||
|
// which is the leak shape this allocator exists to make visible. Two spaces, two
|
||||||
|
// numbers, two real assertions. See SlotAllocator.h.
|
||||||
|
return static_cast<Uint32>(StateOf(kind).Slots.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::CompositeHighWater() const {
|
||||||
|
const KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||||
|
// One past the highest composite slot ever handed out; exactly the base when none ever
|
||||||
|
// was, so the number is monotone from the first mint and a LEAKED COMPOSITE MOVES IT.
|
||||||
|
return static_cast<Uint32>(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::CompositeLiveCount() const {
|
||||||
|
return StateOf(MGPipeKind::ShaderCso).BandLiveCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::FreeCount(MGPipeKind kind) const {
|
||||||
|
const KindState& state = StateOf(kind);
|
||||||
|
return static_cast<Uint32>(state.FreeList.size() + state.BandFreeList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeSlotAllocator::CompositeFreeCount() const {
|
||||||
|
return static_cast<Uint32>(StateOf(MGPipeKind::ShaderCso).BandFreeList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeSlotAllocator::Reset() {
|
||||||
|
for (KindState& state : m_kinds) {
|
||||||
|
state.Slots.clear();
|
||||||
|
state.FreeList.clear();
|
||||||
|
state.BandSlots.clear();
|
||||||
|
state.BandFreeList.clear();
|
||||||
|
state.ByLifetimeId.clear();
|
||||||
|
state.LiveCount = 0;
|
||||||
|
state.BandLiveCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MGPipeSlotAllocator& MGPipeSlots() {
|
||||||
|
// NEVER DESTROYED, deliberately (one allocation for the life of the process). A
|
||||||
|
// frontend object's destructor reaches this allocator - ~BufferObject through
|
||||||
|
// MGPipeEmitResourceDestroyAndFree, ~VertexArrayObject through the death notice - and
|
||||||
|
// MG_Backend/MGPipe/PipeInputs.h's gPipeInputs holds SharedPtrs to those objects at
|
||||||
|
// namespace scope, so they are destroyed by __run_exit_handlers AFTER this
|
||||||
|
// function-local static would have been. A destroyed allocator then answers
|
||||||
|
// FindByLifetimeId out of a freed hash table and Free() writes into freed vectors -
|
||||||
|
// an exit-time heap corruption whose fatality depends only on the allocator's layout.
|
||||||
|
static MGPipeSlotAllocator* allocator = new MGPipeSlotAllocator();
|
||||||
|
return *allocator;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/SlotAllocator.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
class MGPipeSlotAllocator {
|
||||||
|
public:
|
||||||
|
static constexpr SizeT kKindCount = static_cast<SizeT>(MGPipeKind::KindCount);
|
||||||
|
|
||||||
|
// A fresh {slot, gen} of this kind, from the free list if one is waiting and from the
|
||||||
|
// high-water mark otherwise. Never returns slot 0 (reserved: null, and the default
|
||||||
|
// framebuffer for kind Framebuffer), and never returns a ShaderCso slot inside the
|
||||||
|
// composite band, which the program-pipeline resolver mints out of separately.
|
||||||
|
MGPipeHandle Allocate(MGPipeKind kind);
|
||||||
|
// 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.
|
||||||
|
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
|
||||||
|
// object, so the map key is unique for the life of the process.
|
||||||
|
MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const;
|
||||||
|
// FindByLifetimeId, then AllocateFor when it misses. The ordinary client path.
|
||||||
|
MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId);
|
||||||
|
|
||||||
|
// Returns the slot to the free list. The Gen bump happens on the NEXT handout of that
|
||||||
|
// slot, not here, so a handle that is freed twice cannot skip a generation and the
|
||||||
|
// "gen moves only on reuse" contract holds for an object that is never reused.
|
||||||
|
void Free(MGPipeKind kind, MGPipeHandle handle);
|
||||||
|
|
||||||
|
Bool IsLive(MGPipeKind kind, MGPipeHandle handle) const;
|
||||||
|
// 0 for a slot that was never handed out; the generation of the LAST handout
|
||||||
|
// 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.
|
||||||
|
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
|
||||||
|
// base when none ever was. Kind ShaderCso is the only kind with a band, so it is
|
||||||
|
// implied - as it is for AllocateComposite. A LEAKED COMPOSITE MOVES THIS and moves
|
||||||
|
// nothing else, which is what the composite's own leak case asserts on.
|
||||||
|
Uint32 CompositeHighWater() const;
|
||||||
|
// Live slots of this kind, ORDINARY AND COMPOSITE TOGETHER for ShaderCso: a live
|
||||||
|
// composite is a live ShaderCso, the applier's two record tables are one object class,
|
||||||
|
// and a caller asking "how many shader CSOs does this client hold" wants both. The
|
||||||
|
// band's own count is CompositeLiveCount(); the ordinary space's is the difference.
|
||||||
|
Uint32 LiveCount(MGPipeKind kind) const;
|
||||||
|
Uint32 CompositeLiveCount() const;
|
||||||
|
// Slots waiting on a free list. Also BOTH SPACES for ShaderCso, for LiveCount's
|
||||||
|
// reason and with the same caveat: a caller that needs to know WHICH space a slot went
|
||||||
|
// back to reads CompositeFreeCount() and subtracts.
|
||||||
|
Uint32 FreeCount(MGPipeKind kind) const;
|
||||||
|
Uint32 CompositeFreeCount() const;
|
||||||
|
|
||||||
|
// Context teardown / server reset / a unit test's fixture.
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct SlotState {
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Bool Live = false;
|
||||||
|
Bool EverHandedOut = false;
|
||||||
|
Uint64 LifetimeId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KindState {
|
||||||
|
// Indexed by slot; [0] is the reserved slot and is never live.
|
||||||
|
Vector<SlotState> Slots;
|
||||||
|
Vector<Uint32> FreeList;
|
||||||
|
// P4a: the ShaderCso COMPOSITE band, indexed by (slot - the band's base) and
|
||||||
|
// EMPTY for every other kind. A SECOND VECTOR RATHER THAN MORE OF THE FIRST, and
|
||||||
|
// it is not a micro-optimisation: the band starts at 983040, so minting one
|
||||||
|
// composite into the slot-indexed vector above would allocate ~983k SlotStates -
|
||||||
|
// ~23 MB - for a single program pipeline, and a consumer that sized a table off
|
||||||
|
// HighWater would pay the same shape again with a far bigger record. Both spaces
|
||||||
|
// stay dense against their own high-water mark, which is the property this
|
||||||
|
// allocator exists to give the server.
|
||||||
|
Vector<SlotState> BandSlots;
|
||||||
|
Vector<Uint32> BandFreeList;
|
||||||
|
UnorderedMap<Uint64, Uint32> ByLifetimeId;
|
||||||
|
Uint32 LiveCount = 0;
|
||||||
|
// The band's share of LiveCount above, so the two spaces can be reported apart
|
||||||
|
// without walking either table. Always 0 for every kind but ShaderCso.
|
||||||
|
Uint32 BandLiveCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
KindState& StateOf(MGPipeKind kind);
|
||||||
|
const KindState& StateOf(MGPipeKind kind) const;
|
||||||
|
// The SlotState a (kind, slot) names, in whichever of the two vectors holds it, or
|
||||||
|
// null when the slot has never been handed out. One resolver, so a caller that forgets
|
||||||
|
// the band cannot exist.
|
||||||
|
static SlotState* EntryOf(KindState& state, MGPipeKind kind, Uint32 slot);
|
||||||
|
static const SlotState* EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot);
|
||||||
|
|
||||||
|
Array<KindState, kKindCount> m_kinds{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The monolith's one client allocator. Under split there is one per client context.
|
||||||
|
MGPipeSlotAllocator& MGPipeSlots();
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,864 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/Tracker.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
// One bit per row of the ARCHITECTURE.md 5.2 table, hand-written rather than generated:
|
||||||
|
// the list is design, not derived data, and the generator has nothing to derive it from.
|
||||||
|
enum class MGPipeDirty : Uint32 {
|
||||||
|
// ---- value class: P2 emits for these five ----
|
||||||
|
NewRenderState = 0, // RenderState::m_version -> set_dynamic_state
|
||||||
|
NewPipelineState, // RenderState::m_pipelineStateVersion -> create/bind_render_state
|
||||||
|
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
|
||||||
|
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
||||||
|
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
||||||
|
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a and the other three from
|
||||||
|
// P4a - the program family, one subsystem, three bits because the frontend moves them
|
||||||
|
// as three separate events ----
|
||||||
|
NewVertexElements, // the bound VAO's attribute configuration -> create/bind_vertex_elements
|
||||||
|
NewShader, // the current program's link version -> create/bind_shader_state,
|
||||||
|
// set_draw_program, set_dispatch_program (P4a)
|
||||||
|
NewShaderBindings, // image units, block bindings, uniform write set (P4a)
|
||||||
|
NewGlobalConstants, // the default-uniform-block image -> set_global_constants (P4a)
|
||||||
|
// ---- object class. THE FIRST TWO ARE P3a's, not P3b/P4b's: the roadmap puts
|
||||||
|
// set_vertex_buffers and set_index_buffer in the same phase as the vertex-elements
|
||||||
|
// trio, and this comment said otherwise until the commit that wired them. THE NEXT
|
||||||
|
// FOUR ARE P4a's. The last three are still computed and counted only, until P4b. ----
|
||||||
|
NewVertexBuffers, // -> set_vertex_buffers (P3a)
|
||||||
|
NewIndexBuffer, // -> set_index_buffer (P3a)
|
||||||
|
NewFramebuffer, // -> set_framebuffer_state, per bound target (P4a)
|
||||||
|
NewSamplerViews, // -> set_sampler_views (P4a)
|
||||||
|
NewSamplers, // -> bind_sampler_states (P4a)
|
||||||
|
NewShaderImages, // -> set_shader_images (P4a)
|
||||||
|
NewConstBuffers,
|
||||||
|
NewShaderBuffers,
|
||||||
|
NewSoTargets,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr SizeT kMGPipeDirtyCount = static_cast<SizeT>(MGPipeDirty::Count);
|
||||||
|
static_assert(kMGPipeDirtyCount <= 32, "the dirty mask is a Uint32");
|
||||||
|
|
||||||
|
inline constexpr Uint32 MGPipeDirtyBit(MGPipeDirty bit) {
|
||||||
|
return Uint32{1} << static_cast<Uint32>(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The five P2 emits for. Each phase's constant survives as the next phase's A/B control
|
||||||
|
// and as what a test compares the subsystem map against, so none of them is edited in
|
||||||
|
// place when a later phase takes more bits over.
|
||||||
|
inline constexpr Uint32 kMGPipeDirtyEmittedAtP2 =
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewRenderState) | MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewPixelPack) | MGPipeDirtyBit(MGPipeDirty::NewPatchState) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults);
|
||||||
|
|
||||||
|
// The three P3a adds: the vertex-input family, all on one subsystem.
|
||||||
|
inline constexpr Uint32 kMGPipeDirtyEmittedAtP3a =
|
||||||
|
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers) | MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer);
|
||||||
|
|
||||||
|
// The SEVEN P4a adds, across FOUR subsystems: bits 6/7/8 are the program family, 11 the
|
||||||
|
// framebuffer, and 12/13/14 the sampler-view / sampler-state / image-unit sets. Added
|
||||||
|
// rather than edited into the two above, for the reason those two exist: each phase's
|
||||||
|
// constant survives as the next phase's A/B control and as what a test compares the
|
||||||
|
// subsystem map against.
|
||||||
|
//
|
||||||
|
// EVERY ONE OF THESE SHUTTERS WAS ALREADY COMPUTED, LATCHED AND COUNTED before P4a; what
|
||||||
|
// P4a adds is an emitter for them. That is why this is a one-line constant and not seven
|
||||||
|
// new shutters - and it is also why the two narrowings below are stated as requirements.
|
||||||
|
inline constexpr Uint32 kMGPipeDirtyEmittedAtP4a =
|
||||||
|
kMGPipeDirtyEmittedAtP3a | MGPipeDirtyBit(MGPipeDirty::NewShader) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewShaderBindings) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewFramebuffer) | MGPipeDirtyBit(MGPipeDirty::NewSamplerViews) |
|
||||||
|
MGPipeDirtyBit(MGPipeDirty::NewSamplers) | MGPipeDirtyBit(MGPipeDirty::NewShaderImages);
|
||||||
|
|
||||||
|
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
|
||||||
|
"NEW_RENDER_STATE",
|
||||||
|
"NEW_PIPELINE_STATE",
|
||||||
|
"NEW_PIXEL_PACK",
|
||||||
|
"NEW_PATCH_STATE",
|
||||||
|
"NEW_VERTEX_ATTRIB_DEFAULTS",
|
||||||
|
"NEW_VERTEX_ELEMENTS",
|
||||||
|
"NEW_SHADER",
|
||||||
|
"NEW_SHADER_BINDINGS",
|
||||||
|
"NEW_GLOBAL_CONSTANTS",
|
||||||
|
"NEW_VERTEX_BUFFERS",
|
||||||
|
"NEW_INDEX_BUFFER",
|
||||||
|
"NEW_FRAMEBUFFER",
|
||||||
|
"NEW_SAMPLER_VIEWS",
|
||||||
|
"NEW_SAMPLERS",
|
||||||
|
"NEW_SHADER_IMAGES",
|
||||||
|
"NEW_CONST_BUFFERS",
|
||||||
|
"NEW_SHADER_BUFFERS",
|
||||||
|
"NEW_SO_TARGETS",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which runtime MOBILEGL_PIPE_PUSH subsystem bit gates a dirty bit's emission. Zero for
|
||||||
|
// a bit P2 does not emit, which is what makes "the bitmask is a true per-subsystem A/B"
|
||||||
|
// literally true rather than approximately.
|
||||||
|
inline constexpr Uint64 MGPipeSubsystemForDirty(MGPipeDirty bit) {
|
||||||
|
switch (bit) {
|
||||||
|
case MGPipeDirty::NewRenderState:
|
||||||
|
case MGPipeDirty::NewPipelineState:
|
||||||
|
return kMGPipeSubsystemRenderState;
|
||||||
|
case MGPipeDirty::NewPixelPack:
|
||||||
|
return kMGPipeSubsystemPixelPack;
|
||||||
|
case MGPipeDirty::NewPatchState:
|
||||||
|
return kMGPipeSubsystemPatchState;
|
||||||
|
case MGPipeDirty::NewVertexAttribDefaults:
|
||||||
|
return kMGPipeSubsystemVertexAttribDefaults;
|
||||||
|
// P3a's three, all one subsystem: create/bind_vertex_elements, set_vertex_buffers
|
||||||
|
// and set_index_buffer are the vertex-input family and an operator switching it off
|
||||||
|
// has to get the whole family's legacy arm, not two thirds of it.
|
||||||
|
// PipeFill.cpp's SubsystemForEmitter carries the pairing static_asserts.
|
||||||
|
case MGPipeDirty::NewVertexElements:
|
||||||
|
case MGPipeDirty::NewVertexBuffers:
|
||||||
|
case MGPipeDirty::NewIndexBuffer:
|
||||||
|
return kMGPipeSubsystemVertexInput;
|
||||||
|
// P4a's seven, across four subsystems. 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.
|
||||||
|
//
|
||||||
|
// The program family is three bits because the frontend moves them separately - a
|
||||||
|
// relink, a binding change and a uniform write are three events - but one subsystem,
|
||||||
|
// because an operator switching programs off has to get the whole family's legacy arm.
|
||||||
|
// Same for the three unit sets: create_sampler_state, create_sampler_view and the
|
||||||
|
// three kVarTail sets are one family, and half of it is not a control.
|
||||||
|
case MGPipeDirty::NewShader:
|
||||||
|
case MGPipeDirty::NewShaderBindings:
|
||||||
|
case MGPipeDirty::NewGlobalConstants:
|
||||||
|
return kMGPipeSubsystemPrograms;
|
||||||
|
case MGPipeDirty::NewFramebuffer:
|
||||||
|
return kMGPipeSubsystemFramebuffer;
|
||||||
|
case MGPipeDirty::NewSamplerViews:
|
||||||
|
case MGPipeDirty::NewSamplers:
|
||||||
|
case MGPipeDirty::NewShaderImages:
|
||||||
|
return kMGPipeSubsystemSamplers;
|
||||||
|
// NO BIT NAMES kMGPipeSubsystemTextureResources, and that is deliberate rather than an
|
||||||
|
// omission: the texture and renderbuffer resource_* calls and set_texture_params are
|
||||||
|
// dispatched from the GL entry points that cause them - a constructor, a storage
|
||||||
|
// definition, a glTexParameter - not from a dirty walk, exactly as P3a's buffer family
|
||||||
|
// is. Bit 10 gates those dispatch sites; there is no dirty bit to map onto it and
|
||||||
|
// there must not be one, or the emission would be gated twice and disagree with itself.
|
||||||
|
default:
|
||||||
|
// The remaining bits have no call of their own until P4b, so there is no
|
||||||
|
// subsystem to switch and the residual fill keeps supplying their fields.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: each mix takes a 64-bit input into a 64-bit
|
||||||
|
// accumulator, so two DIFFERENT vertex configurations collide with probability ~2^-64
|
||||||
|
// per pair, and the inputs are a monotone lifetime id, a monotone configuration version
|
||||||
|
// and a widened slot version - 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.
|
||||||
|
inline constexpr Uint64 MGPipeMixShutter(Uint64 accumulator, Uint64 value) {
|
||||||
|
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Uint16 counter widened at the TRACKER boundary, never in MG_State
|
||||||
|
// (ARCHITECTURE.md 5.2: MG_State is not changed for this). A decrease is a wrap and adds
|
||||||
|
// 65536. A wrap is harmless locally - one extra re-push, never a missed one - which is
|
||||||
|
// exactly what TrackerTest.WrapAroundRePushesButNeverMisses pins.
|
||||||
|
//
|
||||||
|
// THE ONE CASE IT CANNOT SEE, stated because "never a missed push" is otherwise stronger
|
||||||
|
// than what is true: the wrap test is `now < m_last`, so a counter that advances by
|
||||||
|
// EXACTLY 65536 (or a multiple) between two walks reads as unchanged. That needs 65536
|
||||||
|
// render-state mutations inside one verb boundary, and it is pre-existing in class -
|
||||||
|
// both backends already compare raw Uint16 versions the same way - so P2 records it
|
||||||
|
// rather than widening MG_State's counters, which ARCHITECTURE.md 5.2 rules out.
|
||||||
|
class MGPipeWidenedCounter {
|
||||||
|
public:
|
||||||
|
Uint64 Observe(Uint16 now) {
|
||||||
|
if (m_started && now < m_last) m_high += 0x10000ull;
|
||||||
|
m_started = true;
|
||||||
|
m_last = now;
|
||||||
|
return m_high + now;
|
||||||
|
}
|
||||||
|
void Reset() {
|
||||||
|
m_high = 0;
|
||||||
|
m_last = 0;
|
||||||
|
m_started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint64 m_high = 0;
|
||||||
|
Uint16 m_last = 0;
|
||||||
|
Bool m_started = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MGPipeTracker {
|
||||||
|
public:
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
|
||||||
|
// The dirty walk. Compares every shutter against what was last pushed, LATCHES the
|
||||||
|
// new values, counts the fires per verb class, and returns the mask. Latching here
|
||||||
|
// rather than after emission is deliberate: a bit whose subsystem is switched off is
|
||||||
|
// not emitted, but its fields are then still pulled by the residual fill, so the
|
||||||
|
// pushed block is correct either way and a bit can never fire twice for one change.
|
||||||
|
Uint32 Update(GLContext& ctx, MGPipeVerbClass verbClass) {
|
||||||
|
// A different context is a different server: nothing the tracker latched about
|
||||||
|
// the old one says anything about this one, and the first walk on a fresh
|
||||||
|
// context must publish a COMPLETE state rather than an increment.
|
||||||
|
if (m_context != &ctx) {
|
||||||
|
Reset();
|
||||||
|
m_context = &ctx;
|
||||||
|
}
|
||||||
|
const Bool wasPrimed = m_primed;
|
||||||
|
|
||||||
|
Uint64 now[kMGPipeDirtyCount];
|
||||||
|
const RenderStateParameters& render = ctx.GetRenderStateParameters();
|
||||||
|
|
||||||
|
// ---- bits 0..1: the two Uint16 render-state counters, widened HERE ----
|
||||||
|
now[Index(MGPipeDirty::NewRenderState)] =
|
||||||
|
m_renderStateVersion.Observe(static_cast<Uint16>(ctx.GetRenderStateParametersVersion()));
|
||||||
|
now[Index(MGPipeDirty::NewPipelineState)] =
|
||||||
|
m_pipelineStateVersion.Observe(static_cast<Uint16>(ctx.GetPipelineStateVersion()));
|
||||||
|
|
||||||
|
// ---- bit 4 and the value-class bits 5..8 ----
|
||||||
|
now[Index(MGPipeDirty::NewVertexAttribDefaults)] = ctx.GetAnyVertexAttribDefaultGeneration();
|
||||||
|
|
||||||
|
const auto& vao = ctx.GetBoundVertexArray();
|
||||||
|
const Uint64 vaoIdentity =
|
||||||
|
vao ? MGPipeMixShutter(vao->GetLifetimeId(), vao->GetConfigVersion()) : 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.
|
||||||
|
const auto& program = ctx.GetCurrentProgram();
|
||||||
|
Uint64 shader = 0;
|
||||||
|
Uint64 bindings = 0;
|
||||||
|
Uint64 constants = 0;
|
||||||
|
Uint64 programImages = 0;
|
||||||
|
// THE PROGRAM INPUT OF THE PROGRAM-RESOLVED VIEW SET (P4a fable seam F-1).
|
||||||
|
// set_sampler_views is resolved for the program in use (SamplerEmit.h: the sampler
|
||||||
|
// uniform's TYPE picks which of a unit's targets is the view) and the emitter
|
||||||
|
// memoises that resolution on (lifetime id, link version, backend state version). A
|
||||||
|
// shutter that read only the texture generations therefore missed a glUseProgram:
|
||||||
|
// `glBindTexture x N; glUseProgram(P1); draw; glUseProgram(P2); draw` moved nothing
|
||||||
|
// bit 12 read, so the view set stayed P1's - and E's record epoch, keyed on the two
|
||||||
|
// set serials, then never rebuilt the texture sync list for P2 either. This value is
|
||||||
|
// that memo key, and bit 12 mixes it in below: over-firing costs one re-resolution
|
||||||
|
// the set-hash suppressor absorbs, under-firing left the record describing the
|
||||||
|
// previous program's units.
|
||||||
|
Uint64 opaqueUnits = 0;
|
||||||
|
if (program) {
|
||||||
|
shader = MGPipeMixShutter(program->GetLifetimeId(), program->GetLinkVersion());
|
||||||
|
bindings = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(MGPipeMixShutter(program->GetImageUnitVersion(),
|
||||||
|
program->GetBackendStateVersion()),
|
||||||
|
program->GetBlockBindingVersion()),
|
||||||
|
program->GetUniformWriteSetVersion());
|
||||||
|
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
|
||||||
|
// THE IDENTITY IS MIXED IN (P4a fable seam F-2), exactly as the pipeline arm
|
||||||
|
// below mixes stageLinks into its half: the counter alone is a per-program
|
||||||
|
// number two programs routinely share - 0 == 0 for any pair that never moved an
|
||||||
|
// image unit through glUniform1i, and 0 == 0 against no program at all - so a
|
||||||
|
// glUseProgram between them fired nothing, set_shader_images' window stayed the
|
||||||
|
// previous program's, and a program whose only image is a BUFFER image (E's
|
||||||
|
// SD-4: nothing else moves between the bind and the dispatch) never reached the
|
||||||
|
// record at all.
|
||||||
|
programImages = MGPipeMixShutter(shader, program->GetImageUnitVersion());
|
||||||
|
opaqueUnits = MGPipeMixShutter(shader, program->GetBackendStateVersion());
|
||||||
|
} else if (const auto& pipeline = ctx.GetBoundProgramPipeline(); pipeline) {
|
||||||
|
using Pipeline = MG_State::GLState::ProgramPipelineObject;
|
||||||
|
// THE FIELDS ARE READ DIRECTLY RATHER THAN THROUGH THE TWO FUNCTIONS THAT
|
||||||
|
// ALREADY PACK THEM, and that is a gate constraint, not a preference. Calling
|
||||||
|
// ComputeDrawProgramSignature() / ComputeUniformMirrorVersions() would say
|
||||||
|
// "the same pairs the composite cache and the uniform-mirror gate compare"
|
||||||
|
// far better than this loop does - but gen_pipe_dirty_surface.py derives a
|
||||||
|
// shutter by following each accessor to the member it returns, and both of
|
||||||
|
// those build a LOCAL array and return that, which it cannot place. A shutter
|
||||||
|
// naming them is UNRESOLVED, and then every DirtySurface.def row that names
|
||||||
|
// bits 6, 7, 8 or 14 loses its verdict - including the derivation that is the
|
||||||
|
// only mechanism able to catch the next under-fire here. So the pairs are
|
||||||
|
// spelled out, and the two static_asserts below are what say they must stay in
|
||||||
|
// step with the functions they mirror.
|
||||||
|
static_assert(sizeof(Pipeline::DrawProgramSignature) ==
|
||||||
|
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
|
||||||
|
"bit 6 reads the {lifetimeId, linkVersion} pair per graphics "
|
||||||
|
"stage that ComputeDrawProgramSignature packs");
|
||||||
|
static_assert(sizeof(Pipeline::UniformMirrorVersions) ==
|
||||||
|
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
|
||||||
|
"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.
|
||||||
|
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
|
||||||
|
Uint64 stageState = 0;
|
||||||
|
Uint64 stageImages = 0;
|
||||||
|
// The per-stage sampler/image unit assignments alone (glUniform1i on a stage
|
||||||
|
// program's sampler moves its backend state version and reaches the composite
|
||||||
|
// through the uniform mirror), for bit 12's program input below.
|
||||||
|
Uint64 stageOpaque = 0;
|
||||||
|
for (SizeT stage = 0; stage < Pipeline::kGraphicsStageCount; ++stage) {
|
||||||
|
const auto& staged = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||||
|
if (!staged) continue;
|
||||||
|
stageLinks = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(stageLinks, staged->GetLifetimeId()), staged->GetLinkVersion());
|
||||||
|
stageState = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(MGPipeMixShutter(stageState, staged->GetBackendStateVersion()),
|
||||||
|
MGPipeMixShutter(staged->GetUBOContentVersion(),
|
||||||
|
staged->GetBlockBindingVersion())),
|
||||||
|
staged->GetUniformWriteSetVersion());
|
||||||
|
stageImages = MGPipeMixShutter(stageImages, staged->GetImageUnitVersion());
|
||||||
|
stageOpaque = MGPipeMixShutter(stageOpaque, staged->GetBackendStateVersion());
|
||||||
|
}
|
||||||
|
shader = stageLinks;
|
||||||
|
stageState = MGPipeMixShutter(stageLinks, stageState);
|
||||||
|
bindings = MGPipeMixShutter(stageState, stageImages);
|
||||||
|
constants = stageState;
|
||||||
|
programImages = MGPipeMixShutter(stageLinks, stageImages);
|
||||||
|
opaqueUnits = MGPipeMixShutter(stageLinks, stageOpaque);
|
||||||
|
}
|
||||||
|
now[Index(MGPipeDirty::NewShader)] = shader;
|
||||||
|
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)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// ---- the object-class bits 9..17 ----
|
||||||
|
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
|
||||||
|
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
|
||||||
|
const Uint64 buffers = ctx.GetAnyBufferChangeGeneration();
|
||||||
|
|
||||||
|
// Bit 9. The VAO attribute aggregate mixed with the bound VAO's identity is
|
||||||
|
// already exact for the SET - it is bumped by all three Bump*Version functions,
|
||||||
|
// which are the only writers of an attribute's format, buffer or enable state -
|
||||||
|
// and a driver-id re-mint that moves no client counter is caught server-side by
|
||||||
|
// the backend's own id generation.
|
||||||
|
//
|
||||||
|
// THE PENDING BASE INSTANCE IS MIXED IN, and this is a deviation from the design
|
||||||
|
// note that said "keep the shutter" (recorded in client-v1.md): the draw's
|
||||||
|
// baseInstance is now an EXPLICIT field of set_vertex_buffers and a
|
||||||
|
// ContentHash input, and it moves neither the attribute aggregate nor the VAO
|
||||||
|
// identity. Without it here, a draw whose only change is its base instance would
|
||||||
|
// never reach the emitter at all and the server would keep the previous fetch
|
||||||
|
// shift - which is the same silently-wrong-geometry the backend's
|
||||||
|
// baseInstanceDirty flag exists to prevent, one level further out. It fires
|
||||||
|
// extra only on the draws that actually carry one.
|
||||||
|
now[Index(MGPipeDirty::NewVertexBuffers)] = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity), m_pendingBaseInstance);
|
||||||
|
// Bit 10, NARROWED (P3a, D-I). It used to mix the whole buffer-CONTENT aggregate
|
||||||
|
// with the VAO identity and therefore fired on any buffer write anywhere; what
|
||||||
|
// it guards is one binding slot, so it now reads that slot's own version and the
|
||||||
|
// identity of what is bound to it. The version is a WRAPPING Uint16 bumped only
|
||||||
|
// on a real change, so it goes through the widened counter at this boundary; the
|
||||||
|
// bound object's lifetime id joins it because identity is what closes the wrap
|
||||||
|
// hole. The VAO identity stays in the mix because the element slot BELONGS to
|
||||||
|
// the bound VAO - switching VAOs switches slots.
|
||||||
|
Uint64 indexShutter = 0;
|
||||||
|
if (vao) {
|
||||||
|
const auto& indexSlot = vao->GetIndexBufferBindingSlot();
|
||||||
|
const auto& indexObject = indexSlot.GetBoundObject();
|
||||||
|
indexShutter = MGPipeMixShutter(m_indexSlotVersion.Observe(indexSlot.GetVersion()),
|
||||||
|
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.
|
||||||
|
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(
|
||||||
|
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||||
|
m_framebufferBind.Observe(
|
||||||
|
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
|
||||||
|
m_readFramebufferBind.Observe(
|
||||||
|
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
|
||||||
|
// Bit 12 reads FOUR things (F-1): the two texture aggregates, the bind generation
|
||||||
|
// and the program input computed above. The params aggregate is here because
|
||||||
|
// SamplerEmit.h drops a unit's view to null when SamplesAsIncompleteTexture says so,
|
||||||
|
// and that predicate reads the effective sampler's filters - a glTexParameteri(
|
||||||
|
// MIN_FILTER) that completes a texture fired bit 13 and not this one, so the entry
|
||||||
|
// stayed null. The program input is here because the set is resolved FOR THE
|
||||||
|
// PROGRAM IN USE, and a glUseProgram alone moved nothing this shutter read.
|
||||||
|
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.
|
||||||
|
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
|
||||||
|
ctx.GetTextureBindGeneration());
|
||||||
|
now[Index(MGPipeDirty::NewShaderImages)] = MGPipeMixShutter(
|
||||||
|
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages),
|
||||||
|
ctx.GetTextureBindGeneration());
|
||||||
|
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
|
||||||
|
now[Index(MGPipeDirty::NewShaderBuffers)] = buffers;
|
||||||
|
now[Index(MGPipeDirty::NewSoTargets)] =
|
||||||
|
MGPipeMixShutter(buffers, ctx.GetTransformFeedbackGeneration());
|
||||||
|
|
||||||
|
Uint32 dirty = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||||
|
// Bits 2 and 3 are handled below: they are BitwiseEqual shutters, not
|
||||||
|
// counters, so they have no entry in `now`.
|
||||||
|
if (i == Index(MGPipeDirty::NewPixelPack) || i == Index(MGPipeDirty::NewPatchState)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!m_primed || now[i] != m_lastPushed[i]) dirty |= Uint32{1} << static_cast<Uint32>(i);
|
||||||
|
m_lastPushed[i] = now[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- bit 2: the PACK half of the pixel store, BitwiseEqual ----
|
||||||
|
const PixelStoreParameters pack = ctx.GetPixelStoreParameters(false);
|
||||||
|
if (!m_primed || std::memcmp(&pack, &m_pack, sizeof(pack)) != 0) {
|
||||||
|
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPixelPack);
|
||||||
|
m_pack = pack;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- bit 3: the patch trio, BitwiseEqual, and NaN IS LEGAL ----
|
||||||
|
// A NaN outer level is a legal glPatchParameterfv value and must compare equal to
|
||||||
|
// itself (ARCHITECTURE.md 5.2). Float equality says it is not; memcmp says it is,
|
||||||
|
// which is the whole reason this is a byte compare.
|
||||||
|
PatchTrio patch{};
|
||||||
|
patch.PatchVertices = render.PatchVertices;
|
||||||
|
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = render.PatchDefaultOuterLevel[i];
|
||||||
|
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = render.PatchDefaultInnerLevel[i];
|
||||||
|
if (!m_primed || std::memcmp(&patch, &m_patch, sizeof(patch)) != 0) {
|
||||||
|
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPatchState);
|
||||||
|
m_patch = patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_primed = true;
|
||||||
|
m_freshlyPrimed = !wasPrimed;
|
||||||
|
m_lastDirty = dirty;
|
||||||
|
|
||||||
|
if (MG_Util::PipeStats::Enabled()) {
|
||||||
|
const SizeT cls = static_cast<SizeT>(verbClass);
|
||||||
|
++m_walks[cls];
|
||||||
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||||
|
if (dirty & (Uint32{1} << static_cast<Uint32>(i))) ++m_fires[i][cls];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context teardown, server reset, a unit test's fixture. The next Update returns
|
||||||
|
// every bit set, which is what makes the first verb on a fresh context publish a
|
||||||
|
// complete state rather than an increment. Deliberately does NOT clear the fire
|
||||||
|
// tallies: they are a per-run measurement, not per-context state.
|
||||||
|
//
|
||||||
|
// AND IT DELIBERATELY DOES NOT CLEAR m_pendingBaseInstance. Everything else this
|
||||||
|
// function clears is a LATCH describing what the server was last told; the pending
|
||||||
|
// base instance is THIS CALL'S ARGUMENT, written by the draw entry point one
|
||||||
|
// statement before MGP_FILL and not yet read by anybody. Update() calls Reset() from
|
||||||
|
// inside itself whenever the current GLContext pointer moves, so clearing it here
|
||||||
|
// meant that `eglMakeCurrent(ctxB); glDrawArraysInstancedBaseInstance(..., 7)` put a
|
||||||
|
// BaseInstance of 0 on the wire - one silently mis-shifted instanced draw per context
|
||||||
|
// switch, on the emulation path, with nothing to catch it. The value is cleared by the
|
||||||
|
// verb that consumes it (PipeFill.cpp's step 3, and its no-context early return) and
|
||||||
|
// by MGPipeLeaveVerb, which is where a per-call argument belongs.
|
||||||
|
void Reset() {
|
||||||
|
std::memset(m_lastPushed, 0, sizeof(m_lastPushed));
|
||||||
|
m_renderStateVersion.Reset();
|
||||||
|
m_pipelineStateVersion.Reset();
|
||||||
|
m_framebufferBind.Reset();
|
||||||
|
m_readFramebufferBind.Reset();
|
||||||
|
m_indexSlotVersion.Reset();
|
||||||
|
m_pack = PixelStoreParameters{};
|
||||||
|
m_patch = PatchTrio{};
|
||||||
|
m_staged = RenderStateParameters{};
|
||||||
|
m_stagedAttribs = AttribDefaults{};
|
||||||
|
m_context = nullptr;
|
||||||
|
m_lastDirty = 0;
|
||||||
|
m_primed = false;
|
||||||
|
m_freshlyPrimed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() {
|
||||||
|
std::memset(m_fires, 0, sizeof(m_fires));
|
||||||
|
std::memset(m_walks, 0, sizeof(m_walks));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 FireCount(MGPipeDirty bit, MGPipeVerbClass verbClass) const {
|
||||||
|
return m_fires[Index(bit)][static_cast<SizeT>(verbClass)];
|
||||||
|
}
|
||||||
|
Uint64 FireCount(MGPipeDirty bit) const {
|
||||||
|
Uint64 total = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_fires[Index(bit)][i];
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
Uint64 WalkCount(MGPipeVerbClass verbClass) const {
|
||||||
|
return m_walks[static_cast<SizeT>(verbClass)];
|
||||||
|
}
|
||||||
|
Uint64 WalkCount() const {
|
||||||
|
Uint64 total = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_walks[i];
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 LastDirty() const { return m_lastDirty; }
|
||||||
|
Bool Primed() const { return m_primed; }
|
||||||
|
// True when the LAST Update was the first one after a Reset - a fresh context, or a
|
||||||
|
// server reset. The emission step reads it to send a COMPLETE state rather than an
|
||||||
|
// increment against a staging mirror that describes a context that is gone.
|
||||||
|
Bool FreshlyPrimed() const { return m_freshlyPrimed; }
|
||||||
|
|
||||||
|
// "What the server has" (P2 brief D8). set_dynamic_state sends the dynamic chunks
|
||||||
|
// that differ from this, which is the chunk-level suppressor; a chunk that
|
||||||
|
// memcmp-matches is not sent at all.
|
||||||
|
RenderStateParameters& Staged() { return m_staged; }
|
||||||
|
const RenderStateParameters& Staged() const { return m_staged; }
|
||||||
|
|
||||||
|
// The same mirror for the 32 glVertexAttrib* defaults: set_vertex_attrib_defaults
|
||||||
|
// names only the attributes that differ from it, which is the var-tail's own
|
||||||
|
// suppressor underneath D11's set-hash one.
|
||||||
|
using AttribDefaults = Array<MG_State::GLState::CurrentVertexAttributeValue,
|
||||||
|
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>;
|
||||||
|
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.
|
||||||
|
void SetPendingBaseInstance(Uint32 baseInstance) { m_pendingBaseInstance = baseInstance; }
|
||||||
|
Uint32 PendingBaseInstance() const { return m_pendingBaseInstance; }
|
||||||
|
void ClearPendingBaseInstance() { m_pendingBaseInstance = 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
|
||||||
|
|
||||||
|
struct PatchTrio {
|
||||||
|
Uint PatchVertices;
|
||||||
|
Float Outer[4];
|
||||||
|
Float Inner[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint64 m_lastPushed[kMGPipeDirtyCount]{};
|
||||||
|
MGPipeWidenedCounter m_renderStateVersion;
|
||||||
|
MGPipeWidenedCounter m_pipelineStateVersion;
|
||||||
|
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
|
||||||
|
// that wrapped would let a composite shutter repeat and cost a missed fire.
|
||||||
|
MGPipeWidenedCounter m_framebufferBind;
|
||||||
|
// P4a: the READ framebuffer binding slot's version, its own counter for the same
|
||||||
|
// reason the draw one exists. Two counters rather than one over both slots: a single
|
||||||
|
// widened counter fed two independent Uint16s reads a decrease as a wrap on every
|
||||||
|
// alternation and would add 65536 per switch, which costs nothing in correctness
|
||||||
|
// (over-firing) but makes the high word meaningless.
|
||||||
|
MGPipeWidenedCounter m_readFramebufferBind;
|
||||||
|
// The BOUND VAO's element-array slot version, widened for the same reason. One
|
||||||
|
// counter over a slot that changes with the bound VAO: a stale high word can only
|
||||||
|
// ADD a fire, never drop one, and the VAO identity in the same mix is what makes a
|
||||||
|
// switch between two VAOs differ whatever their slot versions read.
|
||||||
|
MGPipeWidenedCounter m_indexSlotVersion;
|
||||||
|
Uint32 m_pendingBaseInstance = 0;
|
||||||
|
// Bits 2 and 3 are BitwiseEqual shutters, not counters.
|
||||||
|
PixelStoreParameters m_pack{};
|
||||||
|
PatchTrio m_patch{};
|
||||||
|
|
||||||
|
RenderStateParameters m_staged{};
|
||||||
|
AttribDefaults m_stagedAttribs{};
|
||||||
|
|
||||||
|
const void* m_context = nullptr;
|
||||||
|
Uint32 m_lastDirty = 0;
|
||||||
|
Bool m_primed = false;
|
||||||
|
Bool m_freshlyPrimed = false;
|
||||||
|
|
||||||
|
Uint64 m_fires[kMGPipeDirtyCount][kMGPipeVerbClassCount]{};
|
||||||
|
Uint64 m_walks[kMGPipeVerbClassCount]{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ONE attribute default, flattened onto the wire (P2 brief D10). A named function rather
|
||||||
|
// than four lines inside the emitter because this flattening is the whole correctness
|
||||||
|
// question of set_vertex_attrib_defaults: a CurrentVertexAttributeValue is one value in
|
||||||
|
// three views and GLContext converts NUMERICALLY between them, so four words alone are
|
||||||
|
// not the value - glVertexAttrib4f(loc, 1.5f, ...) leaves 1 in intValue and 0x3FC00000 in
|
||||||
|
// floatValue. MGPAttribValue::ValueClass is what makes the four words readable again, and
|
||||||
|
// TrackerAttribPayload pins that here instead of leaving it to the emitter's shape.
|
||||||
|
inline void MGPipeFillAttribValue(Uint32 location,
|
||||||
|
const MG_State::GLState::CurrentVertexAttributeValue& value,
|
||||||
|
Uint32 writtenClass, MGPAttribValue& out) {
|
||||||
|
out = MGPAttribValue{};
|
||||||
|
out.Location = location;
|
||||||
|
out.ValueClass = static_cast<Uint8>(writtenClass);
|
||||||
|
static_assert(sizeof(out.Data) == sizeof(value.floatValue), "MGPAttribValue::Data is four words");
|
||||||
|
switch (writtenClass) {
|
||||||
|
case MG_State::GLState::kVertexAttribValueClassInt:
|
||||||
|
std::memcpy(out.Data, value.intValue.data(), sizeof(out.Data));
|
||||||
|
break;
|
||||||
|
case MG_State::GLState::kVertexAttribValueClassUint:
|
||||||
|
std::memcpy(out.Data, value.uintValue.data(), sizeof(out.Data));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
std::memcpy(out.Data, value.floatValue.data(), sizeof(out.Data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The monolith's one tracker. Under split there is one per client context; the context
|
||||||
|
// identity check inside Update is what makes the single instance safe today.
|
||||||
|
inline MGPipeTracker& MGPipeTrackerInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeSlots()' reason (MG_Impl/Pipe/SlotAllocator.cpp). The
|
||||||
|
// rule is stated over the SET of MGPipe process singletons rather than over the two
|
||||||
|
// that a frontend destructor reaches today: which of them a destructor reaches is a
|
||||||
|
// property of the emitters, and the emitters change (C-1 added a second reaching
|
||||||
|
// path in one commit). One allocation per process, no destructor to lose - this type
|
||||||
|
// has none - and nothing can then answer a late call out of freed storage.
|
||||||
|
static MGPipeTracker* tracker = new MGPipeTracker();
|
||||||
|
return *tracker;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/Pipe/VertexInputEmit.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||||
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#include <MG_Impl/Pipe/Tracker.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
|
||||||
|
#include <xxhash.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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.
|
||||||
|
inline MGPVertexAttribWire MGPipeBuildVertexAttribWire(const MG_State::GLState::VertexAttribute& attrib,
|
||||||
|
Uint32 bindingIndex) {
|
||||||
|
// ASSERT RATHER THAN ASSUME, in both directions, because the three narrowing casts
|
||||||
|
// below cross a package boundary: VertexArrayObject is another package's file and its
|
||||||
|
// 32-slot bound is its invariant, not this one's, so a BindingIndex of 256 would wrap
|
||||||
|
// to 0 and silently point every attribute at binding 0, and a negative Stride (the
|
||||||
|
// frontend field is a signed int) would arrive as a ~4 GiB unsigned distance.
|
||||||
|
MOBILEGL_ASSERT(bindingIndex < 256u,
|
||||||
|
"MGPVertexAttribWire::BindingIndex is a Uint8 and cannot carry %u",
|
||||||
|
static_cast<Uint>(bindingIndex));
|
||||||
|
MOBILEGL_ASSERT(attrib.Size >= 0 && attrib.Size <= 255,
|
||||||
|
"MGPVertexAttribWire::Size is a Uint8 and cannot carry %d", attrib.Size);
|
||||||
|
MGPVertexAttribWire wire{};
|
||||||
|
wire.Offset = static_cast<Uint64>(attrib.Offset);
|
||||||
|
wire.Stride = static_cast<Int32>(attrib.Stride);
|
||||||
|
wire.Type = static_cast<Uint32>(attrib.Type);
|
||||||
|
wire.Size = static_cast<Uint8>(attrib.Size);
|
||||||
|
wire.Enabled = attrib.Enabled ? 1 : 0;
|
||||||
|
wire.Normalized = attrib.Normalized ? 1 : 0;
|
||||||
|
wire.IsInteger = attrib.IsInteger ? 1 : 0;
|
||||||
|
wire.IsLong = attrib.IsLong ? 1 : 0;
|
||||||
|
wire.IsBgra = attrib.IsBgra ? 1 : 0;
|
||||||
|
wire.BindingIndex = static_cast<Uint8>(bindingIndex);
|
||||||
|
return wire;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ARB_vertex_attrib_binding view. Its initial Stride is 16, not 0 (GL 4.6 core table
|
||||||
|
// 23.4), which is why the wire form keeps it signed and copies it verbatim.
|
||||||
|
inline MGPVertexBindingPointWire
|
||||||
|
MGPipeBuildVertexBindingPointWire(const MG_State::GLState::VertexBufferBindingPoint& point) {
|
||||||
|
MGPVertexBindingPointWire wire{};
|
||||||
|
wire.Offset = static_cast<Uint64>(point.Offset);
|
||||||
|
wire.Stride = static_cast<Int32>(point.Stride);
|
||||||
|
wire.Divisor = static_cast<Uint32>(point.Divisor);
|
||||||
|
return wire;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// D-H2.3: the content hash, WITH BaseInstance in it
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// A HARD REQUIREMENT, not a nicety. set_vertex_buffers is suppressed on an unchanged
|
||||||
|
// hash (SetHashSuppressor.h's SetVertexBuffers slot), so a baseInstance that moved while
|
||||||
|
// the buffer set did not would be suppressed and the server would keep the previous
|
||||||
|
// fetch shift - exactly the bug the backend's baseInstanceDirty flag exists to prevent.
|
||||||
|
inline Uint64 MGPipeVertexBufferSetContentHash(const MGPVertexBuffer* entries, Uint32 start, Uint32 count,
|
||||||
|
Uint32 baseInstance) {
|
||||||
|
Uint64 hash = XXH64(entries, static_cast<SizeT>(count) * sizeof(MGPVertexBuffer), 0);
|
||||||
|
hash = MGPipeMixShutter(hash, start);
|
||||||
|
hash = MGPipeMixShutter(hash, count);
|
||||||
|
hash = MGPipeMixShutter(hash, baseInstance);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// The emitter
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MGPipeVertexInputEmitter {
|
||||||
|
public:
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
|
||||||
|
static constexpr SizeT kAttribs = static_cast<SizeT>(VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||||
|
static constexpr SizeT kBindings = static_cast<SizeT>(VertexArrayObject::MAX_VERTEX_ATTRIB_BINDINGS);
|
||||||
|
static_assert(kAttribs <= kMGPipeMaxVertexAttribs && kBindings <= kMGPipeMaxVertexAttribs,
|
||||||
|
"both declared counts are bounded by kMGPipeMaxVertexAttribs");
|
||||||
|
|
||||||
|
// create/bind_vertex_elements. D-G3's three arms, verbatim:
|
||||||
|
//
|
||||||
|
// no VAO bound -> bind the null handle (legal, and it means
|
||||||
|
// exactly "no VAO bound")
|
||||||
|
// the bound VAO CHANGED -> (re)create if its configuration moved since
|
||||||
|
// this handle last published one, then bind
|
||||||
|
// the same VAO, configuration MOVED-> create on the SAME handle, and do NOT rebind
|
||||||
|
//
|
||||||
|
// The latch is PER HANDLE, in a slot-indexed table, so ping-ponging between two VAOs
|
||||||
|
// re-binds but never re-creates either. A Uint32 configuration version does not wrap
|
||||||
|
// in any realistic run and is compared directly; the tracker's widened counter is
|
||||||
|
// for the Uint16s and is not needed here.
|
||||||
|
Uint64 EmitVertexElements(GLContext& ctx) {
|
||||||
|
const auto& vao = ctx.GetBoundVertexArray();
|
||||||
|
if (!vao) {
|
||||||
|
if (!MGPipeHandleIsNull(m_boundHandle)) {
|
||||||
|
MGPipeApplyBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||||
|
++m_binds;
|
||||||
|
m_boundHandle = kMGPipeNullHandle;
|
||||||
|
m_boundLifetimeId = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 lifetimeId = vao->GetLifetimeId();
|
||||||
|
const Uint32 configVersion = vao->GetConfigVersion();
|
||||||
|
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId);
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_latch.size()) m_latch.resize(slot + 1);
|
||||||
|
Latch& latch = m_latch[slot];
|
||||||
|
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion ||
|
||||||
|
latch.Gen != handle.Gen;
|
||||||
|
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion);
|
||||||
|
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
|
||||||
|
MGPipeApplyBindVertexElements(HandleOnly(handle));
|
||||||
|
++m_binds;
|
||||||
|
bytes += sizeof(MGPHandleOnly);
|
||||||
|
m_boundHandle = handle;
|
||||||
|
m_boundLifetimeId = lifetimeId;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_vertex_buffers. Espryt consumes RESOLVED attributes, so the set is one entry
|
||||||
|
// per attribute slot with BindingIndex == the attribute index; Start is 0 and Count
|
||||||
|
// is the highest ENABLED attribute plus one, which is the 32-slot prefix walk the
|
||||||
|
// dirty bit is specified over.
|
||||||
|
//
|
||||||
|
// A client-memory array is Res == kMGPipeNullHandle, and that is not a hole: it is
|
||||||
|
// exactly how the server learns "this attribute is client-sourced, upload it
|
||||||
|
// yourself". Its store genuinely does not exist at this moment - the client-array
|
||||||
|
// uploader runs after PrepareForDraw, at the draw entry point - and moving that
|
||||||
|
// resolution to the client is P8's.
|
||||||
|
Uint64 EmitVertexBuffers(GLContext& ctx, Uint32 baseInstance) {
|
||||||
|
const auto& vao = ctx.GetBoundVertexArray();
|
||||||
|
Uint32 count = 0;
|
||||||
|
if (vao) {
|
||||||
|
for (SizeT i = 0; i < kAttribs; ++i) {
|
||||||
|
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
|
const auto& attrib = vao->GetAttribute(static_cast<Uint>(i));
|
||||||
|
MGPVertexBuffer& entry = m_entries[i];
|
||||||
|
entry = MGPVertexBuffer{};
|
||||||
|
entry.Res = attrib.Buffer ? MGPipeSlots().Acquire(MGPipeKind::Buffer,
|
||||||
|
attrib.Buffer->GetLifetimeId())
|
||||||
|
: kMGPipeNullHandle;
|
||||||
|
// D-A3's sticky mask, ORed HERE rather than only sampled at a storage op.
|
||||||
|
// This is the bit that survives the DSA idiom: a buffer defined through
|
||||||
|
// glNamedBuffer* may never be bound at any resource emission, but a draw
|
||||||
|
// that fetches from it resolves it right here, on the GL thread, at every
|
||||||
|
// draw. Sticky, so one draw is enough for the rest of its life.
|
||||||
|
MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex);
|
||||||
|
// The attribute's own byte offset lives in MGPVertexAttribWire::Offset,
|
||||||
|
// so the entry's is the BINDING's, which the frontend already folded in.
|
||||||
|
entry.Offset = 0;
|
||||||
|
// Signed on the frontend, unsigned on the wire, and a negative one would
|
||||||
|
// arrive as a ~4 GiB fetch distance rather than as an error.
|
||||||
|
MOBILEGL_ASSERT(attrib.Stride >= 0, "a resolved vertex stride is never negative (%d)",
|
||||||
|
attrib.Stride);
|
||||||
|
entry.Stride = static_cast<Uint32>(attrib.Stride);
|
||||||
|
entry.Divisor = static_cast<Uint32>(attrib.Divisor);
|
||||||
|
entry.BindingIndex = static_cast<Uint32>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 hash = MGPipeVertexBufferSetContentHash(m_entries.data(), 0, count, baseInstance);
|
||||||
|
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetVertexBuffers, hash)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_lastBuffers = MGPVertexBuffers{};
|
||||||
|
m_lastBuffers.Start = 0;
|
||||||
|
m_lastBuffers.Count = count;
|
||||||
|
// THE DRAW'S RAW value. The client never pre-shifts an offset and never learns
|
||||||
|
// whether the server emulated the shift or let GL_EXT_base_instance do it -
|
||||||
|
// emulation is server-owned.
|
||||||
|
m_lastBuffers.BaseInstance = baseInstance;
|
||||||
|
m_lastBuffers.ContentHash = hash;
|
||||||
|
MGPipeApplySetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||||
|
++m_bufferSets;
|
||||||
|
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_index_buffer. An INDEPENDENT call, not a subset of the vertex-elements
|
||||||
|
// configuration version (D5) - the index slot is explicitly outside the VAO's
|
||||||
|
// m_configVersion, and the shutter for it is bit 10's, narrowed in Tracker.h.
|
||||||
|
//
|
||||||
|
// Offset and IndexSize are 0 here and the draw verb overrides them: at the validate
|
||||||
|
// point there is no draw to read them from, and the applier stores what it is given.
|
||||||
|
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||||
|
const auto& vao = ctx.GetBoundVertexArray();
|
||||||
|
m_lastIndex = MGPIndexBuffer{};
|
||||||
|
if (vao) {
|
||||||
|
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
|
||||||
|
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
|
||||||
|
// The ELEMENT_ARRAY bit, and it is the one the split path keys on
|
||||||
|
// (kCapNeedsHostIndexBytes -> restart rewriting, multi-draw flattening).
|
||||||
|
// Noted at every draw for RefreshBindMask's reason: an EBO defined through
|
||||||
|
// DSA and unbound before its last respecify would otherwise never publish
|
||||||
|
// it, and getting that bit wrong is invisible in monolith.
|
||||||
|
MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MGPipeApplySetIndexBuffer(m_lastIndex);
|
||||||
|
++m_indexSets;
|
||||||
|
return sizeof(MGPIndexBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- what a unit case reads. None of it costs a copy: the emitter builds INTO
|
||||||
|
// these and hands the applier the same pointers. ----
|
||||||
|
const Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs>& LastAttributes() const { return m_attributes; }
|
||||||
|
const Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs>& LastBindingPoints() const {
|
||||||
|
return m_bindingPoints;
|
||||||
|
}
|
||||||
|
const MGPVertexElements& LastElements() const { return m_lastElements; }
|
||||||
|
const MGPVertexBuffers& LastVertexBuffers() const { return m_lastBuffers; }
|
||||||
|
const Array<MGPVertexBuffer, kMGPipeMaxVertexAttribs>& LastEntries() const { return m_entries; }
|
||||||
|
const MGPIndexBuffer& LastIndexBuffer() const { return m_lastIndex; }
|
||||||
|
MGPipeHandle BoundHandle() const { return m_boundHandle; }
|
||||||
|
Uint64 CreateCount() const { return m_creates; }
|
||||||
|
Uint64 BindCount() const { return m_binds; }
|
||||||
|
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.
|
||||||
|
Bool RecordIsPublished(MGPipeHandle handle) const {
|
||||||
|
if (MGPipeHandleIsNull(handle)) return false;
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot >= m_latch.size()) return false;
|
||||||
|
const Latch& latch = m_latch[slot];
|
||||||
|
return latch.RecordLive && latch.RecordGen == handle.Gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The record named by `handle` is gone from the applier. Also drops the bound-handle
|
||||||
|
// memo when it named it, so the client's idea of BoundVertexElements and the applier's
|
||||||
|
// (which MGPipeApplyDeleteVertexElements just cleared for the same handle) stay in
|
||||||
|
// step rather than diverging until the next bind happens to correct it.
|
||||||
|
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||||
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (slot < m_latch.size() && m_latch[slot].RecordGen == handle.Gen) {
|
||||||
|
m_latch[slot] = Latch{};
|
||||||
|
}
|
||||||
|
if (m_boundHandle == handle) {
|
||||||
|
m_boundHandle = kMGPipeNullHandle;
|
||||||
|
m_boundLifetimeId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh context is a fresh server: the applier's records are gone, so every latch
|
||||||
|
// this emitter holds describes objects the server no longer has. Called from the
|
||||||
|
// validate point's FreshlyPrimed arm beside MGPipeApplierReset and the suppressor's
|
||||||
|
// InvalidateAll, for the same reason they are.
|
||||||
|
//
|
||||||
|
// The PER-CONTEXT half only - see RecordIsPublished above for why RecordLive/RecordGen
|
||||||
|
// survive. Re-creating a configuration the applier already holds is a bounded
|
||||||
|
// over-fire (MGPipeApplyCreateVertexElements starts the record over); forgetting that
|
||||||
|
// it holds one at all would leak the record and its slot at the object's death.
|
||||||
|
void Reset() {
|
||||||
|
for (Latch& latch : m_latch) {
|
||||||
|
latch.Published = false;
|
||||||
|
latch.Gen = 0;
|
||||||
|
latch.ConfigVersion = 0;
|
||||||
|
}
|
||||||
|
m_boundHandle = kMGPipeNullHandle;
|
||||||
|
m_boundLifetimeId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Latch {
|
||||||
|
// The PER-CONTEXT half: "has this emitter told THIS server about this handle's
|
||||||
|
// configuration". Cleared by Reset() at every make-current.
|
||||||
|
Bool Published = false;
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Uint32 ConfigVersion = 0;
|
||||||
|
// The RECORD half: "does the applier hold a create_vertex_elements record at this
|
||||||
|
// slot, for this generation". Lives as long as the record does - see
|
||||||
|
// RecordIsPublished.
|
||||||
|
Bool RecordLive = false;
|
||||||
|
Uint32 RecordGen = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
|
||||||
|
MGPHandleOnly only{};
|
||||||
|
only.Handle = handle;
|
||||||
|
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
|
||||||
|
return only;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 EmitCreate(const VertexArrayObject& vao, MGPipeHandle handle, Latch& latch, Uint32 configVersion) {
|
||||||
|
// ALL 32 OF EACH, deliberately. The record DECLARES both counts and the applier
|
||||||
|
// refuses one whose counts do not describe its own blob, so a self-describing
|
||||||
|
// record is the cheap shape - and G6 is stated over all 32 slots, which a
|
||||||
|
// truncated set could not answer. It rides create_vertex_elements only, i.e.
|
||||||
|
// once per configuration change, never per draw.
|
||||||
|
for (SizeT i = 0; i < kAttribs; ++i) {
|
||||||
|
m_attributes[i] = MGPipeBuildVertexAttribWire(vao.GetAttribute(static_cast<Uint>(i)),
|
||||||
|
vao.GetAttributeBindingIndex(static_cast<Uint>(i)));
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < kBindings; ++i) {
|
||||||
|
m_bindingPoints[i] = MGPipeBuildVertexBindingPointWire(vao.GetBindingPoint(static_cast<Uint>(i)));
|
||||||
|
}
|
||||||
|
// Attributes first, then binding points, both ascending and contiguous.
|
||||||
|
constexpr SizeT kAttribBytes = kAttribs * sizeof(MGPVertexAttribWire);
|
||||||
|
constexpr SizeT kBindingBytes = kBindings * sizeof(MGPVertexBindingPointWire);
|
||||||
|
std::memcpy(m_blob.data(), m_attributes.data(), kAttribBytes);
|
||||||
|
std::memcpy(m_blob.data() + kAttribBytes, m_bindingPoints.data(), kBindingBytes);
|
||||||
|
|
||||||
|
m_lastElements = MGPVertexElements{};
|
||||||
|
m_lastElements.Cso = handle;
|
||||||
|
m_lastElements.AttributeCount = static_cast<Uint32>(kAttribs);
|
||||||
|
m_lastElements.BindingPointCount = static_cast<Uint32>(kBindings);
|
||||||
|
m_lastElements.Blob.Seg = kMGHostSpanSegNone;
|
||||||
|
m_lastElements.Blob.Offset = 0;
|
||||||
|
m_lastElements.Blob.Size = kAttribBytes + kBindingBytes;
|
||||||
|
MGPipeApplyCreateVertexElements(m_lastElements, m_blob.data());
|
||||||
|
++m_creates;
|
||||||
|
latch.Published = true;
|
||||||
|
latch.Gen = handle.Gen;
|
||||||
|
latch.ConfigVersion = configVersion;
|
||||||
|
// THE ONE PRODUCER of the record half: a create that reached the applier is the
|
||||||
|
// only thing that makes delete_vertex_elements a legal call for this handle.
|
||||||
|
latch.RecordLive = true;
|
||||||
|
latch.RecordGen = handle.Gen;
|
||||||
|
return sizeof(MGPVertexElements) + kAttribBytes + kBindingBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs> m_attributes{};
|
||||||
|
Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs> m_bindingPoints{};
|
||||||
|
Array<Uint8, kMGPipeMaxVertexAttribs *(sizeof(MGPVertexAttribWire) + sizeof(MGPVertexBindingPointWire))>
|
||||||
|
m_blob{};
|
||||||
|
Array<MGPVertexBuffer, kMGPipeMaxVertexAttribs> m_entries{};
|
||||||
|
|
||||||
|
MGPVertexElements m_lastElements{};
|
||||||
|
MGPVertexBuffers m_lastBuffers{};
|
||||||
|
MGPIndexBuffer m_lastIndex{};
|
||||||
|
|
||||||
|
Vector<Latch> m_latch;
|
||||||
|
MGPipeHandle m_boundHandle = kMGPipeNullHandle;
|
||||||
|
Uint64 m_boundLifetimeId = 0;
|
||||||
|
|
||||||
|
Uint64 m_creates = 0;
|
||||||
|
Uint64 m_binds = 0;
|
||||||
|
Uint64 m_bufferSets = 0;
|
||||||
|
Uint64 m_indexSets = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The monolith's one vertex-input emitter, beside the tracker, the CSO cache, the
|
||||||
|
// set-hash suppressor and the resource tracker.
|
||||||
|
inline MGPipeVertexInputEmitter& MGPipeVertexInputEmitterInstance() {
|
||||||
|
// NEVER DESTROYED, for MGPipeSlots()' reason (MG_Impl/Pipe/SlotAllocator.cpp), and
|
||||||
|
// this one is not hypothetical: C-1 put this emitter DIRECTLY on ~VertexArrayObject's
|
||||||
|
// path - MGPipeEmitVertexElementsDestroyAndFree asks RecordIsPublished(handle) and
|
||||||
|
// then NoteRecordDestroyed(handle), which read and WRITE m_latch. A destroyed
|
||||||
|
// emitter answers out of a freed Vector and the write grows it, i.e. an operator
|
||||||
|
// new + memcpy + operator delete on an already-freed block.
|
||||||
|
static MGPipeVertexInputEmitter* emitter = new MGPipeVertexInputEmitter();
|
||||||
|
return *emitter;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "P4aFinalFixPeek.h"
|
||||||
|
|
||||||
|
#if !defined(__ANDROID__)
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/MGPipeTypes.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Util/Metrics/PipeStats.h>
|
||||||
|
#define MGITEST_P4A_FINALFIX_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
#if defined(MGITEST_P4A_FINALFIX_PEEK_LIVE)
|
||||||
|
namespace {
|
||||||
|
namespace MGP = MobileGL::MG_Pipe;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||||
|
// Slot 0 is the reserved null slot; the walk is the same shape PipeApplyPeek.cpp's
|
||||||
|
// params reading takes. A GL name is never an identity on the wire, which is exactly
|
||||||
|
// why it is the right key for a harness that starts from the application's view.
|
||||||
|
for (MobileGL::SizeT slot = 1; slot < applier.TextureResources.size(); ++slot) {
|
||||||
|
const MGP::MGPipeResourceRecord& record = applier.TextureResources[slot];
|
||||||
|
if (!record.Live) continue;
|
||||||
|
if (record.Desc.GlNameForDiag != static_cast<MobileGL::Uint32>(glTextureName)) continue;
|
||||||
|
out->Slot = static_cast<unsigned>(slot);
|
||||||
|
out->Gen = static_cast<unsigned>(record.Gen);
|
||||||
|
out->Serial = static_cast<unsigned long long>(record.Serial);
|
||||||
|
out->BindMask = static_cast<unsigned>(record.Desc.BindMask);
|
||||||
|
out->ImageBindableHint = static_cast<unsigned>(record.Desc.ImageBindableHint);
|
||||||
|
out->Levels = static_cast<unsigned>(record.Desc.Levels);
|
||||||
|
out->PendingUploads = static_cast<unsigned>(record.PendingUploads.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
namespace Stats = MobileGL::MG_Util::PipeStats;
|
||||||
|
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
|
||||||
|
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureRemintPulls));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
namespace Stats = MobileGL::MG_Util::PipeStats;
|
||||||
|
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
|
||||||
|
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureUploadEmissions));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bool PeekPipeTextureResourceRecord(unsigned, PipeTextureResourceRecordPeek*) { return false; }
|
||||||
|
bool PeekPipeStatsTextureRemintPulls(unsigned long long*) { return false; }
|
||||||
|
bool PeekPipeStatsTextureUploadEmissions(unsigned long long*) { return false; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The white-box readings P4aFinalFixScenario.cpp takes, in a translation unit of their own for
|
||||||
|
// P4aSeamPeek.h's reason: a scenario TU includes the GL prototype headers and cannot include
|
||||||
|
// MG_Pipe/PipeApply.h or the Espryt managers beside them, and PipeApplyPeek.cpp is the gates
|
||||||
|
// package's file. Every entry point answers false where the reading cannot be taken (a pull
|
||||||
|
// build, Android, or an applier that holds no record for the name), and a false teaches the
|
||||||
|
// caller nothing - the case declines that half by name and keeps its public-GL verdict.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// The applier's resource record for a texture, found by its GL name (GlNameForDiag - a
|
||||||
|
// diagnostics-only field, which is exactly what a test harness is).
|
||||||
|
struct PipeTextureResourceRecordPeek {
|
||||||
|
unsigned Slot;
|
||||||
|
unsigned Gen;
|
||||||
|
unsigned long long Serial;
|
||||||
|
unsigned BindMask;
|
||||||
|
unsigned ImageBindableHint;
|
||||||
|
unsigned Levels;
|
||||||
|
unsigned PendingUploads;
|
||||||
|
};
|
||||||
|
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
|
||||||
|
|
||||||
|
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
|
||||||
|
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
|
||||||
|
// the first call, which is what lets a case read the number without a stats-enabled lane.
|
||||||
|
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
|
||||||
|
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
|
||||||
|
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
|
||||||
|
// empty either way. Arms the counters the same way.
|
||||||
|
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "P4aSeamPeek.h"
|
||||||
|
|
||||||
|
#if !defined(__ANDROID__)
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
|
#define MGITEST_P4A_SEAM_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
#if defined(MGITEST_P4A_SEAM_PEEK_LIVE)
|
||||||
|
namespace {
|
||||||
|
namespace MGP = MobileGL::MG_Pipe;
|
||||||
|
namespace MGB = MobileGL::MG_Backend::DirectGLES;
|
||||||
|
|
||||||
|
// "Is Espryt the backend running" - the same test PipeApplyPeek.cpp makes through a twin:
|
||||||
|
// on Magma no ES entry point was ever resolved and every member of g_GLESFuncs is null.
|
||||||
|
// It is asked BEFORE SamplerSubsystemEnabled(), which is Espryt's own latch and must not
|
||||||
|
// be resolved on a process whose backend is not Espryt.
|
||||||
|
bool EsprytIsRunning() { return MGB::g_GLESFuncs.glBindSampler != nullptr; }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive) {
|
||||||
|
if (outLive == nullptr) return false;
|
||||||
|
if (!EsprytIsRunning()) return false;
|
||||||
|
*outLive = MGB::SamplerSubsystemEnabled();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive) {
|
||||||
|
if (outLive == nullptr) return false;
|
||||||
|
if (!EsprytIsRunning()) return false;
|
||||||
|
*outLive = MGB::FramebufferSubsystemEnabled();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||||
|
out->Start = static_cast<unsigned>(applier.ShaderImageStart);
|
||||||
|
out->Count = static_cast<unsigned>(applier.ShaderImageCount);
|
||||||
|
out->Serial = static_cast<unsigned long long>(applier.ShaderImagesSerial);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
if (!EsprytIsRunning()) return false;
|
||||||
|
if (!MobileGL::MG_State::pGLContext) return false;
|
||||||
|
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||||
|
if (unit >= applier.BoundSamplerStates.size() || unit >= MGB::SamplerImpl::g_boundSamplersCache.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out = EsprytUnitSamplerPeek{};
|
||||||
|
|
||||||
|
// Espryt's own binding shadow: every glBindSampler this backend issues routes through it
|
||||||
|
// (BackendSamplerObject::Bind / UnbindSampler), so it IS what the driver holds.
|
||||||
|
if (MGB::SamplerImpl::BackendSamplerObject* const bound = MGB::SamplerImpl::g_boundSamplersCache[unit]) {
|
||||||
|
out->BoundSamplerId = static_cast<unsigned>(bound->GetBackendSamplerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
const MGP::MGPipeHandle cso = applier.BoundSamplerStates[unit];
|
||||||
|
out->CsoHandleSlot = static_cast<unsigned>(cso.Slot);
|
||||||
|
out->CsoHandleGen = static_cast<unsigned>(cso.Gen);
|
||||||
|
out->UnitInsideWindow = unit >= applier.SamplerStateStart &&
|
||||||
|
unit - applier.SamplerStateStart < applier.SamplerStateCount;
|
||||||
|
// The twin AT THE CSO HANDLE, asked of the same table Espryt asks (FindByHandle): a null
|
||||||
|
// here with a live handle is the F-4 shape - a content-addressed handle looked up in a
|
||||||
|
// table that only ever held identity-minted slots.
|
||||||
|
if (!MGP::MGPipeHandleIsNull(cso)) {
|
||||||
|
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.FindByHandle(cso); slot && *slot) {
|
||||||
|
out->CsoTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the twin keyed on the frontend OBJECT, which is what the pre-handle program pass
|
||||||
|
// used to mint and bind, so a scenario can say which of the two the driver holds.
|
||||||
|
const auto& object = MobileGL::MG_State::pGLContext->GetSamplerObject(
|
||||||
|
static_cast<MobileGL::Uint>(glSamplerName));
|
||||||
|
if (object) {
|
||||||
|
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.Find(object.get()); slot && *slot) {
|
||||||
|
out->IdentityTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bool PeekEsprytSamplerHandleArmIsLive(bool*) { return false; }
|
||||||
|
bool PeekEsprytFramebufferHandleArmIsLive(bool*) { return false; }
|
||||||
|
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek*) { return false; }
|
||||||
|
bool PeekEsprytUnitSampler(unsigned, unsigned, EsprytUnitSamplerPeek*) { return false; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The three readings P4aSeamAuditScenario.cpp takes from the inside, for the two seams the fable
|
||||||
|
// seam audit proved that PUBLIC GL CANNOT SEE: F-4 (the record arm's sampler bind is a permanent
|
||||||
|
// no-op, hidden by the pre-handle program pass binding the same values) and F-2 / SD-4 (the
|
||||||
|
// shader-image window does not follow a program switch, hidden by the server's window/high-water
|
||||||
|
// union taking the pre-handle bind for the units outside it). Both are correct pictures over a
|
||||||
|
// permanent silent fallback, which is precisely the class ROADMAP.md:20 says a gate has to be
|
||||||
|
// able to make red - and the only place the difference exists is inside.
|
||||||
|
//
|
||||||
|
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek.h's reason, verbatim: this file includes
|
||||||
|
// Espryt's own Managers.h, which may not meet a scenario's GL headers in one file. It is NOT
|
||||||
|
// PipeApplyPeek.cpp because that file is package F's (gates v3) and this round may not edit it.
|
||||||
|
//
|
||||||
|
// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK - a pull build, Android,
|
||||||
|
// a backend that is not Espryt - and a caller that gets false has learned NOTHING: "could not
|
||||||
|
// look" is not "was bound". The scenario declines the reading BY NAME and keeps its public-GL
|
||||||
|
// half, which is the shape TextureParamsWithoutASamplerViewScenario.cpp argues for.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// ---- is Espryt's sampler family on its HANDLE arm in this process? -------------------
|
||||||
|
//
|
||||||
|
// The gate for every other reading here. True only on DirectGLES, in a push build, with
|
||||||
|
// Espryt's own resolver answering "handle" for kMGPipeSubsystemSamplers (bit 11 set and its
|
||||||
|
// dependency satisfied) - i.e. exactly when bind_sampler_states / set_shader_images are
|
||||||
|
// consumed, so a white-box assertion about them can be red for its own reason and for no
|
||||||
|
// other. Written only on true.
|
||||||
|
bool PeekEsprytSamplerHandleArmIsLive(bool* outLive);
|
||||||
|
|
||||||
|
// The same question for the FRAMEBUFFER family (bit 9): true when Espryt consumes
|
||||||
|
// set_framebuffer_state in this process. The renderbuffer half of the F-3 case asserts only
|
||||||
|
// there - on the pre-handle arm a renderbuffer re-storaged while attached moves nothing the
|
||||||
|
// FBO memo reads (D-D2's documented hole, pre-P4a code), and the record is what closes it.
|
||||||
|
bool PeekEsprytFramebufferHandleArmIsLive(bool* outLive);
|
||||||
|
|
||||||
|
// ---- the applier's shader-image window, as last received ------------------------------
|
||||||
|
//
|
||||||
|
// MGPipeApplierState::ShaderImageStart / ShaderImageCount / ShaderImagesSerial. Count is
|
||||||
|
// "how many units set_shader_images last described" - 0 means the set has NEVER arrived
|
||||||
|
// (MGPipeApplierReset advances the serial whether or not anything was emitted, so the serial
|
||||||
|
// is not that test). Push build only.
|
||||||
|
struct PipeShaderImageWindowPeek {
|
||||||
|
unsigned Start;
|
||||||
|
unsigned Count;
|
||||||
|
unsigned long long Serial;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PeekPipeShaderImageWindow(PipeShaderImageWindowPeek* out);
|
||||||
|
|
||||||
|
// ---- which driver sampler a texture unit is bound to, and whose twin it is -------------
|
||||||
|
//
|
||||||
|
// For F-4. `BoundSamplerId` is the ES sampler name Espryt's own binding shadow says unit
|
||||||
|
// `unit` carries (0 = none). `CsoHandleSlot/Gen` is bind_sampler_states' handle for the unit,
|
||||||
|
// `CsoTwinSamplerId` the ES name of the twin Espryt holds AT THAT HANDLE (0 = no twin at the
|
||||||
|
// content-addressed slot - the F-4 shape), and `IdentityTwinSamplerId` the ES name of a twin
|
||||||
|
// keyed on the frontend SamplerObject named `glSamplerName` (0 = none). On a correct handle
|
||||||
|
// arm the unit's driver sampler IS the CSO twin. Push build, DirectGLES only.
|
||||||
|
struct EsprytUnitSamplerPeek {
|
||||||
|
unsigned BoundSamplerId;
|
||||||
|
unsigned CsoHandleSlot;
|
||||||
|
unsigned CsoHandleGen;
|
||||||
|
bool UnitInsideWindow;
|
||||||
|
unsigned CsoTwinSamplerId;
|
||||||
|
unsigned IdentityTwinSamplerId;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PeekEsprytUnitSampler(unsigned unit, unsigned glSamplerName, EsprytUnitSamplerPeek* out);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "PipeApplyPeek.h"
|
||||||
|
|
||||||
|
#if !defined(__ANDROID__)
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Pipe/MGPipeTypes.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
|
#define MGITEST_PIPE_APPLY_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
#if defined(MGITEST_PIPE_APPLY_PEEK_LIVE)
|
||||||
|
namespace {
|
||||||
|
namespace MGP = MobileGL::MG_Pipe;
|
||||||
|
namespace MGB = MobileGL::MG_Backend::DirectGLES;
|
||||||
|
|
||||||
|
// The frontend texture object a GL name denotes in the CURRENT context, or null. This is
|
||||||
|
// a LOOKUP KEY and nothing else: every value this file reports comes from the applier or
|
||||||
|
// from Espryt, never from the object found here. (Reading the frontend's own parameter
|
||||||
|
// state would answer the question the scenario is asking with the input to it.)
|
||||||
|
MobileGL::MG_State::GLState::ITextureObject* FrontendTexture(unsigned glTextureName) {
|
||||||
|
if (!MobileGL::MG_State::pGLContext) return nullptr;
|
||||||
|
const auto& object = MobileGL::MG_State::pGLContext->GetTextureObject(
|
||||||
|
static_cast<MobileGL::Uint>(glTextureName));
|
||||||
|
return object ? object.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Espryt's twin for that texture, or null - which is also this file's "is Espryt even the
|
||||||
|
// backend running" answer. On Magma no Espryt twin was ever built, so every entry point
|
||||||
|
// below stops here rather than reaching for g_GLESFuncs, whose members are null there.
|
||||||
|
MGB::TextureImpl::BackendTextureObject* EsprytTwin(unsigned glTextureName) {
|
||||||
|
MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName);
|
||||||
|
if (object == nullptr) return nullptr;
|
||||||
|
auto* const found = MGB::TextureImpl::g_backendTextureObjects.Find(object);
|
||||||
|
if (found == nullptr || !*found) return nullptr;
|
||||||
|
return found->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
int SwizzleToGLEnum(MobileGL::Uint8 encoded) {
|
||||||
|
return static_cast<int>(MobileGL::MG_Util::ConvertTextureSwizzleParamToGLEnum(
|
||||||
|
static_cast<MobileGL::TextureSwizzleParam>(encoded)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGPipeTypes.h owns the two numbers and says why depth is 0 (a zeroed record must decode
|
||||||
|
// to what an untouched texture already has). This is that decode, and nothing else in
|
||||||
|
// this module may open-code it.
|
||||||
|
int DepthStencilModeToGLEnum(MobileGL::Uint8 encoded) {
|
||||||
|
return encoded == MGP::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX
|
||||||
|
: GL_DEPTH_COMPONENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL_TEXTURE_BINDING_* query for a target, or 0 where this file has no answer. A
|
||||||
|
// guess would be worse than a refusal: the binding is what gets RESTORED, so a wrong
|
||||||
|
// pname would leave the driver bound to this test's texture.
|
||||||
|
int BindingQueryFor(unsigned glTarget) {
|
||||||
|
switch (glTarget) {
|
||||||
|
case GL_TEXTURE_2D: return GL_TEXTURE_BINDING_2D;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
|
||||||
|
// Slot 0 is the reserved null handle and is never live (MGPipeHandles.h), so the scan
|
||||||
|
// starts at 1 and a match at 0 is impossible rather than merely unlikely.
|
||||||
|
for (MobileGL::SizeT slot = 1; slot < applier.TextureResources.size(); ++slot) {
|
||||||
|
const MGP::MGPipeResourceRecord& record = applier.TextureResources[slot];
|
||||||
|
if (!record.Live) continue;
|
||||||
|
if (record.Desc.GlNameForDiag != static_cast<MobileGL::Uint32>(glTextureName)) continue;
|
||||||
|
out->Slot = static_cast<unsigned>(slot);
|
||||||
|
out->Gen = static_cast<unsigned>(record.Gen);
|
||||||
|
out->ParamsSerial = static_cast<unsigned long long>(record.ParamsSerial);
|
||||||
|
for (int channel = 0; channel < 4; ++channel) {
|
||||||
|
out->Swizzle[channel] = SwizzleToGLEnum(record.Params.Swizzle[channel]);
|
||||||
|
}
|
||||||
|
out->DepthStencilMode = DepthStencilModeToGLEnum(record.Params.DepthStencilMode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget,
|
||||||
|
EsprytAppliedTextureParamsPeek* out) {
|
||||||
|
if (out == nullptr) return false;
|
||||||
|
const int bindingQuery = BindingQueryFor(glTarget);
|
||||||
|
if (bindingQuery == 0) return false;
|
||||||
|
MGB::TextureImpl::BackendTextureObject* const twin = EsprytTwin(glTextureName);
|
||||||
|
if (twin == nullptr) return false;
|
||||||
|
const MobileGL::Uint backendId = twin->GetBackendTextureId();
|
||||||
|
if (backendId == 0) return false;
|
||||||
|
if (MGB::g_GLESFuncs.glGetTexParameteriv == nullptr ||
|
||||||
|
MGB::g_GLESFuncs.glBindTexture == nullptr || MGB::g_GLESFuncs.glGetIntegerv == nullptr ||
|
||||||
|
MGB::g_GLESFuncs.glGetError == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAVE / QUERY / RESTORE ON THE UNIT THAT IS ALREADY ACTIVE. No glActiveTexture, so the
|
||||||
|
// only driver state this touches is one unit's binding, and it is put back byte for byte
|
||||||
|
// - which is what keeps Espryt's own g_boundTexturesCache true rather than merely
|
||||||
|
// consistent. (Binding through the twin's own Bind() would update that shadow and would
|
||||||
|
// therefore CHANGE what the scenario measures next; this does not.)
|
||||||
|
GLint previousBinding = 0;
|
||||||
|
MGB::g_GLESFuncs.glGetIntegerv(static_cast<GLenum>(bindingQuery), &previousBinding);
|
||||||
|
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget), backendId);
|
||||||
|
|
||||||
|
out->BackendTextureId = static_cast<unsigned>(backendId);
|
||||||
|
static const GLenum kSwizzlePnames[4] = {GL_TEXTURE_SWIZZLE_R, GL_TEXTURE_SWIZZLE_G,
|
||||||
|
GL_TEXTURE_SWIZZLE_B, GL_TEXTURE_SWIZZLE_A};
|
||||||
|
for (int channel = 0; channel < 4; ++channel) {
|
||||||
|
GLint value = 0;
|
||||||
|
MGB::g_GLESFuncs.glGetTexParameteriv(static_cast<GLenum>(glTarget),
|
||||||
|
kSwizzlePnames[channel], &value);
|
||||||
|
out->Swizzle[channel] = static_cast<int>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The depth/stencil aspect mode is ES 3.1 and is INVALID_ENUM on a driver without it, so
|
||||||
|
// it is asked for last and its own error decides whether the answer is usable. The queue
|
||||||
|
// is drained first because a stale error from anywhere else would be indistinguishable
|
||||||
|
// from this call's - Espryt drains it the same way at every one of its own sync sites
|
||||||
|
// (DebugImpl::ErrorLopper), and this module's own GL errors are read from the FRONTEND
|
||||||
|
// state (ScenarioTest::FirstGLError), which none of this touches.
|
||||||
|
while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
GLint mode = 0;
|
||||||
|
MGB::g_GLESFuncs.glGetTexParameteriv(static_cast<GLenum>(glTarget),
|
||||||
|
GL_DEPTH_STENCIL_TEXTURE_MODE, &mode);
|
||||||
|
out->DepthStencilModeIsReadable = MGB::g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||||
|
out->DepthStencilMode = static_cast<int>(mode);
|
||||||
|
|
||||||
|
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget),
|
||||||
|
static_cast<GLuint>(previousBinding));
|
||||||
|
while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists) {
|
||||||
|
if (outExists == nullptr) return false;
|
||||||
|
MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName);
|
||||||
|
if (object == nullptr) return false;
|
||||||
|
// Espryt must be the backend running, or "no view" would be true of every texture on
|
||||||
|
// every other backend and the assertion would be vacuous where it is loudest.
|
||||||
|
if (EsprytTwin(glTextureName) == nullptr) return false;
|
||||||
|
// HandleOfSamplerViewForTexture is the monolith glue that derives the view's handle from
|
||||||
|
// the TEXTURE's lifetime id (D-F2: one view per ITextureObject), so this asks Espryt's
|
||||||
|
// own table the same way Espryt asks it - it does not consult the applier record's
|
||||||
|
// ViewCso, which is the client's statement about the same fact and would make one side
|
||||||
|
// of the seam vouch for the other.
|
||||||
|
const MGP::MGPipeHandle view = MGB::SamplerViewImpl::HandleOfSamplerViewForTexture(object);
|
||||||
|
if (MGP::MGPipeHandleIsNull(view)) {
|
||||||
|
*outExists = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
*outExists = MGB::SamplerViewImpl::FindSamplerViewForHandle(view) != nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount) {
|
||||||
|
if (outCount == nullptr) return false;
|
||||||
|
*outCount = static_cast<unsigned long long>(MGP::MGPipeApplier().RefusedNoConsumer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bool PeekPipeTextureParamsRecord(unsigned, PipeTextureParamsRecordPeek*) { return false; }
|
||||||
|
bool PeekEsprytAppliedTextureParams(unsigned, unsigned, EsprytAppliedTextureParamsPeek*) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool PeekEsprytHasSamplerViewForTexture(unsigned, bool*) { return false; }
|
||||||
|
bool PeekPipeApplierRefusedNoConsumer(unsigned long long*) { return false; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The APPLIER's texture-parameter record, ESPRYT's applied value for the same texture, and
|
||||||
|
// whether that texture has a sampler view yet. Three readings taken from a scenario, for gate
|
||||||
|
// G9's WHITE-BOX half.
|
||||||
|
//
|
||||||
|
// WHY A WHITE-BOX HALF EXISTS AT ALL (ID-19, brief section F, gates review R1). G9's public-GL
|
||||||
|
// cases in TextureParamsWithoutASamplerViewScenario.cpp catch "the parameter never reached the
|
||||||
|
// driver". They CANNOT catch "the parameter reached the driver LATE", because a texture
|
||||||
|
// parameter's only public-GL observable is a SAMPLE and the sample is itself what repairs an
|
||||||
|
// unsynced parameter: it puts the texture on the unit list, and that walk pushes the parameters
|
||||||
|
// for anything whose params serial moved. A backend that deferred every attachment-only
|
||||||
|
// texture's parameters to the first sampler view would be green on all four of those cases,
|
||||||
|
// forever, on every tree. The distinction only exists on the inside, so the reading has to be
|
||||||
|
// taken there - while the texture is still attachment-only, before any sample.
|
||||||
|
//
|
||||||
|
// A SEPARATE TRANSLATION UNIT for PipeSlotPeek.h's reason, verbatim: the scenario sources
|
||||||
|
// include the GL headers with prototypes and MobileGL's umbrella header is not meant to meet
|
||||||
|
// them in one file. This one goes further than PipeSlotPeek and includes Espryt's own
|
||||||
|
// Managers.h, which is exactly why it may not be anywhere near a scenario's GL headers.
|
||||||
|
//
|
||||||
|
// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK, and a caller that
|
||||||
|
// gets false has learned NOTHING - "could not look" is not "was applied". Out of reach means:
|
||||||
|
// a PULL build (there is no applier: it is `#if MOBILEGL_PIPE_PUSH`); Android, where this module
|
||||||
|
// links the shipping libMobileGL.so built -fvisibility=hidden and no internal symbol resolves;
|
||||||
|
// a backend that is not DirectGLES (Espryt is the subject; Magma answers the same GL question
|
||||||
|
// through P7's own paths); and, for the record peek, a mask whose texture-resource bit is off,
|
||||||
|
// where no record exists to find because nothing was ever emitted.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// ---- the applier's set_texture_params record for one GL texture name ------------------
|
||||||
|
//
|
||||||
|
// ADDRESSED BY GL NAME, and the search key is MGPResourceDesc::GlNameForDiag. That field is
|
||||||
|
// diagnostics-only by contract - never an identity, never a memo key (MGPipeTypes.h) - and
|
||||||
|
// this is a diagnostic: a test harness looking for the record a named GL object produced.
|
||||||
|
// The alternative would be to ask the CLIENT emitter for the texture's handle, and the
|
||||||
|
// review is explicit that this probe must arm on package D's applier/backend state and not
|
||||||
|
// on the emitter markers B and C set: they are different questions, and a shared marker
|
||||||
|
// would re-create the shape review F-M5 was raised about.
|
||||||
|
struct PipeTextureParamsRecordPeek {
|
||||||
|
// The handle the record sits at, so a caller can print it.
|
||||||
|
unsigned Slot;
|
||||||
|
unsigned Gen;
|
||||||
|
// set_texture_params' own serial. 0 means the record exists (the resource was created)
|
||||||
|
// but NO set_texture_params has ever been applied to it - which is a different finding
|
||||||
|
// from "no record", and the two must not be merged.
|
||||||
|
unsigned long long ParamsSerial;
|
||||||
|
// MGPTextureParams::Swizzle[4], translated to the GL enums the application passed to
|
||||||
|
// glTextureParameteri (GL_ZERO / GL_ONE / GL_RED / GL_GREEN / GL_BLUE / GL_ALPHA), so
|
||||||
|
// the scenario compares what it set against what the record carries in ONE vocabulary
|
||||||
|
// and neither side has to know the other's encoding.
|
||||||
|
int Swizzle[4];
|
||||||
|
// MGPTextureParams::DepthStencilMode, translated the same way: GL_DEPTH_COMPONENT or
|
||||||
|
// GL_STENCIL_INDEX.
|
||||||
|
int DepthStencilMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out);
|
||||||
|
|
||||||
|
// ---- Espryt's APPLIED value for the same texture --------------------------------------
|
||||||
|
//
|
||||||
|
// Read from the DRIVER, through the twin's own ES name, because "applied" means the driver
|
||||||
|
// was told - the same thing package D's white-box unit probe asserts against its mocked
|
||||||
|
// driver (esprytobj-v2 (9)). The current binding on the ACTIVE unit is saved and restored
|
||||||
|
// around the query and no unit is switched, so Espryt's binding shadow still describes
|
||||||
|
// reality afterwards: nothing is perturbed for it to be stale about.
|
||||||
|
//
|
||||||
|
// `glTarget` is the texture's GL target (only GL_TEXTURE_2D is supported today; any other
|
||||||
|
// target returns false rather than guessing a binding query).
|
||||||
|
struct EsprytAppliedTextureParamsPeek {
|
||||||
|
// The driver name Espryt minted for this texture, for the caller's message.
|
||||||
|
unsigned BackendTextureId;
|
||||||
|
int Swizzle[4];
|
||||||
|
int DepthStencilMode;
|
||||||
|
// False when the driver rejected the depth/stencil query - a non-depth texture, or an ES
|
||||||
|
// level without GL_DEPTH_STENCIL_TEXTURE_MODE. The swizzle half is still valid.
|
||||||
|
bool DepthStencilModeIsReadable;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget,
|
||||||
|
EsprytAppliedTextureParamsPeek* out);
|
||||||
|
|
||||||
|
// ---- and the claim that makes the two above mean anything ------------------------------
|
||||||
|
//
|
||||||
|
// Whether Espryt holds a SAMPLER VIEW twin for this texture. This is the assertion the
|
||||||
|
// public-GL cases cannot make, because making it there would create the view. `*outExists`
|
||||||
|
// is written only on true.
|
||||||
|
bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists);
|
||||||
|
|
||||||
|
// ---- c0f's belt, for the ObjectSubsystemControl arms -----------------------------------
|
||||||
|
//
|
||||||
|
// MGPipeApplierState::RefusedNoConsumer: the number of P4a-family entry points that were
|
||||||
|
// refused because no backend had registered MGPipeResourceOps. On a backend WITH a consumer
|
||||||
|
// it must never move; on one without (Magma, ID-39/ID-40) the client's own gate is supposed
|
||||||
|
// to stop the emission before the belt is reached, so it must never move there either. A
|
||||||
|
// non-zero delta says the gate and the belt disagreed, which is the whole point of having
|
||||||
|
// both. Reset by MGPipeApplierReset, so a caller reads it as a DELTA and treats a value that
|
||||||
|
// went DOWN as "the applier was reset, count everything since as `after`".
|
||||||
|
bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "PipeSlotPeek.h"
|
||||||
|
|
||||||
|
#if !defined(__ANDROID__)
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#define MGITEST_PIPE_SLOT_PEEK_LIVE 1
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
#if defined(MGITEST_PIPE_SLOT_PEEK_LIVE)
|
||||||
|
namespace {
|
||||||
|
// One arm per member, and NO `default:` on purpose: adding a PipeSlotKind without
|
||||||
|
// deciding which MGPipeKind it names is a compiler warning here (-Wswitch) rather than
|
||||||
|
// a row that silently counts VertexElementsCso and reports "did not leak" about a kind
|
||||||
|
// it never looked at. The trailing return is the unreachable one the compiler needs.
|
||||||
|
MobileGL::MG_Pipe::MGPipeKind Translate(PipeSlotKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case PipeSlotKind::Buffer: return MobileGL::MG_Pipe::MGPipeKind::Buffer;
|
||||||
|
case PipeSlotKind::VertexElementsCso:
|
||||||
|
return MobileGL::MG_Pipe::MGPipeKind::VertexElementsCso;
|
||||||
|
case PipeSlotKind::Texture: return MobileGL::MG_Pipe::MGPipeKind::Texture;
|
||||||
|
case PipeSlotKind::Renderbuffer: return MobileGL::MG_Pipe::MGPipeKind::Renderbuffer;
|
||||||
|
case PipeSlotKind::Framebuffer: return MobileGL::MG_Pipe::MGPipeKind::Framebuffer;
|
||||||
|
case PipeSlotKind::SamplerCso: return MobileGL::MG_Pipe::MGPipeKind::SamplerCso;
|
||||||
|
case PipeSlotKind::SamplerViewCso:
|
||||||
|
return MobileGL::MG_Pipe::MGPipeKind::SamplerViewCso;
|
||||||
|
case PipeSlotKind::ShaderCso: return MobileGL::MG_Pipe::MGPipeKind::ShaderCso;
|
||||||
|
}
|
||||||
|
return MobileGL::MG_Pipe::MGPipeKind::None;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PeekPipeSlotLiveCount(PipeSlotKind kind, unsigned* outLive) {
|
||||||
|
if (outLive == nullptr) return false;
|
||||||
|
*outLive = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().LiveCount(Translate(kind)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeSlotHighWater(PipeSlotKind kind, unsigned* outHighWater) {
|
||||||
|
if (outHighWater == nullptr) return false;
|
||||||
|
// The ORDINARY space only, for every kind including ShaderCso (contract-v2.md 4.3).
|
||||||
|
*outHighWater = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().HighWater(Translate(kind)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeCompositeSlotLiveCount(unsigned* outLive) {
|
||||||
|
if (outLive == nullptr) return false;
|
||||||
|
*outLive = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().CompositeLiveCount());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeCompositeSlotHighWater(unsigned* outHighWater) {
|
||||||
|
if (outHighWater == nullptr) return false;
|
||||||
|
*outHighWater = static_cast<unsigned>(MobileGL::MG_Pipe::MGPipeSlots().CompositeHighWater());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeekPipeCompositeSlotBandBase(unsigned* outBandBase) {
|
||||||
|
if (outBandBase == nullptr) return false;
|
||||||
|
*outBandBase = static_cast<unsigned>(MobileGL::MG_Pipe::kMGPipeShaderCsoCompositeSlotBase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
bool PeekPipeSlotLiveCount(PipeSlotKind, unsigned*) { return false; }
|
||||||
|
bool PeekPipeSlotHighWater(PipeSlotKind, unsigned*) { return false; }
|
||||||
|
bool PeekPipeCompositeSlotLiveCount(unsigned*) { return false; }
|
||||||
|
bool PeekPipeCompositeSlotHighWater(unsigned*) { return false; }
|
||||||
|
bool PeekPipeCompositeSlotBandBase(unsigned*) { return false; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The CLIENT slot allocator's occupancy, read from a scenario.
|
||||||
|
//
|
||||||
|
// It exists for one assertion, P3a's C-1: a frontend object that dies must return its
|
||||||
|
// MGPipeHandle slot WHATEVER BACKEND IS RUNNING. That question has no answer in the GL API -
|
||||||
|
// the leak it rules out is entirely inside the library, and it is invisible in pixels, in GL
|
||||||
|
// names and in `glGetError` - so the only honest observable is the allocator's own live count
|
||||||
|
// and high-water mark. Reading them is what makes the case fail on the backend it actually
|
||||||
|
// failed on (DirectVulkan, which installs no StateObjectDeathOps) rather than only on the one
|
||||||
|
// where a backend-owned free happened to exist.
|
||||||
|
//
|
||||||
|
// A separate translation unit for BackendCapsPeek.h's reason, verbatim: the scenario sources
|
||||||
|
// include the GL headers with prototypes and MobileGL's umbrella header is not meant to meet
|
||||||
|
// them in one file.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// Which client-side object kind to ask about. Mirrors MG_Pipe::MGPipeKind for exactly the
|
||||||
|
// kinds a scenario has a reason to count, so that the enum does not travel through this
|
||||||
|
// header and the GL headers together.
|
||||||
|
enum class PipeSlotKind {
|
||||||
|
Buffer,
|
||||||
|
VertexElementsCso,
|
||||||
|
// P4a's six (G8b). Every one of them is a kind the CLIENT mints and the client alone
|
||||||
|
// frees (BRIEF-P4A.md D-I1: one death helper per kind, called from the frontend
|
||||||
|
// object's own destructor, whatever backend is running), so every one of them can leak
|
||||||
|
// the P3a C-1 way - and the leak is invisible in pixels, in GL names and in
|
||||||
|
// glGetError, exactly as the VertexElementsCso one was.
|
||||||
|
Texture,
|
||||||
|
Renderbuffer,
|
||||||
|
// Framebuffer has a HANDLE but no wire lifetime (D-I2): no create_*, no destroy row in
|
||||||
|
// the catalogue, and its death helper does the notice and the free and emits nothing.
|
||||||
|
// That makes the allocator the ONLY observable of its lifetime, so this row matters
|
||||||
|
// more here than the others rather than less.
|
||||||
|
Framebuffer,
|
||||||
|
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
|
||||||
|
// that is what the allocator has: the band is a second dense table inside the same kind
|
||||||
|
// and LiveCount counts both.
|
||||||
|
//
|
||||||
|
// THE TWO SPACES' HIGH-WATER MARKS ARE NOT ONE NUMBER, and the correction matters here
|
||||||
|
// more than anywhere else. c0b split them (contract-v2.md 4.3): HighWater(ShaderCso) is
|
||||||
|
// now the ORDINARY space only and the band's own mark is CompositeHighWater(), because
|
||||||
|
// a merged mark is pinned at ~983k from the first composite mint onward and every "the
|
||||||
|
// high-water mark did not move over N churn rounds" assertion about ordinary programs
|
||||||
|
// would be vacuously true for the rest of the process. The composite's leak case is a
|
||||||
|
// separate CASE and reads the BAND'S OWN counters below (PeekPipeCompositeSlot*) - a
|
||||||
|
// composite's slot has TWO independent release paths (the pipeline cache's LRU eviction
|
||||||
|
// and the composite ProgramObject's destructor), and a slot that never comes back to
|
||||||
|
// the band moves neither of the ordinary numbers.
|
||||||
|
ShaderCso,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Live slots of this kind right now, and one past the highest slot ever handed out.
|
||||||
|
// Both return false, touching nothing, where the allocator is out of reach: in a PULL
|
||||||
|
// build there is no allocator at all (it is `#if MOBILEGL_PIPE_PUSH`), and on Android this
|
||||||
|
// module links the shipping libMobileGL.so built -fvisibility=hidden, so no internal symbol
|
||||||
|
// resolves. A caller that gets false must SKIP rather than pass - "could not look" is not
|
||||||
|
// "did not leak".
|
||||||
|
bool PeekPipeSlotLiveCount(PipeSlotKind kind, unsigned* outLive);
|
||||||
|
bool PeekPipeSlotHighWater(PipeSlotKind kind, unsigned* outHighWater);
|
||||||
|
|
||||||
|
// The ShaderCso COMPOSITE BAND's own three numbers, the seventh..ninth members
|
||||||
|
// contract-v2.md 4.3 asks this header for. There is no `kind` argument because the band is
|
||||||
|
// ShaderCso's alone - AllocateComposite is the one door into it and no other kind has one.
|
||||||
|
// All three return false on the same terms as the two above, and a caller that gets false
|
||||||
|
// must SKIP.
|
||||||
|
//
|
||||||
|
// PeekPipeCompositeSlotLiveCount = MGPipeSlotAllocator::CompositeLiveCount(), the band's
|
||||||
|
// share of LiveCount(ShaderCso).
|
||||||
|
// PeekPipeCompositeSlotHighWater = CompositeHighWater() VERBATIM, i.e. one past the
|
||||||
|
// highest band slot ever handed out. It is an ABSOLUTE
|
||||||
|
// slot number and therefore starts at the band's base,
|
||||||
|
// not at zero - "no composite was ever minted" reads as
|
||||||
|
// `high water == band base`, which is what the third
|
||||||
|
// member is for. It is not returned base-relative
|
||||||
|
// because a peek whose name says HighWater and whose
|
||||||
|
// value is a delta is exactly the kind of quietly
|
||||||
|
// redefined counter this member exists to correct.
|
||||||
|
// PeekPipeCompositeSlotBandBase = kMGPipeShaderCsoCompositeSlotBase, the floor the
|
||||||
|
// other two are read against. A constant, but it
|
||||||
|
// reaches a scenario only through this header: the
|
||||||
|
// MG_Pipe headers and the GL headers are not meant to
|
||||||
|
// meet in one translation unit, which is why this
|
||||||
|
// harness exists at all.
|
||||||
|
bool PeekPipeCompositeSlotLiveCount(unsigned* outLive);
|
||||||
|
bool PeekPipeCompositeSlotHighWater(unsigned* outHighWater);
|
||||||
|
bool PeekPipeCompositeSlotBandBase(unsigned* outBandBase);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Reading ONE PipeStats summary window out of the library's own log, for the scenarios whose
|
||||||
|
// claim is about a counter rather than about pixels.
|
||||||
|
//
|
||||||
|
// WHY THROUGH A LOG FILE AT ALL. MG_Util::PipeStats is internal to the library and this module
|
||||||
|
// cannot link against it (ScenarioFixture.h has the long version: on Android this binary links
|
||||||
|
// the SHIPPING libMobileGL.so, built -fvisibility=hidden). The library's `MGPipe stats:` line is
|
||||||
|
// the only channel, so a lane that wants to read a counter sets MOBILEGL_PIPE_STATS=1,
|
||||||
|
// MOBILEGL_PIPE_STATS_PERIOD=1 - one line per eglSwapBuffers - and a MOBILEGL_LOG_FILE_PATH of
|
||||||
|
// its OWN.
|
||||||
|
//
|
||||||
|
// THE LOG PATH HAS TO BE PRIVATE TO ONE CTEST ENTRY, and that is not a style rule: the library
|
||||||
|
// opens it fopen(path, "w"), so every process launched in a lane TRUNCATES it. Two entries of one
|
||||||
|
// lane reading the same path race under `ctest -j`, and the shape of the failure is an empty read
|
||||||
|
// that looks exactly like "the counter was never emitted". So a case that reads a window gets a
|
||||||
|
// ctest entry whose TEST_FILTER selects that case alone, with a log path nothing else writes -
|
||||||
|
// the rule PipeVerifyArmingScenario and CsoContentAddressingScenario already follow.
|
||||||
|
//
|
||||||
|
// THE WINDOW IS "SINCE THE PREVIOUS LINE" (PipeStats::FormatWindowLine), so the caller closes the
|
||||||
|
// setup window with a swap, runs the workload, swaps again, and reads the LAST line - which then
|
||||||
|
// covers the workload and nothing else.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace MGITest::PipeStatsWindow {
|
||||||
|
|
||||||
|
// The lane's private log path, or empty when the lane configured none.
|
||||||
|
inline std::string LibraryLogPath() {
|
||||||
|
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||||
|
return (path != nullptr && *path != '\0') ? std::string(path) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string ReadWholeFile(const std::string& path) {
|
||||||
|
if (path.empty()) return {};
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file.good()) return {};
|
||||||
|
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last summary line in the log, verbatim. `found` is false when the library never emitted
|
||||||
|
// one, which is a different failure from "the counter read zero" and has to be reported as
|
||||||
|
// one: it means the stats channel never reached the process, not that the workload did
|
||||||
|
// nothing.
|
||||||
|
struct Window {
|
||||||
|
bool found = false;
|
||||||
|
std::string line;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline Window Last(const std::string& log) {
|
||||||
|
Window window;
|
||||||
|
const std::string marker = "MGPipe stats:";
|
||||||
|
const std::size_t at = log.rfind(marker);
|
||||||
|
if (at == std::string::npos) return window;
|
||||||
|
const std::size_t end = log.find('\n', at);
|
||||||
|
window.line = log.substr(at, end == std::string::npos ? std::string::npos : end - at);
|
||||||
|
window.found = true;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Window LastFromLaneLog() { return Last(ReadWholeFile(LibraryLogPath())); }
|
||||||
|
|
||||||
|
// One counter out of that line, by its short name ("mpr", "draws", "csom"), or -1 when the
|
||||||
|
// line does not carry it. The search includes the SEPARATOR before the name and the `=` after
|
||||||
|
// it, so "draws" cannot match "draws/f=" and "mpr" cannot match a longer name ending in it -
|
||||||
|
// a substring match here would read a neighbouring counter's value and report it as this
|
||||||
|
// one's, which is the one way a counter assertion can be wrong without ever failing.
|
||||||
|
inline long long CounterOrAbsent(const Window& window, const char* shortName) {
|
||||||
|
if (!window.found) return -1;
|
||||||
|
// A counter is preceded either by a space (` mpr=`, ` draws=`) or by its bracket's
|
||||||
|
// opening (`cso[csom=`, `bytes/f[stage-buffer=`); nothing in the line is preceded by
|
||||||
|
// anything else.
|
||||||
|
for (const char* prefix : {" ", "["}) {
|
||||||
|
const std::string key = std::string(prefix) + shortName + "=";
|
||||||
|
const std::size_t at = window.line.find(key);
|
||||||
|
if (at == std::string::npos) continue;
|
||||||
|
return std::strtoll(window.line.c_str() + at + key.size(), nullptr, 10);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest::PipeStatsWindow
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CsoContentAddressingScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE CSO CONTENT-ADDRESSING NEGATIVE CONTROL (gate G12).
|
||||||
|
//
|
||||||
|
// P2's render-state CSO is content-addressed: the client hashes the 396 pipeline bytes, probes a
|
||||||
|
// 64-entry cache, memcmps a hash hit and reuses the handle. The whole design is measured against
|
||||||
|
// a knob that turns that off - kMGPipeBehaviourNoCsoContentAddressing, bit 63 of the runtime
|
||||||
|
// MOBILEGL_PIPE_PUSH bitmask - so that "push is slower" can be told apart from "the CSO design is
|
||||||
|
// slower" (P2 brief D.4.5). A measurement knob has one characteristic failure mode: it stops
|
||||||
|
// steering anything and every later number is quietly taken against a switch that does nothing.
|
||||||
|
// This file is the entry that cannot let that happen.
|
||||||
|
//
|
||||||
|
// WHAT IT ASSERTS, per arm, and why those are the right shapes:
|
||||||
|
//
|
||||||
|
// content-addressed (MOBILEGL_PIPE_PUSH=0x7f)
|
||||||
|
// A Blaze3D blend toggle - enable / draw / disable / draw, N times, which is the workload
|
||||||
|
// the CsoCache exists for (ARCHITECTURE.md 5.1: the push happens at validate rather than in
|
||||||
|
// the setter precisely because Blaze3D brackets every batch this way) - visits exactly TWO
|
||||||
|
// distinct pipeline subsets. So the mint count must stay small and BOUNDED while the bind
|
||||||
|
// count grows with the draws: csom << csob.
|
||||||
|
//
|
||||||
|
// no content addressing (MOBILEGL_PIPE_PUSH=0x800000000000007f)
|
||||||
|
// Every pipeline-version change mints a fresh CSO and the map is never probed, so mint and
|
||||||
|
// bind must move together: csom == csob. This is the assertion a dead switch fails - with
|
||||||
|
// the bit ignored, this arm would report csom << csob just like the other one.
|
||||||
|
//
|
||||||
|
// both arms
|
||||||
|
// The PIXELS must not move. The quad is drawn with alpha 1.0 through
|
||||||
|
// GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA, so the blended and unblended draws produce the
|
||||||
|
// same colour by construction and the readback is the same image in both arms and after
|
||||||
|
// every toggle. "The counters moved and the picture did not" is the whole claim.
|
||||||
|
//
|
||||||
|
// HOW THE COUNTERS ARE READ. MG_Util::PipeStats is internal to the library and this module cannot
|
||||||
|
// link against it (ScenarioFixture.h explains why: on Android this binary links the SHIPPING
|
||||||
|
// libMobileGL.so, built -fvisibility=hidden). The library's own summary line is the only channel,
|
||||||
|
// so each lane sets MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 - one line per
|
||||||
|
// eglSwapBuffers - and a MOBILEGL_LOG_FILE_PATH of its OWN. The log path has to be private: the
|
||||||
|
// library opens it fopen(path, "w"), so every process in a lane truncates it, and a whole-file
|
||||||
|
// read in a shared lane races a neighbour's bring-up. That is the same rule, and the same
|
||||||
|
// remedy, as PipeVerifyArmingScenario's arming lane.
|
||||||
|
//
|
||||||
|
// The window a summary line reports is "since the previous line" (PipeStats::FormatWindowLine), so
|
||||||
|
// the workload runs inside ONE frame: a swap before it closes the setup window, and the swap after
|
||||||
|
// it emits a line whose csom / csob cover the toggle loop and nothing else.
|
||||||
|
//
|
||||||
|
// WHY IT CAN SKIP. The counters are minted by the client-side tracker (P2 package B), and this
|
||||||
|
// file is written against the P2 contract commit, before that package lands. Until the tracker
|
||||||
|
// exists there is no CSO to mint, csom is structurally 0 and an assertion about its ratio to csob
|
||||||
|
// would be a statement about nothing. The build answers the question rather than a hand-maintained
|
||||||
|
// list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/ for the two
|
||||||
|
// counters' names and passes the answer in as MGITEST_PIPE_TRACKER_PRESENT, with a
|
||||||
|
// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale.
|
||||||
|
// It is a CONTENT probe, not a filename probe, precisely so that the owning package keeps control
|
||||||
|
// of its own file layout - it implements the tracker and the cache header-only today, and a glob
|
||||||
|
// for `Tracker.cpp` would have kept this control skipping forever after that package landed, with
|
||||||
|
// a reason that had become false. When an emitter lands the arms arm themselves; until then the
|
||||||
|
// entries are registered, visible and SKIPPED with the reason - never absent, and never green for
|
||||||
|
// having asserted nothing.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set by the two CsoContentAddressing. ctest entries and by nothing else; a harness
|
||||||
|
// marker, never read by the library. Its absence means an ambient entry, where neither
|
||||||
|
// the stats channel nor a private log path is configured.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_CSO_LANE";
|
||||||
|
constexpr const char* kLaneContentAddressed = "content-addressed";
|
||||||
|
constexpr const char* kLaneNoContentAddressing = "no-content-addressing";
|
||||||
|
|
||||||
|
// Toggle pairs per frame. 8 is small enough to keep the frame cheap and large enough that
|
||||||
|
// "mints stay bounded" and "mints track binds" are different numbers by a wide margin.
|
||||||
|
constexpr int kTogglePairs = 8;
|
||||||
|
constexpr int kDrawsPerFrame = kTogglePairs * 2;
|
||||||
|
// The blend toggle visits two distinct pipeline subsets, so two CSOs. The bound is
|
||||||
|
// deliberately a little looser than 2: a future chunk-table change could legitimately
|
||||||
|
// split one of them, and the claim being pinned here is "bounded, not per-draw".
|
||||||
|
constexpr long long kMaxDistinctCsos = 4;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LaneName() {
|
||||||
|
const char* lane = std::getenv(kLaneMarker);
|
||||||
|
return lane != nullptr ? std::string(lane) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LibraryLogPath() {
|
||||||
|
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||||
|
return (path != nullptr && *path != '\0') ? std::string(path) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ReadWholeFile(const std::string& path) {
|
||||||
|
if (path.empty()) return {};
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file.good()) return {};
|
||||||
|
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// One window's CSO counters, as the library printed them.
|
||||||
|
struct CsoWindow {
|
||||||
|
bool found = false;
|
||||||
|
long long mints = -1;
|
||||||
|
long long binds = -1;
|
||||||
|
std::string line;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses `... cso[csom=<N> csob=<M>] ...` out of the LAST "MGPipe stats:" line in the log.
|
||||||
|
// The last line, because the window a line reports is "since the previous line" and the
|
||||||
|
// caller closes the setup window with a swap before the workload.
|
||||||
|
CsoWindow LastCsoWindow(const std::string& log) {
|
||||||
|
CsoWindow window;
|
||||||
|
const std::string marker = "MGPipe stats:";
|
||||||
|
std::size_t at = log.rfind(marker);
|
||||||
|
if (at == std::string::npos) return window;
|
||||||
|
const std::size_t end = log.find('\n', at);
|
||||||
|
window.line = log.substr(at, end == std::string::npos ? std::string::npos : end - at);
|
||||||
|
|
||||||
|
const std::string mintKey = "csom=";
|
||||||
|
const std::string bindKey = "csob=";
|
||||||
|
const std::size_t mintAt = window.line.find(mintKey);
|
||||||
|
const std::size_t bindAt = window.line.find(bindKey);
|
||||||
|
if (mintAt == std::string::npos || bindAt == std::string::npos) return window;
|
||||||
|
window.mints = std::strtoll(window.line.c_str() + mintAt + mintKey.size(), nullptr, 10);
|
||||||
|
window.binds = std::strtoll(window.line.c_str() + bindAt + bindKey.size(), nullptr, 10);
|
||||||
|
window.found = true;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CsoContentAddressingScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
m_lane = LaneName();
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
const float quad[12] = {-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f};
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||||
|
RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||||
|
// through a return value; every caller pairs it with `if (IsSkipped()) return;`.
|
||||||
|
void SkipUnlessTheLaneIsAssertableHere() {
|
||||||
|
if (m_lane.empty()) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the two CsoContentAddressing. ctest entries set "
|
||||||
|
"MGITEST_CSO_LANE together with the MOBILEGL_PIPE_PUSH bitmask, "
|
||||||
|
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH. None of that is configured in the ambient "
|
||||||
|
"entries, and the ambient log is shared, so a read here would race.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH, so there is no "
|
||||||
|
"render-state CSO to mint, no cso[] bracket in the summary line and "
|
||||||
|
"nothing for the content-addressing bit to steer. The entry is "
|
||||||
|
"registered here anyway so that `ctest -L integration-gpu` names the "
|
||||||
|
"same tests in the pull build and the push build (gate G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_TRACKER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "the CSO counters have no emitter in this build: no source under "
|
||||||
|
"MobileGL/MG_Impl/Pipe/ names RenderStateCsoMints or "
|
||||||
|
"RenderStateCsoBinds, so nothing mints or binds a render-state CSO "
|
||||||
|
"and csom / csob are structurally zero. P2 package B owns the tracker "
|
||||||
|
"and the CSO cache; this entry arms itself when they land, whatever "
|
||||||
|
"files that package chooses to put them in.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's summary "
|
||||||
|
"line is the only channel this module has for reading PipeStats";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable / draw / disable / draw, kTogglePairs times, entirely inside one frame.
|
||||||
|
// Returns the readback taken at the end of that frame, before the swap.
|
||||||
|
Image RunBlendToggleFrame() {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
|
||||||
|
for (int i = 0; i < kTogglePairs; ++i) {
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_lane;
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_vbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ONE case per lane, and that is a hard constraint rather than a style choice.
|
||||||
|
//
|
||||||
|
// This case READS the library log, and the log is a per-LANE resource: the library opens it
|
||||||
|
// fopen(path, "w"), so every process in a lane truncates it. A second case in this lane would
|
||||||
|
// therefore race this one under `ctest -j`, and the shape of the failure is a silent, empty
|
||||||
|
// read that looks exactly like "the counters were never emitted". Splitting the plumbing
|
||||||
|
// assertion into its own case would have bought a clearer failure message and paid for it
|
||||||
|
// with a flake in the thing the message is about. The plumbing is asserted first, with its
|
||||||
|
// own message, inside this one process instead.
|
||||||
|
TEST_F(CsoContentAddressingScenario, TheBlendToggleMintsBoundedlyWithContentAddressingAndPerBindWithout) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessTheLaneIsAssertableHere();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
Gl().EndFrame(); // close the setup window
|
||||||
|
const Image first = RunBlendToggleFrame();
|
||||||
|
const CsoWindow window = LastCsoWindow(ReadWholeFile(LibraryLogPath()));
|
||||||
|
// The plumbing first, with its own message, so a counter-ratio failure below can never
|
||||||
|
// be confused with "the lane never turned the stats channel on".
|
||||||
|
ASSERT_TRUE(window.found)
|
||||||
|
<< "no 'MGPipe stats:' line carrying cso[csom= csob=] in " << LibraryLogPath()
|
||||||
|
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting "
|
||||||
|
"here) and the cso[] bracket is unconditional inside that #if, so it cannot be "
|
||||||
|
"missing for a build reason: either MOBILEGL_PIPE_STATS / "
|
||||||
|
"MOBILEGL_PIPE_STATS_PERIOD did not reach the process, or no summary line was "
|
||||||
|
"emitted at all because nothing reached PipeStats::OnPresent.";
|
||||||
|
RecordProperty("cso_line", window.line.c_str());
|
||||||
|
|
||||||
|
// Every draw in the frame changed the pipeline subset, so every draw is a bind. This
|
||||||
|
// is the denominator both arms are read against; without it, "csom == csob" would also
|
||||||
|
// be satisfied by a frame in which neither happened at all.
|
||||||
|
ASSERT_GE(window.binds, static_cast<long long>(kDrawsPerFrame))
|
||||||
|
<< "the toggle frame issued " << kDrawsPerFrame
|
||||||
|
<< " draws whose pipeline subset alternates, so it must have issued at least that many "
|
||||||
|
"render-state binds. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
|
||||||
|
if (m_lane == kLaneContentAddressed) {
|
||||||
|
EXPECT_LE(window.mints, kMaxDistinctCsos)
|
||||||
|
<< "with content addressing on, enable/draw/disable/draw x " << kTogglePairs
|
||||||
|
<< " visits two distinct pipeline subsets and must mint a bounded number of CSOs, then "
|
||||||
|
"reuse them. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
EXPECT_LT(window.mints, window.binds)
|
||||||
|
<< "with content addressing on the cache must be answering binds it did not mint. "
|
||||||
|
<< window.line;
|
||||||
|
} else if (m_lane == kLaneNoContentAddressing) {
|
||||||
|
EXPECT_EQ(window.mints, window.binds)
|
||||||
|
<< "kMGPipeBehaviourNoCsoContentAddressing (bit 63 of MOBILEGL_PIPE_PUSH) must make every "
|
||||||
|
"bind mint a fresh CSO - the map is never probed and no handle is ever reused. Equal "
|
||||||
|
"counters are the only reading that proves the bit STEERED anything: if it were "
|
||||||
|
"ignored, this arm would report the same bounded mint count as the other one. It "
|
||||||
|
"reported: "
|
||||||
|
<< window.line;
|
||||||
|
} else {
|
||||||
|
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane << "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and the picture is the same in both arms and after every toggle. The quad is
|
||||||
|
// opaque, so the blended and unblended draws agree by construction.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(first, kInset, first.Width() - kInset, kInset, first.Height() - kInset,
|
||||||
|
"green", 0.0, "the blend-toggle frame [" + m_lane + "]"));
|
||||||
|
const Image second = RunBlendToggleFrame();
|
||||||
|
EXPECT_TRUE(second == first)
|
||||||
|
<< "the second toggle frame does not match the first: " << second.ByteDiffCount(first)
|
||||||
|
<< " bytes differ. The CSO path must not change what is drawn.";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -25,13 +25,31 @@
|
|||||||
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
|
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
|
||||||
// * a compute-shader write through an SSBO binding of the same arena is read
|
// * a compute-shader write through an SSBO binding of the same arena is read
|
||||||
// back - the GPU-written path for adopted stores (glFinish + direct read).
|
// back - the GPU-written path for adopted stores (glFinish + direct read).
|
||||||
|
//
|
||||||
|
// P3a (gate G10, G12) adds a fourth case and two more lanes, and neither of them
|
||||||
|
// changes what the three above assert:
|
||||||
|
//
|
||||||
|
// * AnAdoptionCostsExactlyOneMapPersistentRoundtrip counts the acquisition.
|
||||||
|
// ARCHITECTURE.md:474 prices the adopted store at one round trip per STORAGE
|
||||||
|
// DEFINITION; `map-persistent-roundtrips` counts every map_persistent
|
||||||
|
// emission, mint or decline (D-B2), so one definition plus a frame of draws
|
||||||
|
// must publish exactly one. It reads the library's summary line, so it needs
|
||||||
|
// a lane with the stats channel and a private log path, and it SKIPS - with
|
||||||
|
// the reason - anywhere else and on any tree that does not emit the counter.
|
||||||
|
// * the three original cases are registered TWICE MORE, with P3a's resource and
|
||||||
|
// vertex-input subsystem bits set and cleared, because this file is where an
|
||||||
|
// adopted store's whole life is exercised: definition, in-flight SubData,
|
||||||
|
// readback and a GPU write. If the handle path and the legacy BufferBackendOps
|
||||||
|
// path disagree about any of it, one of the two arms goes red here.
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
#include "../Harness/ScenarioFixture.h"
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
#ifdef GLAPI
|
#ifdef GLAPI
|
||||||
@@ -73,6 +91,19 @@ layout(std430, binding = 0) buffer Arena { uint word; };
|
|||||||
void main() { word = 0xC0FFEEu; }
|
void main() { word = 0xC0FFEEu; }
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
|
||||||
|
// never read by the library.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
|
||||||
|
// Draws issued against the arena inside the counted window. One definition, many draws:
|
||||||
|
// "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
|
||||||
|
// numbers or the assertion cannot tell them apart.
|
||||||
|
constexpr int kDrawsInTheWindow = 5;
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
float x, y;
|
float x, y;
|
||||||
float r, g, b;
|
float r, g, b;
|
||||||
@@ -102,6 +133,12 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
// The NULL-data definition is the adoption point (and Minecraft's
|
// The NULL-data definition is the adoption point (and Minecraft's
|
||||||
// arena-creation idiom).
|
// arena-creation idiom).
|
||||||
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
ConfigureVertexArray(m_vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigureVertexArray(GLuint vao) {
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
reinterpret_cast<void*>(kVertexOffset));
|
reinterpret_cast<void*>(kVertexOffset));
|
||||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
@@ -173,15 +210,49 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawQuad() {
|
void DrawQuad(GLuint vao = 0) {
|
||||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
glUseProgram(m_program);
|
glUseProgram(m_program);
|
||||||
glBindVertexArray(m_vao);
|
glBindVertexArray(vao != 0 ? vao : m_vao);
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||||
|
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
|
||||||
|
void SkipUnlessTheRoundtripCounterIsReadableHere() {
|
||||||
|
if (std::getenv(kLaneMarker) == nullptr) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the MapPersistentRoundtrips. ctest entry "
|
||||||
|
"sets MGITEST_MPR_LANE together with MOBILEGL_PIPE_PUSH's P3a mask, "
|
||||||
|
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH. The ambient entries and the two subsystem "
|
||||||
|
"arms configure none of that, and their log is shared - a read there "
|
||||||
|
"would race a neighbour's bring-up.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH, so "
|
||||||
|
"CallClass::MapPersistentRoundtrips does not exist and the summary "
|
||||||
|
"line carries no mpr=. The entry stays registered so that "
|
||||||
|
"`ctest -L integration-gpu` names the same tests in both builds (G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
|
||||||
|
"emits map_persistent and mpr= is structurally zero. P3a package B "
|
||||||
|
"owns that emitter; this entry arms itself when it lands.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||||
|
"summary line is the only channel this module has for reading "
|
||||||
|
"PipeStats";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::array<unsigned char, 4> CenterPixel() {
|
std::array<unsigned char, 4> CenterPixel() {
|
||||||
std::array<unsigned char, 4> px = {0, 0, 0, 0};
|
std::array<unsigned char, 4> px = {0, 0, 0, 0};
|
||||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
@@ -222,6 +293,97 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respecifying a frontend buffer preserves its VAO attachments even when the
|
||||||
|
// backend replaces the adopted store's GL name. Keep every attribute binding
|
||||||
|
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, RespecifiedVertexArenaKeepsVaoBindings) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
UploadQuad(1.f, 0.f, 0.f);
|
||||||
|
DrawQuad();
|
||||||
|
ASSERT_GT(CenterPixel()[0], 200);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
GLuint otherVao = 0;
|
||||||
|
glGenVertexArrays(1, &otherVao);
|
||||||
|
ConfigureVertexArray(otherVao);
|
||||||
|
DrawQuad(otherVao);
|
||||||
|
EXPECT_GT(CenterPixel()[0], 200);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
constexpr std::array<GLsizeiptr, 3> sizes = {
|
||||||
|
kArenaBytes, kArenaBytes + 4096, kArenaBytes - 4096,
|
||||||
|
};
|
||||||
|
constexpr std::array<std::array<float, 3>, 3> colors = {{
|
||||||
|
{0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, {1.f, 0.f, 0.f},
|
||||||
|
}};
|
||||||
|
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||||
|
SCOPED_TRACE(sizes[i]);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
UploadQuad(colors[i][0], colors[i][1], colors[i][2]);
|
||||||
|
// The unbound VAO can retain the deleted store; the current VAO's
|
||||||
|
// attachments can be cleared by deletion. Both must be repaired.
|
||||||
|
for (GLuint vao : {m_vao, otherVao}) {
|
||||||
|
SCOPED_TRACE(vao);
|
||||||
|
DrawQuad(vao);
|
||||||
|
const auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
for (std::size_t channel = 0; channel < 3; ++channel) {
|
||||||
|
if (colors[i][channel] != 0.f) {
|
||||||
|
EXPECT_GT(px[channel], 200) << "VAO did not fetch the replacement vertex store";
|
||||||
|
} else {
|
||||||
|
EXPECT_LT(px[channel], 50) << "VAO still fetched the previous vertex store";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &otherVao);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
auto vertices = QuadVertices(1.f, 0.f, 0.f);
|
||||||
|
const auto green = QuadVertices(0.f, 1.f, 0.f);
|
||||||
|
vertices.insert(vertices.end(), green.begin(), green.end());
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
|
||||||
|
GLuint indices = 0;
|
||||||
|
glGenBuffers(1, &indices);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices);
|
||||||
|
// Redefine through COPY_WRITE_BUFFER so the element binding slot never
|
||||||
|
// changes. The small final store also exercises returning to shadow storage.
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, indices);
|
||||||
|
constexpr std::array<GLsizeiptr, 4> sizes = {
|
||||||
|
kArenaBytes, kArenaBytes, kArenaBytes + 4096, 4096,
|
||||||
|
};
|
||||||
|
for (std::size_t i = 0; i < sizes.size(); ++i) {
|
||||||
|
SCOPED_TRACE(sizes[i]);
|
||||||
|
const GLuint first = (i % 2) == 0 ? 0u : 6u;
|
||||||
|
const std::array<GLuint, 6> elements = {
|
||||||
|
first, first + 1, first + 2, first + 3, first + 4, first + 5,
|
||||||
|
};
|
||||||
|
glBufferData(GL_COPY_WRITE_BUFFER, sizes[i], nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBufferSubData(GL_COPY_WRITE_BUFFER, 0, sizeof(elements), elements.data());
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
|
||||||
|
const auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_GT(px[first == 0 ? 0 : 1], 200) << "VAO did not fetch the replacement index store";
|
||||||
|
EXPECT_LT(px[first == 0 ? 1 : 0], 50) << "VAO still fetched the previous index store";
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &indices);
|
||||||
|
}
|
||||||
|
|
||||||
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
// The shadow IS the mapping: a readback straight after a CPU write must hand
|
||||||
// back exactly those bytes.
|
// back exactly those bytes.
|
||||||
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||||
@@ -275,4 +437,72 @@ void main() { word = 0xC0FFEEu; }
|
|||||||
<< "the compute write into the adopted arena did not reach the CPU readback";
|
<< "the compute write into the adopted arena did not reach the CPU readback";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// G10, the per-adoption half: ONE storage definition of an arena costs ONE map_persistent
|
||||||
|
// emission, however many draws read it afterwards.
|
||||||
|
//
|
||||||
|
// The arena SetUp defined is deliberately re-defined inside the counted window rather than
|
||||||
|
// measured from outside it: the window a summary line reports is "since the previous line",
|
||||||
|
// so the definition has to happen between the two swaps that bracket it, and a case that
|
||||||
|
// counted SetUp's definition would be reading a window it did not control.
|
||||||
|
//
|
||||||
|
// ONE reading case per lane, for the reason PipeStatsWindow.h gives: the library truncates the
|
||||||
|
// log per process, so two readers in a lane race under `ctest -j`.
|
||||||
|
TEST_F(LargeArenaAdoptionScenario, AnAdoptionCostsExactlyOneMapPersistentRoundtrip) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
SkipUnlessTheRoundtripCounterIsReadableHere();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
Gl().EndFrame(); // close the setup window, SetUp's own definition included
|
||||||
|
|
||||||
|
// One definition of a store past the 16 MiB adoption threshold, taken by RE-SPECIFYING
|
||||||
|
// SetUp's arena while m_vao's attributes are still pointing into it - and the attributes
|
||||||
|
// are deliberately NOT re-declared afterwards, so the draws below can only land if the
|
||||||
|
// backend VAO followed the new store on its own.
|
||||||
|
//
|
||||||
|
// That is the hard shape on purpose. It was routed around in the first cut of this file
|
||||||
|
// because feat/disaggregated did not yet carry `dev`'s d7655247 ("rebind VAOs when an
|
||||||
|
// adopted buffer is respecified - the immediate retire path forgot the buffer-id
|
||||||
|
// generation") and the workload was a hard SIGSEGV inside the vertex fetch on the first
|
||||||
|
// draw after the re-specification. ID-9 merged that fix (feat/disaggregated 5cb826b0) and
|
||||||
|
// requires it to hold in BOTH the legacy and the handle arm of the respecify/retire path,
|
||||||
|
// so this workload counts the path rather than avoiding it: under the
|
||||||
|
// ResourceSubsystemOn./Off. lanes the same body runs on both arms, and a handle arm that
|
||||||
|
// re-implemented the retire without the rebind is a crash here rather than a silent
|
||||||
|
// divergence found on device.
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the arena inside the counted window failed";
|
||||||
|
|
||||||
|
// ... and then a frame's worth of traffic against it, of the shape the arena exists for:
|
||||||
|
// a SubData per draw, every one of which lands in the adopted mapping and none of which
|
||||||
|
// may acquire it again.
|
||||||
|
for (int draw = 0; draw < kDrawsInTheWindow; ++draw) {
|
||||||
|
UploadQuad(0.f, 1.f, 0.f);
|
||||||
|
DrawQuad();
|
||||||
|
}
|
||||||
|
const auto px = CenterPixel();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_GT(px[1], 200) << "the draws inside the counted window never landed, so the count below "
|
||||||
|
"would be a number about nothing";
|
||||||
|
|
||||||
|
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||||
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||||
|
ASSERT_TRUE(window.found) << "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ": either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not "
|
||||||
|
"reach the process, or nothing reached PipeStats::OnPresent.";
|
||||||
|
RecordProperty("stats_line", window.line.c_str());
|
||||||
|
|
||||||
|
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
|
||||||
|
ASSERT_GE(roundtrips, 0) << "the summary line carries no mpr= field: " << window.line;
|
||||||
|
EXPECT_EQ(roundtrips, 1)
|
||||||
|
<< "one storage definition of an adopted arena is one map_persistent emission "
|
||||||
|
"(ARCHITECTURE.md:474, D-B2: mint OR decline, both need an answer from the resource "
|
||||||
|
"owner). This window defined the arena once and drew from it "
|
||||||
|
<< kDrawsInTheWindow << " times, so 1 is the whole cost; " << kDrawsInTheWindow
|
||||||
|
<< " would mean the acquisition moved onto the draw path - the ~167 ms/arena hiccup this "
|
||||||
|
"adoption removed, re-introduced - and 0 would mean the emission stopped happening. It "
|
||||||
|
"reported: "
|
||||||
|
<< window.line;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,878 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE P4a SUBSYSTEM A/B IS REAL, AND ITS DEPENDENCY REFUSALS ARE EXERCISED (gate G12).
|
||||||
|
//
|
||||||
|
// P4a migrates FOUR subsystems (D-K1, MG_Pipe/MGPipe.h):
|
||||||
|
//
|
||||||
|
// bit 9 kMGPipeSubsystemFramebuffer set_framebuffer_state
|
||||||
|
// bit 10 kMGPipeSubsystemTextureResources texture + renderbuffer resource_*, set_texture_params
|
||||||
|
// bit 11 kMGPipeSubsystemSamplers sampler CSO, sampler view, the three unit sets
|
||||||
|
// bit 12 kMGPipeSubsystemPrograms shader CSO, draw/dispatch program, global constants
|
||||||
|
//
|
||||||
|
// so the push build's default mask becomes kMGPipeSubsystemsMigratedAtP4a = 0x1fff, and P3a's
|
||||||
|
// 0x1ff survives as the control that clears exactly those four - MGPipe.h's rule that every phase's
|
||||||
|
// constant keeps meaning what it meant, so an operator's recorded mask is still readable a phase
|
||||||
|
// later. THE OFF LANE IS 0x1ff AND NOT A HAND-PICKED PATTERN, for that reason.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// every later comparison is quietly taken against a switch that does nothing. This file is the
|
||||||
|
// entry that cannot let that happen. It is the P4a analogue of ResourceSubsystemControlScenario and
|
||||||
|
// deliberately its twin in shape.
|
||||||
|
//
|
||||||
|
// WHAT IT ASSERTS, per lane:
|
||||||
|
//
|
||||||
|
// on (MOBILEGL_PIPE_PUSH=0x1fff)
|
||||||
|
// The client emits P4a's records for the workload: a framebuffer state per bound target that
|
||||||
|
// moved, the three unit sets, and the client-side texture upload record. The window's
|
||||||
|
// emit[fbe= sve= sse= sie= ctu=] bracket therefore carries a NON-ZERO total.
|
||||||
|
//
|
||||||
|
// off (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default = P4a's four subsystems cleared)
|
||||||
|
// The frontend dispatch falls through to the legacy MGB_CTX-reading arms, nothing is emitted
|
||||||
|
// through any of the four families, and every one of those five counters must read ZERO.
|
||||||
|
// This is the reading a dead switch fails: with the bits ignored, this lane would report the
|
||||||
|
// same non-zero counts as the other one.
|
||||||
|
//
|
||||||
|
// refused (MOBILEGL_PIPE_PUSH=0x9ff = bits 0..8 plus bit 11, samplers, WITHOUT bit 10)
|
||||||
|
// D-K2's dependency refusal. Every MGPBoundView::Texture and MGPImageView::Res names a
|
||||||
|
// Texture handle and only bit 10 populates the texture slot table, so a sampler subsystem
|
||||||
|
// without it would miss every lookup and walk on without unbinding. The bring-up logs ONE
|
||||||
|
// error naming BOTH bits, refuses bit 11 and runs the legacy sampler arm - modelled on the
|
||||||
|
// bit-8-requires-bit-7 refusal that already ships (Managers.cpp:2393-2410). The assertion is
|
||||||
|
// that the refusal is NAMED and that the run then produces the same pixels as any other
|
||||||
|
// lane: a refusal that half-ran, or that aborted, would both be failures here.
|
||||||
|
//
|
||||||
|
// refused-texture (MOBILEGL_PIPE_PUSH=0x5ff = bits 0..8 plus bit 10, texture resources, WITHOUT
|
||||||
|
// bit 11)
|
||||||
|
// D-K2's FOURTH row (ID-15), and the direction the brief originally called harmless.
|
||||||
|
// MGPTextureParams::BuiltinSampler is a SamplerCso HANDLE and only bit 11 mints sampler
|
||||||
|
// CSOs, so with bit 10 alone every set_texture_params would carry a null there and the
|
||||||
|
// applier's Fatal{ProtocolCorruption} is the next thing that happens. Same two assertions
|
||||||
|
// as the lane above, with the two bits' roles swapped.
|
||||||
|
//
|
||||||
|
// both refusal lanes
|
||||||
|
// "NAMED" means ONE LINE of the library's log, at ERROR severity, that says it REFUSED and
|
||||||
|
// names both bits. Not a substring anywhere in the file: the word "sampler" appears in
|
||||||
|
// almost any log the sampler path writes to, and an assertion that cannot go red for its
|
||||||
|
// stated reason is worse than no assertion (review F-M6).
|
||||||
|
//
|
||||||
|
// every lane
|
||||||
|
// THE PIXELS MUST NOT MOVE. The workload draws one solid-colour quad through a texture, an
|
||||||
|
// explicit sampler object and a user framebuffer, and every lane must read back that colour.
|
||||||
|
// "The counters moved and the picture did not" is the whole claim - a switch that changed
|
||||||
|
// what is drawn would not be an A/B, it would be a bug.
|
||||||
|
//
|
||||||
|
// WHY IT CAN SKIP. The counters are emitted by the client-side emitters P4a packages B and C own,
|
||||||
|
// and this file is written against the P4a contract commit, before either lands. Until then nothing
|
||||||
|
// emits, the five counters are structurally zero in BOTH lanes, and an assertion about the
|
||||||
|
// difference would be a statement about nothing. The build answers the question rather than a
|
||||||
|
// hand-maintained list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/
|
||||||
|
// for the counters' names and passes the answer in as MGITEST_PIPE_OBJECT_EMITTER_PRESENT, with a
|
||||||
|
// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale. It
|
||||||
|
// is a CONTENT probe, not a filename probe, so the owning packages keep control of their own file
|
||||||
|
// layout - P4a's new client files are headers (D-P), and a glob for a named .cpp would have kept
|
||||||
|
// this control skipping forever with a reason that had become false.
|
||||||
|
//
|
||||||
|
// DIRECTGLES ONLY, and that is the honest scope: P4a migrates Espryt's framebuffer, texture,
|
||||||
|
// sampler and program paths. Magma's are P7 (D-Q) and register nothing here, so a DirectVulkan lane
|
||||||
|
// would be measuring the client emitters against a backend nobody asked to change.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeApplyPeek.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set by the three ObjectSubsystemControl. ctest entries and by nothing else; a harness
|
||||||
|
// marker, never read by the library. Its absence means an ambient entry, where neither the
|
||||||
|
// stats channel nor a private log path is configured.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_OBJECT_SUBSYSTEM_LANE";
|
||||||
|
constexpr const char* kLaneOn = "on";
|
||||||
|
constexpr const char* kLaneOff = "off";
|
||||||
|
constexpr const char* kLaneRefused = "refused";
|
||||||
|
// D-K2's FOURTH row (ID-15): bit 10 without bit 11. 0x5ff is 0x1ff plus bit 10.
|
||||||
|
constexpr const char* kLaneRefusedTexture = "refused-texture";
|
||||||
|
// c0f's two halves (ID-39/ID-40), run at the phase default on BOTH backends: the client
|
||||||
|
// GATE (a P4a family emits only where a backend registered MGPipeResourceOps) and the
|
||||||
|
// applier's BELT (every P4a entry point refuses and counts RefusedNoConsumer when none
|
||||||
|
// did). One lane per backend, because the interesting one is the backend with NO
|
||||||
|
// consumer - Magma - and the other is the control that says the assertion is not
|
||||||
|
// vacuously true of a tree where nothing emits at all.
|
||||||
|
constexpr const char* kLaneConsumer = "consumer";
|
||||||
|
constexpr const char* kLaneNoConsumer = "no-consumer";
|
||||||
|
|
||||||
|
bool LaneIsARefusalLane(const std::string& lane) {
|
||||||
|
return lane == kLaneRefused || lane == kLaneRefusedTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LaneIsAConsumerLane(const std::string& lane) {
|
||||||
|
return lane == kLaneConsumer || lane == kLaneNoConsumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
constexpr int kTextureSize = 4;
|
||||||
|
// Enough frames that a per-frame emitter and a per-draw emitter read differently, and few
|
||||||
|
// enough that one summary window covers exactly this.
|
||||||
|
constexpr int kDrawsInTheWindow = 4;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = aPos * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texture(uTex, vUv); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LaneName() {
|
||||||
|
const char* lane = std::getenv(kLaneMarker);
|
||||||
|
return lane != nullptr ? std::string(lane) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- reading the refusal out of the library's own log ------------------------------
|
||||||
|
//
|
||||||
|
// THE UNIT IS A LINE, AND THE LINE HAS TO BE THE REFUSAL (review F-M6). The first cut of
|
||||||
|
// this asked whether the WHOLE FILE contained a lowercase "sampler" and whether it
|
||||||
|
// contained "texture resource", anywhere, in any order, at any severity. Both are true of
|
||||||
|
// almost any log the moment the sampler path says anything at all, so the assertion could
|
||||||
|
// not go red for the reason it claims and the one P4a control that is not vacuous before
|
||||||
|
// the emitters land would have been vacuous too.
|
||||||
|
//
|
||||||
|
// What is matched instead is one line that is ALL of:
|
||||||
|
// * at ERROR severity - the library writes "[<time>] [<os> <thread>/<TAG>]: <message>",
|
||||||
|
// one record per line (MG_Util/Debug/Log.cpp), and D-K2 asks for an MGLOG_E. A refusal
|
||||||
|
// that was demoted to a D or a W is a refusal an operator's log will not carry;
|
||||||
|
// * carrying the helper's own decision clause, verbatim - so a line that merely
|
||||||
|
// mentions the two bits (a future summary, a comment echoed into the log) is not
|
||||||
|
// mistaken for the decision;
|
||||||
|
// * naming the bit that was SET and the bit it NEEDED, on that same line, AND IN THAT
|
||||||
|
// ORDER - see the direction check below.
|
||||||
|
//
|
||||||
|
// Espryt's text is one MGLOG_E from the helper the three dependent families share
|
||||||
|
// (Managers.cpp, PipeSubsystemDependencyMissing): "MGPipe: <A> (bit N) is set but <B>
|
||||||
|
// (bit M) is clear; <why> - REFUSING the dependent bit and running the legacy arm. Set
|
||||||
|
// both bits, or clear both". Three spellings are accepted per bit - the constant's name,
|
||||||
|
// "(bit N)", and the hexadecimal mask - so the assertion pins the DECISION and the
|
||||||
|
// DIRECTION, and not the family-specific prose in <why>.
|
||||||
|
//
|
||||||
|
// THE DIRECTION IS THE HALF THIS FILE USED TO BE MISSING (review F-v2-m1). The first form
|
||||||
|
// of the matcher asked "does the line name bit A?" AND "does the line name bit B?", which
|
||||||
|
// is a SYMMETRIC conjunction: swapping the two arguments - exactly what separates the
|
||||||
|
// 0x5ff case from the 0x9ff one below, and what each of their comments claims to be
|
||||||
|
// doing - could not change the answer, and both cases went green on either line. A
|
||||||
|
// resolver that refused correctly but printed the MIRROR sentence would have been green
|
||||||
|
// on a refusal that told the operator the wrong dependency, which is the same class of
|
||||||
|
// "the log says something plausible" defect that made the whole-file substring search
|
||||||
|
// (F-M6) worthless one level up. The two resolvers are forty lines apart in one file,
|
||||||
|
// share this helper and differ only in the `what` string, so the copy-paste is one edit
|
||||||
|
// away at all times.
|
||||||
|
//
|
||||||
|
// What makes the direction readable is the sentence's own shape: the SET bit is named
|
||||||
|
// before " is set but " and the NEEDED bit between that and " is clear". So the check is
|
||||||
|
// four offsets in strictly increasing order, and it is the sentence Espryt emits rather
|
||||||
|
// than a re-statement of it.
|
||||||
|
constexpr const char* kSaysItRefused = "REFUSING the dependent bit and running the legacy arm";
|
||||||
|
constexpr const char* kSaysWhichIsSet = " is set but ";
|
||||||
|
constexpr const char* kSaysWhichIsClear = " is clear";
|
||||||
|
|
||||||
|
// The earliest offset at which any accepted spelling of one bit appears, or npos. The
|
||||||
|
// EARLIEST rather than any: a spelling that also occurs later in <why> (Espryt's
|
||||||
|
// bit-10-requires-bit-11 sentence says "only bit 11 mints sampler CSOs" in its reason)
|
||||||
|
// must not be able to satisfy an ordering the first occurrence does not.
|
||||||
|
std::size_t EarliestSpellingOffset(const std::string& line,
|
||||||
|
const std::vector<std::string>& spellings) {
|
||||||
|
std::size_t earliest = std::string::npos;
|
||||||
|
for (const std::string& spelling : spellings) {
|
||||||
|
const std::size_t at = line.find(spelling);
|
||||||
|
if (at != std::string::npos && (earliest == std::string::npos || at < earliest)) {
|
||||||
|
earliest = at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return earliest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The matching line, or an empty string. Returned rather than a bool so the case can print
|
||||||
|
// what it found: a reader of a green refusal lane must be able to see the sentence.
|
||||||
|
std::string FindTheRefusalLine(const std::string& log,
|
||||||
|
const std::vector<std::string>& bitThatWasSet,
|
||||||
|
const std::vector<std::string>& bitThatWasNeeded) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos <= log.size()) {
|
||||||
|
const std::size_t newline = log.find('\n', pos);
|
||||||
|
const std::string line = log.substr(
|
||||||
|
pos, newline == std::string::npos ? std::string::npos : newline - pos);
|
||||||
|
const bool atErrorSeverity = line.find("/ERROR]") != std::string::npos;
|
||||||
|
const std::size_t refusedAt = line.find(kSaysItRefused);
|
||||||
|
const std::size_t setAt = EarliestSpellingOffset(line, bitThatWasSet);
|
||||||
|
const std::size_t setClauseAt = line.find(kSaysWhichIsSet);
|
||||||
|
const std::size_t neededAt = EarliestSpellingOffset(line, bitThatWasNeeded);
|
||||||
|
const std::size_t clearClauseAt = line.find(kSaysWhichIsClear);
|
||||||
|
const bool everyPartIsThere =
|
||||||
|
refusedAt != std::string::npos && setAt != std::string::npos &&
|
||||||
|
setClauseAt != std::string::npos && neededAt != std::string::npos &&
|
||||||
|
clearClauseAt != std::string::npos;
|
||||||
|
// "<set bit> ... is set but ... <needed bit> ... is clear", strictly in that
|
||||||
|
// order. Swapping the caller's two arguments breaks the chain, which is the
|
||||||
|
// whole of F-v2-m1.
|
||||||
|
const bool inTheRightDirection =
|
||||||
|
everyPartIsThere && setAt < setClauseAt && setClauseAt < neededAt &&
|
||||||
|
neededAt < clearClauseAt;
|
||||||
|
if (atErrorSeverity && inTheRightDirection) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
if (newline == std::string::npos) break;
|
||||||
|
pos = newline + 1;
|
||||||
|
}
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The three accepted spellings of each of the two P4a bits this file's two refusal lanes
|
||||||
|
// are about. MGPipe.h: bit 10 = kMGPipeSubsystemTextureResources = 0x400,
|
||||||
|
// bit 11 = kMGPipeSubsystemSamplers = 0x800.
|
||||||
|
std::vector<std::string> SamplerBitSpellings() {
|
||||||
|
return {"kMGPipeSubsystemSamplers", "(bit 11)", "0x800"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> TextureResourceBitSpellings() {
|
||||||
|
return {"kMGPipeSubsystemTextureResources", "(bit 10)", "0x400"};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectSubsystemControlScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
m_lane = LaneName();
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
|
||||||
|
glGenBuffers(1, &m_quadBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindSampler(0, 0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||||
|
// through a return value; every caller pairs it with `if (IsSkipped()) return;`.
|
||||||
|
void SkipUnlessTheLaneIsAssertableHere(bool needsTheEmitters) {
|
||||||
|
if (m_lane.empty()) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the three ObjectSubsystemControl. "
|
||||||
|
"ctest entries set " << kLaneMarker
|
||||||
|
<< " together with the MOBILEGL_PIPE_PUSH bitmask that arm means, "
|
||||||
|
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a "
|
||||||
|
"private MOBILEGL_LOG_FILE_PATH. None of that is configured in "
|
||||||
|
"the ambient entries, and the ambient log is shared, so a read "
|
||||||
|
"here would race.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: there are no "
|
||||||
|
"subsystem bits to clear, P4a's five CallClass members do not "
|
||||||
|
"exist and the summary line carries no emit[...] bracket. The "
|
||||||
|
"entry is registered here anyway so that `ctest -L "
|
||||||
|
"integration-gpu` names the same tests in the pull build and the "
|
||||||
|
"push build (gate G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (needsTheEmitters && !BuildMarkerIsSet("MGITEST_PIPE_OBJECT_EMITTER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Impl/Pipe/ emits FramebufferEmissions, so nothing "
|
||||||
|
"sends a P4a record, every counter in the emit[] bracket is "
|
||||||
|
"structurally zero in BOTH lanes and the difference between them "
|
||||||
|
"is not observable yet. P4a packages B (framebuffer, texture) "
|
||||||
|
"and C (sampler, image, program) own those emitters; this "
|
||||||
|
"control arms itself when they land, whatever files they use.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||||
|
"own log is the only channel this module has for reading "
|
||||||
|
"PipeStats and the bring-up's refusal line";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The workload, and every one of P4a's four families is in it exactly once per draw:
|
||||||
|
// a USER FRAMEBUFFER with a texture attachment (bit 9), a TEXTURE with parameters and
|
||||||
|
// an upload (bit 10), an explicit SAMPLER OBJECT on the unit (bit 11) and a PROGRAM
|
||||||
|
// with a default-uniform-block write (bit 12). A lane that steered only one of the four
|
||||||
|
// would move only its own counter, which is why they are counted separately.
|
||||||
|
void RunTheWorkload() {
|
||||||
|
std::vector<std::uint8_t> texels(kTextureSize * kTextureSize * 4);
|
||||||
|
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||||
|
texels[i] = 0;
|
||||||
|
texels[i + 1] = 255;
|
||||||
|
texels[i + 2] = 0;
|
||||||
|
texels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
glGenTextures(1, &m_texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||||
|
|
||||||
|
glGenSamplers(1, &m_sampler);
|
||||||
|
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
// The user framebuffer, drawn into once per iteration so that the framebuffer
|
||||||
|
// record has a reason to move: the binding alternates between it and the default
|
||||||
|
// framebuffer, which is exactly what a per-target set_framebuffer_state counts.
|
||||||
|
glGenTextures(1, &m_attachment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_attachment);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glGenFramebuffers(1, &m_fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
m_attachment, 0);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
|
||||||
|
for (int draw = 0; draw < kDrawsInTheWindow; ++draw) {
|
||||||
|
// Into the user framebuffer...
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glBindSampler(0, m_sampler);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
// ...and into the default one, which is what the case reads back.
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
// One sub-region upload per iteration, so the client-side texture upload
|
||||||
|
// counter (ctu) has something to count and the server's tex[emit=] has the
|
||||||
|
// same something.
|
||||||
|
const std::uint8_t green[4] = {0, 255, 0, 255};
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, draw % kTextureSize, 0, 1, 1, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseTheWorkload() {
|
||||||
|
glBindSampler(0, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||||
|
if (m_sampler != 0) glDeleteSamplers(1, &m_sampler);
|
||||||
|
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||||
|
if (m_attachment != 0) glDeleteTextures(1, &m_attachment);
|
||||||
|
m_fbo = m_sampler = m_texture = m_attachment = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_lane;
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_quadBuffer = 0;
|
||||||
|
GLuint m_texture = 0;
|
||||||
|
GLuint m_attachment = 0;
|
||||||
|
GLuint m_sampler = 0;
|
||||||
|
GLuint m_fbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ONE case per lane, and it is a constraint rather than a preference: this case READS the
|
||||||
|
// library log, the log is a per-LANE resource (the library opens it fopen(path, "w"), so
|
||||||
|
// every process in a lane truncates it), and a second case in the same lane would race this
|
||||||
|
// one under `ctest -j` with a failure indistinguishable from "the counter was never
|
||||||
|
// emitted". The CMake registration gives each lane a TEST_FILTER naming one case.
|
||||||
|
TEST_F(ObjectSubsystemControlScenario, ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/true);
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
if (LaneIsAConsumerLane(m_lane)) {
|
||||||
|
GTEST_SKIP() << "the two consumer lanes run their own case instead "
|
||||||
|
"(TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate). They "
|
||||||
|
"are at the phase default on both backends and their subject is "
|
||||||
|
"c0f's gate/belt pair, not the on/off A/B: on the backend with no "
|
||||||
|
"consumer the emit[] bracket is structurally zero AT the default "
|
||||||
|
"mask, which is neither the on-lane's expectation nor the "
|
||||||
|
"off-lane's.";
|
||||||
|
}
|
||||||
|
if (LaneIsARefusalLane(m_lane)) {
|
||||||
|
GTEST_SKIP() << "the refusal lanes run their own case instead (0x9ff -> "
|
||||||
|
"ASamplerBitWithoutTheTextureBitIsRefusedAndNamed, 0x5ff -> "
|
||||||
|
"ATextureBitWithoutTheSamplerBitIsRefusedAndNamed): a refused "
|
||||||
|
"subsystem's emission counts are neither the on-lane's nor the "
|
||||||
|
"off-lane's, and asserting either would be reading a third arm as "
|
||||||
|
"if it were one of the two.";
|
||||||
|
}
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
Gl().EndFrame(); // close the setup window: everything below is one window
|
||||||
|
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RunTheWorkload();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the workload left a GL error behind";
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||||
|
|
||||||
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||||
|
ASSERT_TRUE(window.found)
|
||||||
|
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting "
|
||||||
|
"here), so either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not reach "
|
||||||
|
"the process, or no summary line was emitted at all because nothing reached "
|
||||||
|
"PipeStats::OnPresent.";
|
||||||
|
RecordProperty("stats_line", window.line.c_str());
|
||||||
|
|
||||||
|
// The five counters of the emit[] bracket, read individually so that a lane which
|
||||||
|
// steered one family and not another says WHICH.
|
||||||
|
const long long framebuffer = PipeStatsWindow::CounterOrAbsent(window, "fbe");
|
||||||
|
const long long samplerViews = PipeStatsWindow::CounterOrAbsent(window, "sve");
|
||||||
|
const long long samplerStates = PipeStatsWindow::CounterOrAbsent(window, "sse");
|
||||||
|
const long long shaderImages = PipeStatsWindow::CounterOrAbsent(window, "sie");
|
||||||
|
const long long clientUploads = PipeStatsWindow::CounterOrAbsent(window, "ctu");
|
||||||
|
ASSERT_GE(framebuffer, 0)
|
||||||
|
<< "the summary line carries no fbe= field, so this build's PipeStats has no P4a "
|
||||||
|
"emission counters to read: "
|
||||||
|
<< window.line;
|
||||||
|
ASSERT_GE(samplerViews, 0) << "no sve= field: " << window.line;
|
||||||
|
ASSERT_GE(samplerStates, 0) << "no sse= field: " << window.line;
|
||||||
|
ASSERT_GE(shaderImages, 0) << "no sie= field: " << window.line;
|
||||||
|
ASSERT_GE(clientUploads, 0) << "no ctu= field: " << window.line;
|
||||||
|
const long long total = framebuffer + samplerViews + samplerStates + shaderImages +
|
||||||
|
clientUploads;
|
||||||
|
|
||||||
|
if (m_lane == kLaneOn) {
|
||||||
|
EXPECT_GT(total, 0)
|
||||||
|
<< "with bits 9|10|11|12 SET the four P4a families are the path this workload "
|
||||||
|
"takes - a user framebuffer bound and unbound "
|
||||||
|
<< kDrawsInTheWindow
|
||||||
|
<< " times, a texture with parameters and a sub-region upload per iteration, an "
|
||||||
|
"explicit sampler object on the unit and a program with a default-uniform "
|
||||||
|
"write - so the window's emit[] bracket must carry something. All five "
|
||||||
|
"reading zero means the emitters never ran on the arm that is supposed to run "
|
||||||
|
"them. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
// The framebuffer family on its own, because it is the one that would be hidden by
|
||||||
|
// a large upload count: a suppressor that stopped suppressing shows up as fbe
|
||||||
|
// tracking the DRAW count, and a family that never emitted shows up as zero.
|
||||||
|
EXPECT_GT(framebuffer, 0)
|
||||||
|
<< "fbe= is zero on the ON lane: set_framebuffer_state never went out even "
|
||||||
|
"though the workload bound a user framebuffer and the default framebuffer "
|
||||||
|
<< kDrawsInTheWindow << " times each. " << window.line;
|
||||||
|
} else if (m_lane == kLaneOff) {
|
||||||
|
EXPECT_EQ(total, 0)
|
||||||
|
<< "with bits 9|10|11|12 CLEARED (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default) the "
|
||||||
|
"frontend dispatch must fall through to the legacy MGB_CTX-reading arms and "
|
||||||
|
"emit nothing through any of the four P4a families, so every counter in the "
|
||||||
|
"emit[] bracket must be zero. A non-zero count here is the dead-switch "
|
||||||
|
"reading: the bits are being ignored, both arms run the same code, and every "
|
||||||
|
"push-vs-pull number taken against this A/B is measuring one arm twice. It "
|
||||||
|
"reported: "
|
||||||
|
<< window.line;
|
||||||
|
} else {
|
||||||
|
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane
|
||||||
|
<< "': the arms are on / off / refused / refused-texture / consumer / "
|
||||||
|
"no-consumer. Reading an unrecognised name as any of them would make "
|
||||||
|
"this lane assert another arm's expectation while claiming to test "
|
||||||
|
"this one.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and the picture is the same whichever arm ran.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, "green", 0.0,
|
||||||
|
"the sampled draw [" + m_lane + "]"))
|
||||||
|
<< "the subsystem bits changed what is DRAWN, which is not an A/B - the handle path "
|
||||||
|
"and the legacy path must produce the same pixels from the same texture, sampler "
|
||||||
|
"and framebuffer.";
|
||||||
|
|
||||||
|
ReleaseTheWorkload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// D-K2's dependency refusal, in the direction that has to be refused.
|
||||||
|
//
|
||||||
|
// 0x9ff is bits 0..8 (everything P3a shipped) plus bit 11 (samplers) and WITHOUT bit 10
|
||||||
|
// (texture resources). Every MGPBoundView::Texture and every MGPImageView::Res names a
|
||||||
|
// Texture handle, and only bit 10 populates the texture slot table, so with bit 11 alone
|
||||||
|
// every lookup would miss and the unit walk would `continue` without unbinding - a
|
||||||
|
// half-run subsystem, which ROADMAP.md:7 forbids as loudly as a dead switch. The bring-up
|
||||||
|
// logs ONE error naming both bits, refuses bit 11, and runs the legacy sampler arm.
|
||||||
|
//
|
||||||
|
// TWO ASSERTIONS, and the second is the one that stops this from being a log-scraping test:
|
||||||
|
// the refusal is NAMED in the library's own log, and the run then draws the same picture as
|
||||||
|
// every other lane. A refusal that aborted the process, and a refusal that silently let the
|
||||||
|
// half-configured arm run, are both failures - and they look completely different here.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(ObjectSubsystemControlScenario, ASamplerBitWithoutTheTextureBitIsRefusedAndNamed) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// needsTheEmitters=false: the refusal is a BRING-UP decision made from the bitmask
|
||||||
|
// alone, so it is assertable before any emitter exists - which is exactly what makes it
|
||||||
|
// the one P4a control that is not vacuous on the contract tree.
|
||||||
|
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
if (m_lane != kLaneRefused) {
|
||||||
|
GTEST_SKIP() << "runs only in the refusal lane (MOBILEGL_PIPE_PUSH=0x9ff): the "
|
||||||
|
"on/off lanes configure a mask whose dependencies are all satisfied, "
|
||||||
|
"so there is no refusal there to find and a search for one would "
|
||||||
|
"report a healthy lane as red.";
|
||||||
|
}
|
||||||
|
// The refusal is decided from the bitmask, but it is a BACKEND's decision: D-K2 puts it
|
||||||
|
// in ResolveSamplersSubsystemArm(), beside the bit-8-requires-bit-7 refusal that
|
||||||
|
// already ships, and that function is package D's (Managers.cpp). A backend that does
|
||||||
|
// not yet honour P4a's mask at all cannot refuse a dependency inside it, so on such a
|
||||||
|
// tree there is nothing here to find and this case SKIPS rather than reporting the
|
||||||
|
// absence of an unimplemented subsystem as a failure. The marker is the same one
|
||||||
|
// HandleRecycle's P4a cases read - "does any source under this backend name one of the
|
||||||
|
// four P4a subsystem constants" - because naming the constant is exactly what honouring
|
||||||
|
// the mask means.
|
||||||
|
{
|
||||||
|
const std::string& backend = Gl().BackendName();
|
||||||
|
const std::string marker =
|
||||||
|
"MGITEST_HANDLE_REKEY_OBJECTS_" + (backend == "DirectVulkan"
|
||||||
|
? std::string("DirectVulkan")
|
||||||
|
: std::string("DirectGLES"));
|
||||||
|
if (!BuildMarkerIsSet(marker.c_str())) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Backend/"
|
||||||
|
<< backend
|
||||||
|
<< " names any of kMGPipeSubsystem{Framebuffer, TextureResources, "
|
||||||
|
"Samplers, Programs}, so this backend does not honour P4a's mask "
|
||||||
|
"and cannot refuse a dependency inside it. D-K2's refusal lives "
|
||||||
|
"in ResolveSamplersSubsystemArm() beside the bit-8-requires-bit-7 "
|
||||||
|
"one that already ships (Managers.cpp:2393-2410), which is P4a "
|
||||||
|
"package D's file; this control arms itself when that lands. The "
|
||||||
|
"lane itself is not wasted: the library came up under 0x9ff, "
|
||||||
|
"which on a tree with no P4a arm is P3a's mask plus one inert "
|
||||||
|
"bit, and a mask that aborted a bring-up would have failed this "
|
||||||
|
"entry before the skip.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RunTheWorkload();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
|
||||||
|
<< "the workload left a GL error behind on the refused lane, which would mean the "
|
||||||
|
"refusal did not fall back cleanly to the legacy arm";
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
|
||||||
|
ASSERT_FALSE(log.empty())
|
||||||
|
<< "the library wrote nothing to " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ", so the refusal cannot be read back. MOBILEGL_LOG_FILE_PATH is the only channel "
|
||||||
|
"this module has for the library's own report.";
|
||||||
|
// ONE LINE, at ERROR severity, saying it refused and naming BOTH bits. See
|
||||||
|
// FindTheRefusalLine: a substring search over the whole file cannot go red for the
|
||||||
|
// reason this case claims (F-M6).
|
||||||
|
const std::string refusal =
|
||||||
|
FindTheRefusalLine(log, SamplerBitSpellings(), TextureResourceBitSpellings());
|
||||||
|
EXPECT_FALSE(refusal.empty())
|
||||||
|
<< "MOBILEGL_PIPE_PUSH=0x9ff sets the sampler subsystem (bit 11) without the texture "
|
||||||
|
"resource subsystem (bit 10) it depends on, and no single ERROR line of the "
|
||||||
|
"library's log both says it REFUSED and names the two bits. D-K2 requires ONE "
|
||||||
|
"MGLOG_E naming both and a fall back to the legacy sampler arm; a mask that is "
|
||||||
|
"silently half-honoured is the failure this case exists to catch, and it is "
|
||||||
|
"invisible in the pixels by construction. Accepted spellings per bit are the "
|
||||||
|
"constant's name, '(bit 11)' / '(bit 10)', and '0x800' / '0x400'. The log was "
|
||||||
|
<< log.size() << " bytes and is at " << PipeStatsWindow::LibraryLogPath() << ".";
|
||||||
|
if (!refusal.empty()) {
|
||||||
|
// Printed on the pass as well: a reader of a green refusal lane must be able to
|
||||||
|
// see the sentence the lane went green on.
|
||||||
|
std::cout << "[ ObjectSubsystemControl ] refusal line: " << refusal << std::endl;
|
||||||
|
RecordProperty("refusal_line", refusal.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, "green", 0.0,
|
||||||
|
"the sampled draw [refused]"))
|
||||||
|
<< "the refused configuration did not draw what every other lane draws. A refusal is "
|
||||||
|
"supposed to run the LEGACY arm, which is the arm that ships in a pull build - so "
|
||||||
|
"the pixels are the one thing it may not change.";
|
||||||
|
|
||||||
|
ReleaseTheWorkload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// D-K2's FOURTH dependency row, in the OTHER direction: bit 10 without bit 11 (ID-15).
|
||||||
|
//
|
||||||
|
// 0x5ff is bits 0..8 plus bit 10 (texture resources) and WITHOUT bit 11 (samplers).
|
||||||
|
//
|
||||||
|
// WHY THIS IS A REFUSAL AND NOT THE "FINE" MIRROR PAIR THE BRIEF ORIGINALLY CALLED IT.
|
||||||
|
// BRIEF-P4A.md's D-K2 says "bit 10 without bit 11 is fine", and that sentence is wrong for
|
||||||
|
// P4a AS BUILT: MGPTextureParams carries a BuiltinSampler, which is a SamplerCso HANDLE,
|
||||||
|
// and only bit 11 mints sampler CSOs - c0b's four unconditional mints deliberately exclude
|
||||||
|
// that kind (contract-v2.md), and package C content-addresses them through its own cache
|
||||||
|
// (ID-14). With bit 10 set and bit 11 clear every set_texture_params would therefore carry
|
||||||
|
// a NULL BuiltinSampler, which the applier treats as Fatal{ProtocolCorruption} (wire H1),
|
||||||
|
// and minting it client-side in the arm that exists to exclude samplers was rejected. So
|
||||||
|
// the dependency is real and it has to be refused at bring-up, exactly like bit 11 without
|
||||||
|
// bit 10 above and bit 8 without bit 7 one phase earlier. ID-15 puts the refusal in the
|
||||||
|
// texture family's Resolve*SubsystemArm - package D's Managers.cpp - and this case is the
|
||||||
|
// pin that says it is there.
|
||||||
|
//
|
||||||
|
// ON A TREE WHOSE BACKEND DOES NOT HONOUR P4a's MASK THIS SKIPS, NAMED, exactly as the
|
||||||
|
// 0x9ff case does and for the same reason: a backend that never reads the four constants
|
||||||
|
// cannot refuse a dependency between two of them, and reporting the absence of an
|
||||||
|
// unimplemented subsystem as a failure is what ID-2 forbids. Once the backend DOES name
|
||||||
|
// them the case is a hard pin, which is the point - if D's texture-family resolver honours
|
||||||
|
// the mask and does not carry this row, this entry is where that shows.
|
||||||
|
//
|
||||||
|
// WHAT THIS ARM DOES ON THE INTEGRATED TREE, corrected (review F-v2-m2). An earlier
|
||||||
|
// round's report told the integrator to expect this lane to go RED between esprytobj's
|
||||||
|
// integration and package D's rework, and to read that red as expected. That window does
|
||||||
|
// not exist: esprytobj v2 already carries D-K2's fourth row - Managers.cpp's
|
||||||
|
// ResolveTextureResourceSubsystemArm refuses bit 10 without bit 11 with the sentence this
|
||||||
|
// case matches - so the arm ARMS AND PASSES. A red here is therefore a real finding about
|
||||||
|
// that resolver (it stopped refusing, refused for the wrong reason, or printed the mirror
|
||||||
|
// sentence, which the direction check above is what catches) and must not be waved
|
||||||
|
// through as a sequencing artefact.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(ObjectSubsystemControlScenario, ATextureBitWithoutTheSamplerBitIsRefusedAndNamed) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// needsTheEmitters=false, for the 0x9ff case's reason: a bring-up decision made from
|
||||||
|
// the bitmask alone is assertable before any emitter exists.
|
||||||
|
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
if (m_lane != kLaneRefusedTexture) {
|
||||||
|
GTEST_SKIP() << "runs only in the texture-side refusal lane "
|
||||||
|
"(MOBILEGL_PIPE_PUSH=0x5ff): every other lane configures a mask "
|
||||||
|
"whose dependencies are satisfied or a different refusal, so there "
|
||||||
|
"is nothing here to find and a search for one would report a "
|
||||||
|
"healthy lane as red.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string& backend = Gl().BackendName();
|
||||||
|
const std::string marker =
|
||||||
|
"MGITEST_HANDLE_REKEY_OBJECTS_" + (backend == "DirectVulkan"
|
||||||
|
? std::string("DirectVulkan")
|
||||||
|
: std::string("DirectGLES"));
|
||||||
|
if (!BuildMarkerIsSet(marker.c_str())) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Backend/"
|
||||||
|
<< backend
|
||||||
|
<< " names any of kMGPipeSubsystem{Framebuffer, TextureResources, "
|
||||||
|
"Samplers, Programs}, so this backend does not honour P4a's mask "
|
||||||
|
"and cannot refuse a dependency inside it. D-K2's fourth row "
|
||||||
|
"(bit 10 requires bit 11, ID-15) lives in the texture family's "
|
||||||
|
"Resolve*SubsystemArm beside the bit-11-requires-bit-10 and "
|
||||||
|
"bit-8-requires-bit-7 refusals, which is P4a package D's file; "
|
||||||
|
"this control arms itself when that lands. The lane itself is "
|
||||||
|
"not wasted: the library came up under 0x5ff, which on a tree "
|
||||||
|
"with no P4a arm is P3a's mask plus one inert bit, and a mask "
|
||||||
|
"that aborted a bring-up would have failed this entry before "
|
||||||
|
"the skip.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RunTheWorkload();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
|
||||||
|
<< "the workload left a GL error behind on the refused lane, which would mean the "
|
||||||
|
"refusal did not fall back cleanly to the legacy arm";
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
|
||||||
|
ASSERT_FALSE(log.empty())
|
||||||
|
<< "the library wrote nothing to " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ", so the refusal cannot be read back. MOBILEGL_LOG_FILE_PATH is the only channel "
|
||||||
|
"this module has for the library's own report.";
|
||||||
|
// The same line shape as the 0x9ff arm, with the two bits' roles swapped: the bit that
|
||||||
|
// was SET is the texture-resource one and the bit it NEEDED is the sampler one. The
|
||||||
|
// swap is now a REAL difference between the two cases: FindTheRefusalLine requires the
|
||||||
|
// set bit to be named before " is set but " and the needed bit after it (F-v2-m1), so
|
||||||
|
// this call and the 0x9ff one above accept disjoint sentences. Espryt's is
|
||||||
|
// "kMGPipeSubsystemTextureResources (bit 10) is set but kMGPipeSubsystemSamplers
|
||||||
|
// (bit 11) is clear; MGPTextureParams::BuiltinSampler is a SamplerCso handle, only
|
||||||
|
// bit 11 mints sampler CSOs, and the applier's verdict for a null one is
|
||||||
|
// Fatal{ProtocolCorruption} - REFUSING the dependent bit and running the legacy arm.
|
||||||
|
// Set both bits, or clear both" (Managers.cpp, ResolveTextureResourceSubsystemArm).
|
||||||
|
const std::string refusal =
|
||||||
|
FindTheRefusalLine(log, TextureResourceBitSpellings(), SamplerBitSpellings());
|
||||||
|
EXPECT_FALSE(refusal.empty())
|
||||||
|
<< "MOBILEGL_PIPE_PUSH=0x5ff sets the texture resource subsystem (bit 10) without "
|
||||||
|
"the sampler subsystem (bit 11) that MGPTextureParams::BuiltinSampler depends on, "
|
||||||
|
"and no single ERROR line of the library's log both says it REFUSED and names the "
|
||||||
|
"two bits. Only bit 11 mints sampler CSOs, so every set_texture_params emitted "
|
||||||
|
"under this mask would carry a null BuiltinSampler and the applier's Fatal is the "
|
||||||
|
"next thing that happens - which is why this pair is a refusal at bring-up and "
|
||||||
|
"not the harmless mirror of the 0x9ff one. Accepted spellings per bit are the "
|
||||||
|
"constant's name, '(bit 10)' / '(bit 11)', and '0x400' / '0x800'. The log was "
|
||||||
|
<< log.size() << " bytes and is at " << PipeStatsWindow::LibraryLogPath() << ".";
|
||||||
|
if (!refusal.empty()) {
|
||||||
|
std::cout << "[ ObjectSubsystemControl ] refusal line: " << refusal << std::endl;
|
||||||
|
RecordProperty("refusal_line", refusal.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, "green", 0.0,
|
||||||
|
"the sampled draw [refused-texture]"))
|
||||||
|
<< "the refused configuration did not draw what every other lane draws. A refusal is "
|
||||||
|
"supposed to run the LEGACY arm, which is the arm that ships in a pull build - so "
|
||||||
|
"the pixels are the one thing it may not change.";
|
||||||
|
|
||||||
|
ReleaseTheWorkload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// c0f's GATE AND BELT, MEASURED TOGETHER (ID-39, ID-40).
|
||||||
|
//
|
||||||
|
// WHAT WENT WRONG AND WHY IT NEEDS A LANE. P4a's four families were wired without the
|
||||||
|
// gate P3a's buffers have had since PipeFill.cpp ~656: emission required the family's bit
|
||||||
|
// and nothing else. On Magma, which registers no MGPipeResourceOps, the client therefore
|
||||||
|
// emitted, THE APPLIER ACCEPTED, the client cleared its dirty flags on that acceptance -
|
||||||
|
// and Magma's legacy path then found nothing to upload. Sixty-six DirectVulkan cases went
|
||||||
|
// red at once, all texture-upload-shaped, and every one of them was green at 0x1ff. The
|
||||||
|
// fix has two halves that are deliberately independent: the client's gate (bit N AND
|
||||||
|
// wired AND a backend registered the ops) and the applier's belt (every P4a entry point
|
||||||
|
// returns accepted = false and counts RefusedNoConsumer when none did).
|
||||||
|
//
|
||||||
|
// THE ASSERTION IS THAT THE BELT NEVER FIRES, and it is the same assertion on both
|
||||||
|
// backends, which is what makes it worth having twice:
|
||||||
|
//
|
||||||
|
// no-consumer (DirectVulkan, 0x1fff): the belt is the SAFETY NET. A non-zero count here
|
||||||
|
// means a record reached the applier on a backend with no consumer - i.e. the client
|
||||||
|
// gate leaked and only the belt stopped the dirty flag from being cleared. That is
|
||||||
|
// ID-39's bug caught one layer later, and it is invisible in these pixels because the
|
||||||
|
// belt does its job; the 66 red cases were in another suite entirely.
|
||||||
|
// consumer (DirectGLES, 0x1fff): the CONTROL. Espryt registers the ops, so no entry
|
||||||
|
// point may take the no-consumer arm at all. Without this lane a green above could
|
||||||
|
// also mean "nothing is ever emitted anywhere", which is exactly what a gate that was
|
||||||
|
// accidentally always-false would look like.
|
||||||
|
//
|
||||||
|
// A DELTA, not an absolute: the counter is process-global and other cases in this binary
|
||||||
|
// run before this one. MGPipeApplierReset also zeroes it, so a count that went DOWN is
|
||||||
|
// read as "the applier was reset and everything since is `after`" rather than as an
|
||||||
|
// underflow.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(ObjectSubsystemControlScenario, TheAppliersNoConsumerBeltNeverFiresBehindTheClientsGate) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// needsTheEmitters=false: the assertion is that a counter did NOT move, which is
|
||||||
|
// meaningful before the emitters land as well as after - and on the no-consumer lane
|
||||||
|
// it is meaningful precisely BECAUSE nothing may be emitted there.
|
||||||
|
SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false);
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
if (!LaneIsAConsumerLane(m_lane)) {
|
||||||
|
GTEST_SKIP() << "runs only in the two consumer lanes (MGITEST_OBJECT_SUBSYSTEM_LANE="
|
||||||
|
<< kLaneConsumer << " / " << kLaneNoConsumer
|
||||||
|
<< "), which pin MOBILEGL_PIPE_PUSH at the phase default on the two "
|
||||||
|
"backends. Every other lane configures a mask or a backend whose "
|
||||||
|
"emission shape is a different question.";
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long long before = 0;
|
||||||
|
if (!PeekPipeApplierRefusedNoConsumer(&before)) {
|
||||||
|
GTEST_SKIP() << "MGPipeApplierState::RefusedNoConsumer is out of reach here: there "
|
||||||
|
"is no applier in a PULL build (it is #if MOBILEGL_PIPE_PUSH), and "
|
||||||
|
"on Android this module links the shipping libMobileGL.so built "
|
||||||
|
"-fvisibility=hidden. 'Could not look' is not 'did not fire'.";
|
||||||
|
}
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RunTheWorkload();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
|
||||||
|
<< "the workload left a GL error behind on the " << m_lane << " lane";
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
unsigned long long after = 0;
|
||||||
|
ASSERT_TRUE(PeekPipeApplierRefusedNoConsumer(&after))
|
||||||
|
<< "the counter could be read before the workload and not after it";
|
||||||
|
// Down means MGPipeApplierReset ran inside the window, so everything still counted is
|
||||||
|
// what happened since - which is the number this case is about either way.
|
||||||
|
const unsigned long long fired = after >= before ? after - before : after;
|
||||||
|
|
||||||
|
std::cout << "[ ObjectSubsystemControl ] " << m_lane
|
||||||
|
<< " lane: applier RefusedNoConsumer " << before << " -> " << after
|
||||||
|
<< " over the workload (delta " << fired << ")" << std::endl;
|
||||||
|
RecordProperty("refused_no_consumer_delta", static_cast<int>(fired));
|
||||||
|
|
||||||
|
EXPECT_EQ(fired, 0u)
|
||||||
|
<< "the applier's no-consumer BELT fired " << fired
|
||||||
|
<< " time(s) during this workload on the " << m_lane
|
||||||
|
<< " lane. The belt exists so that a P4a record arriving on a backend that "
|
||||||
|
"registered no MGPipeResourceOps is refused rather than accepted - and an "
|
||||||
|
"accepted record is what makes the client clear the dirty flags whose texels "
|
||||||
|
"nobody then uploads (ID-39: sixty-six DirectVulkan cases, all texture-upload "
|
||||||
|
"shaped). A non-zero count means the CLIENT'S GATE let an emission through and "
|
||||||
|
"only the belt caught it: the two are supposed to agree, and PipeFill's "
|
||||||
|
"P4aFamilyHasItsConsumer() is where they stopped.";
|
||||||
|
|
||||||
|
// The pixels, on both lanes, for the reason every arm of this file asserts them: a
|
||||||
|
// backend running its legacy path because no consumer is registered must draw exactly
|
||||||
|
// what a backend running the handle arm draws.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, "green", 0.0,
|
||||||
|
"the sampled draw [" + m_lane + "]"))
|
||||||
|
<< "the " << m_lane
|
||||||
|
<< " lane did not draw what every other lane draws, so whatever the counter says, "
|
||||||
|
"this configuration is not running the workload correctly.";
|
||||||
|
|
||||||
|
ReleaseTheWorkload();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aFinalFixScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE THREE FINDINGS OF THE P4a FINAL WHOLE-DIFF REVIEW (final-review-v1.md C-1, C-2,
|
||||||
|
// M-A), each pinned by the public-GL sequence that was red on the tree the review read and is
|
||||||
|
// green with its fix. Every sequence here is legal GL and none of the 80-odd scenarios before
|
||||||
|
// this file drove it, which is how two criticals shipped through a green gate.
|
||||||
|
//
|
||||||
|
// C-1 The client never passed the applier the LEVEL a respecify redefines, so every per-level
|
||||||
|
// glTexImage*D / glGenerateMipmap grow took the applier's whole-resource arm and dropped
|
||||||
|
// EVERY pending upload of the texture - including a level the applier had already
|
||||||
|
// accepted and whose client-side dirty flag was therefore already clear (D-D5 step 1).
|
||||||
|
// Nobody owed those texels any more. The window is "accepted but not yet consumed":
|
||||||
|
// a verb the texture is not reached by (a draw with another texture) drains the level
|
||||||
|
// into the applier, Espryt does not sync the texture, and the next level definition eats
|
||||||
|
// the entry. Two hazard cases (a level-1 definition, a glGenerateMipmap) read a black
|
||||||
|
// level 0 on the handle arm; the three controls beside them (no verb between, level 0
|
||||||
|
// consumed first, an immediate generate) are red on every arm, which is what pins the
|
||||||
|
// window rather than the mip path.
|
||||||
|
// C-2 A dead-but-not-recycled texture handle still resolved to the freed ITextureObject*
|
||||||
|
// inside the client's drain: the death helper freed the slot without telling the emitter,
|
||||||
|
// the drain list kept the level, and the next verb's drain called virtual
|
||||||
|
// GetStorageType() on freed memory - `glTexImage2D; glDeleteTextures; <any verb>` was a
|
||||||
|
// SIGABRT ("pure virtual method called") at the shipping default mask. The same
|
||||||
|
// delete-then-use shape is driven for every kind P4a mints (renderbuffer, sampler object,
|
||||||
|
// program, framebuffer) and for a slot recycled straight after the death (ABA), on both
|
||||||
|
// backends: the death path is backend-neutral by ruling (ID-8) and the DirectVulkan lane
|
||||||
|
// must see it too.
|
||||||
|
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
|
||||||
|
// dead: the applier never saw a texture become image-bound, the metadata respecify
|
||||||
|
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
|
||||||
|
// neither prevented nor counted. The case here reads the applier's record around a
|
||||||
|
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
|
||||||
|
// standing beside it, and the picture after the transition is the texels that upload
|
||||||
|
// carried.
|
||||||
|
//
|
||||||
|
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
|
||||||
|
// public-GL half (P4aSeamAuditScenario.cpp's shape): a pull build or a backend with no P4a
|
||||||
|
// consumer holds no record to read, and skipping the whole case there would delete the verdict
|
||||||
|
// those lanes carry. The C-1 and M-A cases assert their pictures on DirectGLES only - Espryt is
|
||||||
|
// the one consumer of the texture records this phase wires, so on any other backend the handle
|
||||||
|
// arm is inert by design and the picture proves nothing about it.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/P4aFinalFixPeek.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = aPos * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texture(uTex, vUv); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
class P4aFinalFixScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
|
||||||
|
glGenBuffers(1, &m_quadBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
|
||||||
|
// The "other" texture: a complete, single-level white texture, so a draw that
|
||||||
|
// samples it is a verb the texture under test is not reached by.
|
||||||
|
m_other = MakeLevel0(255, 255, 255, /*maxLevel=*/0);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (m_other != 0) glDeleteTextures(1, &m_other);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The C-1 and M-A pictures are about Espryt's consumption of the texture records;
|
||||||
|
// Magma registers no consumer for the P4a families (c0f), so the handle arm is inert
|
||||||
|
// there by design and a green picture proves nothing about the finding. Marks the
|
||||||
|
// case skipped; the caller tests IsSkipped() and returns.
|
||||||
|
void SkipUnlessEspryt(const char* what) {
|
||||||
|
if (Gl().BackendName() == "DirectGLES") return;
|
||||||
|
GTEST_SKIP() << what << " is consumed by DirectGLES only; backend is " << Gl().BackendName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::uint8_t> Solid(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
std::vector<std::uint8_t> texels(static_cast<std::size_t>(size) * size * 4);
|
||||||
|
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||||
|
texels[i] = r;
|
||||||
|
texels[i + 1] = g;
|
||||||
|
texels[i + 2] = b;
|
||||||
|
texels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
return texels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 4x4 level 0 of one colour, NEAREST_MIPMAP_NEAREST with the level range clamped
|
||||||
|
// to `maxLevel`, so a single-level texture is complete and a chain is complete once
|
||||||
|
// its levels exist.
|
||||||
|
static GLuint MakeLevel0(std::uint8_t r, std::uint8_t g, std::uint8_t b, int maxLevel, int size = 4) {
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(size, r, g, b);
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DefineLevel1(GLuint texture, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(2, r, g, b);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A full-viewport draw sampling `texture` on unit 0 through `program` (the fixture's
|
||||||
|
// by default). The viewport is far larger than the 4x4 base level, so this is
|
||||||
|
// MAGNIFICATION and reads LEVEL 0 whatever the chain holds above it.
|
||||||
|
Image DrawSampled(GLuint texture, GLuint program = 0) {
|
||||||
|
if (program == 0) program = m_program;
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
Gl().EndFrame();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::AssertionResult Mostly(const Image& image, const char* color, const std::string& when) {
|
||||||
|
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset, image.Height() - kInset, color,
|
||||||
|
0.0, when);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Report(const char* caseName, const Image& image) {
|
||||||
|
const char* mask = std::getenv("MOBILEGL_PIPE_PUSH");
|
||||||
|
const int cx = image.Width() / 2;
|
||||||
|
const int cy = image.Height() / 2;
|
||||||
|
std::cout << "[ P4aFinalFix ] case=" << caseName << " backend=" << Gl().BackendName()
|
||||||
|
<< " MOBILEGL_PIPE_PUSH=" << (mask ? mask : "(unset)") << " centre=" << image.At(cx, cy)
|
||||||
|
<< " (" << image.ColorName(cx, cy) << ")" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The white-box gate of the M-A case: true when the applier holds a record for the
|
||||||
|
// texture in this process. Prints the decline.
|
||||||
|
bool RecordIsReadable(unsigned glTextureName, const char* what, PipeTextureResourceRecordPeek* out) {
|
||||||
|
if (PeekPipeTextureResourceRecord(glTextureName, out)) return true;
|
||||||
|
std::cout << "[ P4aFinalFix ] white-box reading DECLINED for " << what
|
||||||
|
<< ": the applier holds no record for texture " << glTextureName
|
||||||
|
<< " (a pull build, or a backend with no P4a consumer); the public-GL half of "
|
||||||
|
"the case still runs"
|
||||||
|
<< std::endl;
|
||||||
|
RecordProperty("p4a_finalfix_white_box", "declined");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_quadBuffer = 0;
|
||||||
|
GLuint m_other = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// C-1: a per-level definition around a verb the texture is not reached by
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
// THE HAZARD. L0's upload is accepted at the unrelated draw's validate point (the client
|
||||||
|
// clears its flag), Espryt never syncs T there (it is bound nowhere), then the level-1
|
||||||
|
// definition respecifies the resource. Before the fix that respecify carried no level and
|
||||||
|
// the applier dropped every pending upload; level 0 was allocated undefined.
|
||||||
|
TEST_F(P4aFinalFixScenario, PerLevelDefinitionAcrossAnUnrelatedDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("C-1's per-level respecify");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
const Image unrelated = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||||
|
DefineLevel1(texture, 255, 0, 0);
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("PerLevelDefinitionAcrossAnUnrelatedDraw", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red",
|
||||||
|
"level 0 after a level-1 definition that followed a draw the texture was not "
|
||||||
|
"reached by - its accepted-but-unconsumed upload was dropped by the whole-"
|
||||||
|
"resource arm"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL: both levels defined before any verb; both are pending at the first sync.
|
||||||
|
TEST_F(P4aFinalFixScenario, ConsecutiveDefinitionsNoVerbBetween) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("C-1's per-level respecify");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
DefineLevel1(texture, 255, 0, 0);
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ConsecutiveDefinitionsNoVerbBetween", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "level 0 with both levels defined back to back"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL: level 0 is consumed by Espryt (T is sampled) before level 1 is defined.
|
||||||
|
TEST_F(P4aFinalFixScenario, LevelZeroConsumedBeforeLevelOne) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("C-1's per-level respecify");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
const Image first = DrawSampled(texture);
|
||||||
|
EXPECT_TRUE(Mostly(first, "red", "level 0 alone"));
|
||||||
|
DefineLevel1(texture, 255, 0, 0);
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("LevelZeroConsumedBeforeLevelOne", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "level 0 after level 1 was added to a synced texture"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE HAZARD, glGenerateMipmap flavour: the frontend grows the level chain (one
|
||||||
|
// AllocateStorage -> respecify per level) BEFORE the backend generate runs, with level 0
|
||||||
|
// accepted-but-unconsumed. The driver then built the chain from an undefined level 0.
|
||||||
|
TEST_F(P4aFinalFixScenario, GenerateMipmapAcrossAnUnrelatedDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("C-1's per-level respecify");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
|
||||||
|
const Image unrelated = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("GenerateMipmapAcrossAnUnrelatedDraw", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red",
|
||||||
|
"level 0 after a glGenerateMipmap that followed a draw the texture was not "
|
||||||
|
"reached by"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL for the generate: no verb between the upload and the generate.
|
||||||
|
TEST_F(P4aFinalFixScenario, GenerateMipmapImmediately) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("C-1's per-level respecify");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("GenerateMipmapImmediately", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "level 0 after an immediate glGenerateMipmap"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// C-2: delete-then-use, for every kind P4a mints, on both backends
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
// A level goes dirty, the texture dies before any verb, and the next verb's drain walks
|
||||||
|
// the entry. Before the fix the emitter resolved the dead handle to the freed object and
|
||||||
|
// the drain called a virtual on it: SIGABRT in the first round. Eight rounds, and the
|
||||||
|
// lane registered with MALLOC_PERTURB_ scribbles every freed block so a resolved-but-
|
||||||
|
// dead pointer faults rather than reads the object's ghost.
|
||||||
|
TEST_F(P4aFinalFixScenario, ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (int round = 0; round < 8; ++round) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &texture); // the last reference: the frontend object dies here
|
||||||
|
// Something else is allocated between the death and the drain, so the freed
|
||||||
|
// storage is not simply re-handed to the next object.
|
||||||
|
std::vector<std::uint8_t> churn(4096 + round * 1024, static_cast<std::uint8_t>(round));
|
||||||
|
(void)churn;
|
||||||
|
const Image image = DrawSampled(m_other); // the validate point: the drain runs here
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
if (round == 0) Report("ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "white", "the draw after a dirty texture died"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABA: the slot the dead texture held is handed straight to the next texture (the free
|
||||||
|
// list is LIFO). The new texture's picture must be its own, and the dead one's drain
|
||||||
|
// entry must not be replayed onto it.
|
||||||
|
TEST_F(P4aFinalFixScenario, ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
{
|
||||||
|
GLuint dead = 0;
|
||||||
|
glGenTextures(1, &dead);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, dead);
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &dead); // dirty, dead, no verb between
|
||||||
|
}
|
||||||
|
const GLuint successor = MakeLevel0(0, 0, 255, /*maxLevel=*/0, /*size=*/8);
|
||||||
|
const Image image = DrawSampled(successor);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "blue", "the successor of a dead dirty texture on the recycled slot"));
|
||||||
|
const Image other = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(other, "white", "an unrelated draw after the recycled slot was used"));
|
||||||
|
GLuint cleanup = successor;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A renderbuffer with defined storage, attached, cleared through its framebuffer, then
|
||||||
|
// both die before the next verb.
|
||||||
|
TEST_F(P4aFinalFixScenario, ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
GLuint renderbuffer = 0;
|
||||||
|
glGenRenderbuffers(1, &renderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 8, 8);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
glViewport(0, 0, 8, 8);
|
||||||
|
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
const Image cleared = ReadPixels(8, 8);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(cleared, 0, 8, 0, 8, "green", 0.0, "the renderbuffer after the clear"));
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &renderbuffer); // the attachment's last reference went with the FBO
|
||||||
|
const Image image = DrawSampled(m_other);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "white", "the draw after a renderbuffer and its framebuffer died"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sampler object bound to the unit the draw samples through, deleted while bound: GL
|
||||||
|
// unbinds it from every unit at glDeleteSamplers, and the texture's own parameters apply
|
||||||
|
// again. Both draws must be the texture's colour.
|
||||||
|
TEST_F(P4aFinalFixScenario, ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
GLuint sampler = 0;
|
||||||
|
glGenSamplers(1, &sampler);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindSampler(0, sampler);
|
||||||
|
const Image withSampler = DrawSampled(texture);
|
||||||
|
EXPECT_TRUE(Mostly(withSampler, "red", "the draw through the bound sampler object"));
|
||||||
|
glDeleteSamplers(1, &sampler); // bound: unbound by the delete, then dies
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "the draw after the bound sampler object died"));
|
||||||
|
glBindSampler(0, 0);
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second program, in use when it is deleted (GL keeps it alive until it is no longer
|
||||||
|
// current), then released by a glUseProgram of the fixture's program: it dies there, and
|
||||||
|
// the draw that follows runs through the survivor.
|
||||||
|
TEST_F(P4aFinalFixScenario, AProgramDeletedWhileInUseLeavesTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
const GLuint second = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(second, 0u) << error;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
const Image throughSecond = DrawSampled(texture, second);
|
||||||
|
EXPECT_TRUE(Mostly(throughSecond, "red", "the draw through the second program"));
|
||||||
|
glDeleteProgram(second); // current: flagged for deletion, still very much alive
|
||||||
|
const Image stillCurrent = DrawSampled(texture, second);
|
||||||
|
EXPECT_TRUE(Mostly(stillCurrent, "red", "the draw through a program flagged for deletion"));
|
||||||
|
const Image image = DrawSampled(texture); // glUseProgram(m_program): the second dies here
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("AProgramDeletedWhileInUseLeavesTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "the draw after the deleted program was released"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A framebuffer handed to the server BY NAME (a DSA clear emits a Named record, ID-19(c))
|
||||||
|
// and deleted before the next verb; its attachment lives on and carries the clear.
|
||||||
|
TEST_F(P4aFinalFixScenario, AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
glClearNamedFramebufferfv(fbo, GL_COLOR, 0, green);
|
||||||
|
glDeleteFramebuffers(1, &fbo); // unbound and named: dies here
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "green", "the attachment of a framebuffer that died after a DSA clear"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// M-A: an image bind after the allocation is a metadata respecify with the hint set
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
// glTexStorage2D (immutable: no later respecify to ride), a red upload consumed by a draw,
|
||||||
|
// then a blue upload drained by a verb the texture is not reached by (accepted, standing
|
||||||
|
// in the applier's pending set), then glBindImageTexture. The bind must reach the record
|
||||||
|
// as a metadata update - ImageBindableHint 1, the pending upload still standing - and the
|
||||||
|
// draw after it must show the blue that upload carried through the widened carrier the
|
||||||
|
// hint schedules.
|
||||||
|
TEST_F(P4aFinalFixScenario, AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("M-A's image-bindable hint");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
if (maxImageUnits < 1) {
|
||||||
|
GTEST_SKIP() << "no image units";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
|
||||||
|
// hint reached it is re-minted image-bindable at the bind and its levels replayed
|
||||||
|
// from the client's shadow - one remint pull, counted. Arming the counter here is
|
||||||
|
// what makes it readable without a stats-enabled lane.
|
||||||
|
unsigned long long pullsBefore = 0;
|
||||||
|
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||||
|
const std::vector<std::uint8_t> red = Solid(4, 255, 0, 0);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const Image before = DrawSampled(texture); // allocated and consumed, NOT image-bindable
|
||||||
|
EXPECT_TRUE(Mostly(before, "red", "the immutable texture before the image bind"));
|
||||||
|
|
||||||
|
PipeTextureResourceRecordPeek record{};
|
||||||
|
const bool readable = RecordIsReadable(texture, "M-A's image-bindable hint", &record);
|
||||||
|
if (readable) {
|
||||||
|
EXPECT_EQ(record.ImageBindableHint, 0u) << "nothing has image-bound this texture yet";
|
||||||
|
EXPECT_EQ(record.PendingUploads, 0u) << "the red upload was consumed by the draw";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A blue upload, drained by a verb that does not reach T: accepted, unconsumed.
|
||||||
|
const std::vector<std::uint8_t> blue = Solid(4, 0, 0, 255);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const Image unrelated = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
|
||||||
|
if (readable) {
|
||||||
|
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
|
||||||
|
EXPECT_EQ(record.PendingUploads, 1u) << "the blue upload was not drained into the applier";
|
||||||
|
}
|
||||||
|
const unsigned long long serialBeforeBind = record.Serial;
|
||||||
|
unsigned long long uploadsBeforeBind = 0;
|
||||||
|
const bool uploadsReadable = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeBind);
|
||||||
|
|
||||||
|
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
|
||||||
|
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
|
||||||
|
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
|
||||||
|
// every defined level from the shadow (the remint pull the counter below counts), so
|
||||||
|
// the standing upload is consumed by that regeneration here and the picture that
|
||||||
|
// follows is blue whatever the metadata respecify did to the record - the KEPT
|
||||||
|
// property is proved further down, on a texture no remint stands in front of.
|
||||||
|
(void)uploadsBeforeBind;
|
||||||
|
(void)uploadsReadable;
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
if (readable) {
|
||||||
|
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
|
||||||
|
EXPECT_EQ(record.ImageBindableHint, 1u)
|
||||||
|
<< "glBindImageTexture did not reach the applier's record as ImageBindableHint";
|
||||||
|
EXPECT_NE(record.BindMask & (1u << 6), 0u) << "kMGPipeBindShaderImage was not produced";
|
||||||
|
EXPECT_GT(record.Serial, serialBeforeBind) << "the metadata respecify moved no serial";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "blue", "the texture after the image bind that followed an unconsumed upload"));
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
unsigned long long pullsAfter = 0;
|
||||||
|
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsAfter)) {
|
||||||
|
EXPECT_EQ(pullsAfter, pullsBefore + 1)
|
||||||
|
<< "the re-mint of a texture allocated before its hint was not counted as a remint pull "
|
||||||
|
"(trp= on the stats line is ROADMAP open question 2's number)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE PREVENTION HALF, measured the other way round: a texture whose hint arrives at
|
||||||
|
// the bind, BEFORE its first sync, is allocated image-bindable up front and pulls
|
||||||
|
// nothing - the counter does not move.
|
||||||
|
GLuint early = 0;
|
||||||
|
glGenTextures(1, &early);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, early);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindImageTexture(0, early, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); // before any sync
|
||||||
|
const Image earlyImage = DrawSampled(early);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(Mostly(earlyImage, "red", "a texture image-bound before its first sync"));
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
unsigned long long pullsEarly = 0;
|
||||||
|
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsEarly)) {
|
||||||
|
EXPECT_EQ(pullsEarly, pullsAfter)
|
||||||
|
<< "a texture whose hint preceded its first sync was still re-minted (the prevention "
|
||||||
|
"half of the hint did not fire)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE METADATA RESPECIFY KEEPS A STANDING UPLOAD, end to end and with no remint in the
|
||||||
|
// way: `early` is image-bindable already, so a NEW sticky bit reaching it - the
|
||||||
|
// RENDER_TARGET bit a DSA attachment produces at its setter (a Named record, ID-19(c)),
|
||||||
|
// with no sync of the texture in between - is a pure metadata update. The blue upload
|
||||||
|
// drained before it must still stand in the record afterwards (or, if a sync did run,
|
||||||
|
// have been uploaded rather than dropped) and reach the driver at the next draw.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, early);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const Image unrelatedAgain = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(unrelatedAgain, "white", "the unrelated draw"));
|
||||||
|
PipeTextureResourceRecordPeek earlyRecord{};
|
||||||
|
const bool earlyReadable = PeekPipeTextureResourceRecord(early, &earlyRecord);
|
||||||
|
if (earlyReadable) {
|
||||||
|
EXPECT_EQ(earlyRecord.PendingUploads, 1u) << "the blue upload was not drained into the applier";
|
||||||
|
}
|
||||||
|
const unsigned long long earlySerialBefore = earlyRecord.Serial;
|
||||||
|
unsigned long long uploadsBeforeAttach = 0;
|
||||||
|
const bool uploadsCounted = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeAttach);
|
||||||
|
GLuint namedFbo = 0;
|
||||||
|
glCreateFramebuffers(1, &namedFbo);
|
||||||
|
glNamedFramebufferTexture(namedFbo, GL_COLOR_ATTACHMENT0, early, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
if (earlyReadable) {
|
||||||
|
ASSERT_TRUE(PeekPipeTextureResourceRecord(early, &earlyRecord));
|
||||||
|
EXPECT_NE(earlyRecord.BindMask & (1u << 7), 0u)
|
||||||
|
<< "the DSA attachment did not produce kMGPipeBindRenderTarget";
|
||||||
|
EXPECT_GT(earlyRecord.Serial, earlySerialBefore) << "the mask move reached the record as no respecify";
|
||||||
|
unsigned long long uploadsAfterAttach = 0;
|
||||||
|
if (earlyRecord.PendingUploads == 0 && uploadsCounted &&
|
||||||
|
PeekPipeStatsTextureUploadEmissions(&uploadsAfterAttach)) {
|
||||||
|
EXPECT_GT(uploadsAfterAttach, uploadsBeforeAttach)
|
||||||
|
<< "the standing upload vanished from the record without Espryt uploading anything: "
|
||||||
|
"the metadata respecify dropped it";
|
||||||
|
} else {
|
||||||
|
EXPECT_EQ(earlyRecord.PendingUploads, 1u)
|
||||||
|
<< "the metadata respecify dropped the pending upload standing beside it";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const Image earlyAfter = DrawSampled(early);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
EXPECT_TRUE(Mostly(earlyAfter, "blue", "the upload that stood across a metadata respecify"));
|
||||||
|
glDeleteFramebuffers(1, &namedFbo);
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
GLuint cleanupEarly = early;
|
||||||
|
glDeleteTextures(1, &cleanupEarly);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,803 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aSeamAuditScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE FOUR SEAMS THE P4a FABLE SEAM AUDIT PROVED, each pinned by the public-GL sequence
|
||||||
|
// (or the white-box reading) that was red on the tree the audit read and is green with its fix.
|
||||||
|
//
|
||||||
|
// The audit's rule, which every case here is an instance of: 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. A record whose field has a setter no shutter sees is a stale record with nothing
|
||||||
|
// to refuse - no census line, no Fatal, a wrong picture or a permanent silent fallback - which is
|
||||||
|
// why none of the 80 scenarios before this file caught any of the four (Tracker.h carries the
|
||||||
|
// record-field -> setter -> shutter table this file is the gate for).
|
||||||
|
//
|
||||||
|
// F-3 set_framebuffer_state INLINES an attachment's format (D-C1) and a storage redefinition of
|
||||||
|
// an ATTACHED texture or renderbuffer moved nothing bit 11 read: Espryt's handle arm then
|
||||||
|
// answered its alpha-widening / snorm-clamp / integer masks from the stale copy. AND THE
|
||||||
|
// PRE-HANDLE ARM WAS NOT FRESH EITHER, which these cases found on the 0x1ff / 0 / pull
|
||||||
|
// lanes: a redefinition that keeps the driver id (mutable texture storage regenerated in
|
||||||
|
// place, a renderbuffer re-storaged in place) moves neither the framebuffer's frontend
|
||||||
|
// versions nor the backend-id generation the FBO memo reads, so SyncToBackend never
|
||||||
|
// re-ran and the masks stayed on both arms. The texture half is fixed on both arms OF A
|
||||||
|
// PUSH BUILD (an in-place regeneration now takes the same generation a re-mint takes -
|
||||||
|
// compiled under MOBILEGL_PIPE_PUSH because G1 keeps the pull library byte-identical to
|
||||||
|
// the P4a baseline, so the pull build keeps the pre-P4a hole until the fix lands on dev on
|
||||||
|
// its own and the texture cases decline by name there); the renderbuffer half only on the
|
||||||
|
// handle arm, where the resource record carries the re-storage - on the pre-handle arm a
|
||||||
|
// renderbuffer's twin is only ever reached from inside the FBO walk the memo skips (D-D2's
|
||||||
|
// documented hole, pre-P4a code), so that case asserts on the handle arm and declines by
|
||||||
|
// name elsewhere. Three cases, both directions, texture and renderbuffer. DirectGLES only:
|
||||||
|
// the masks are Espryt's substitution machinery.
|
||||||
|
// F-1 set_sampler_views is resolved for the PROGRAM IN USE and bit 12's shutter read no program
|
||||||
|
// input, so a glUseProgram alone never re-emitted it; E's record epoch (the two set serials)
|
||||||
|
// then kept the program-independent texture sync list from ever rebuilding, and a texture
|
||||||
|
// bound to an EMPTY slot under one program was never synced for the next. One case, both
|
||||||
|
// backends, red as a black quad.
|
||||||
|
// F-2 bit 14's plain-program arm mixed a per-program COUNTER two programs routinely share, so a
|
||||||
|
// program switch never re-emitted set_shader_images and the window stayed the previous
|
||||||
|
// program's - and E's SD-4 (a buffer image never reaching the record at all) is the same
|
||||||
|
// bit through the null -> program transition. One case, white-box, both backends run it.
|
||||||
|
// F-4 BindCurrentUnitSamplers' record arm looked a CONTENT-addressed CSO handle up in the
|
||||||
|
// IDENTITY-keyed twin registry: a miss on every draw, hidden because the pre-handle program
|
||||||
|
// pass bound the same values. One case, white-box: the unit's driver sampler must be the
|
||||||
|
// CSO's own twin.
|
||||||
|
//
|
||||||
|
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
|
||||||
|
// public-GL half (the shape TextureParamsWithoutASamplerViewScenario.cpp argues for): a pull
|
||||||
|
// build, Magma, or a lane whose mask leaves Espryt's sampler family on its legacy arm has no
|
||||||
|
// record arm to assert about, and skipping the whole case there would delete the public-GL
|
||||||
|
// verdict those lanes carry. Every decline is printed and RecordProperty'd.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/P4aSeamPeek.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kSize = 16;
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
|
||||||
|
// No attributes: the quad's corners come from gl_VertexID, so a bare VAO is all a draw
|
||||||
|
// needs and no vertex-input state can enter any of the sequences below.
|
||||||
|
constexpr const char* kQuadVS = R"(#version 330 core
|
||||||
|
void main() {
|
||||||
|
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
||||||
|
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kColorFS = R"(#version 330 core
|
||||||
|
uniform vec4 uColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = uColor; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// texelFetch, so WHICH image the unit holds is the whole answer and no filter, wrap or
|
||||||
|
// completeness rule can explain a colour away.
|
||||||
|
constexpr const char* kFetchFS = R"(#version 330 core
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// texture() at (1.5, 1.5): outside the image on both axes, so the WRAP mode of whichever
|
||||||
|
// sampler applies - the unit's sampler object or the texture's built-in one - decides
|
||||||
|
// whether the texel or the border colour comes back.
|
||||||
|
constexpr const char* kOutsideSampleFS = R"(#version 330 core
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texture(uTex, vec2(1.5, 1.5)); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// F-2 / SD-4: two compute programs over BUFFER images (the SD-4 shape - the kind E's I2
|
||||||
|
// flip found never reached the record at all), the second naming one unit more than the
|
||||||
|
// first, and both image-unit counters equal (layout(binding) assigns the unit at link, so
|
||||||
|
// neither program ever moves it through glUniform1i).
|
||||||
|
constexpr const char* kOneBufferImageCS = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 0, r32ui) writeonly uniform uimageBuffer i0;
|
||||||
|
void main() { imageStore(i0, 0, uvec4(7u, 0u, 0u, 0u)); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kTwoBufferImagesCS = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 0, r32ui) readonly uniform uimageBuffer i0;
|
||||||
|
layout(binding = 1, r32ui) writeonly uniform uimageBuffer i1;
|
||||||
|
void main() { imageStore(i1, 0, imageLoad(i0, 0) + uvec4(2u, 0u, 0u, 0u)); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
class P4aSeamAuditScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The F-3 cases are about Espryt's four cross-object masks, which are its own
|
||||||
|
// substitution machinery (three-channel widening, SNORM/UNORM clamp, integer outputs);
|
||||||
|
// Magma answers the same GL questions on its own terms, so a verdict there would pin
|
||||||
|
// a coincidence - the same reason SnormAttachment and ThreeChannelAttachment skip.
|
||||||
|
// Marks the case skipped; the caller tests IsSkipped() and returns (GTEST_SKIP is a
|
||||||
|
// void statement, so it cannot return the verdict itself).
|
||||||
|
void SkipUnlessEspryt(const char* what) {
|
||||||
|
if (Gl().BackendName() == "DirectGLES") return;
|
||||||
|
GTEST_SKIP() << what << " is a DirectGLES handle-arm seam; backend is " << Gl().BackendName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrawQuad() { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
|
||||||
|
|
||||||
|
// One pixel's RGBA as floats, from the currently bound READ framebuffer.
|
||||||
|
static void ReadPixelFloat(int x, int y, float out[4]) {
|
||||||
|
out[0] = out[1] = out[2] = out[3] = -1.0f;
|
||||||
|
glReadPixels(x, y, 1, 1, GL_RGBA, GL_FLOAT, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The white-box gate shared by F-2 and F-4: true when Espryt's sampler family is on
|
||||||
|
// its handle arm in this process, so the applier's unit sets are consumed and an
|
||||||
|
// assertion about them can only be red for its own reason. Prints the decline.
|
||||||
|
bool SamplerHandleArmIsLive(const char* what) {
|
||||||
|
bool live = false;
|
||||||
|
std::string why;
|
||||||
|
if (!PeekEsprytSamplerHandleArmIsLive(&live)) {
|
||||||
|
why = "the reading cannot be taken here (a pull build, Android, or a backend that "
|
||||||
|
"is not DirectGLES)";
|
||||||
|
} else if (!live) {
|
||||||
|
why = "Espryt's sampler family runs its legacy arm in this process "
|
||||||
|
"(MOBILEGL_PIPE_PUSH leaves bit 11 clear or refuses it)";
|
||||||
|
}
|
||||||
|
if (why.empty()) return true;
|
||||||
|
std::cout << "[ P4aSeamAudit ] white-box reading DECLINED for " << what << ": " << why
|
||||||
|
<< "; the public-GL half of the case still runs" << std::endl;
|
||||||
|
RecordProperty("p4a_seam_white_box", "declined");
|
||||||
|
RecordProperty("p4a_seam_white_box_reason", why);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 2x2 RGBA8 texture filled with one colour, NEAREST, single level - complete under
|
||||||
|
// every rule, so nothing about completeness can enter the F-1 and F-4 sequences.
|
||||||
|
static GLuint MakeSolidTexture2D(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
std::uint8_t texels[2 * 2 * 4];
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
texels[i * 4 + 0] = r;
|
||||||
|
texels[i * 4 + 1] = g;
|
||||||
|
texels[i * 4 + 2] = b;
|
||||||
|
texels[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComputeImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
GLint maxBufferSize = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxBufferSize);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 2 && maxComputeImageUniforms >= 2 && maxBufferSize >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint MakeComputeProgram(const char* source, std::string* outError) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
*outError = std::string("compute shader did not compile: ") + log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
*outError = std::string("compute program did not link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An R32UI buffer texture over a fresh 4-texel buffer, every texel `fill`.
|
||||||
|
static GLuint MakeBufferTexture(GLuint* outBuffer, GLuint fill) {
|
||||||
|
const GLuint texels[4] = {fill, fill, fill, fill};
|
||||||
|
glGenBuffers(1, outBuffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, *outBuffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, sizeof(texels), texels, GL_DYNAMIC_COPY);
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, *outBuffer);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint ReadBufferTexel0(GLuint buffer) {
|
||||||
|
GLuint value = 0xFFFFFFFFu;
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glGetBufferSubData(GL_TEXTURE_BUFFER, 0, sizeof(value), &value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// F-3: a storage redefinition WHILE ATTACHED reaches the framebuffer record
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// GL_SRGB8 is a format Espryt can only render into through its three-channel widening
|
||||||
|
// (llvmpipe reports INCOMPLETE_ATTACHMENT for it natively - ThreeChannelAttachmentScenario
|
||||||
|
// measured the table), so its draw buffer carries the alpha-widened mask: every draw has its
|
||||||
|
// alpha masked off so the stored alpha stays at the 1.0 a three-channel format implies.
|
||||||
|
// Redefine the same attached texture as GL_SRGB8_ALPHA8 and the application owns alpha
|
||||||
|
// again - the mask must clear. On the tree the audit read the record still said SRGB8, the
|
||||||
|
// handle arm kept masking, and the 0.25 this case draws never reached the storage; on the
|
||||||
|
// pre-handle arm the twin regenerated the (mutable) storage on the same driver id, nothing
|
||||||
|
// the FBO memo reads moved, and the masks stayed the same way.
|
||||||
|
|
||||||
|
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedWhileAttachedReachesTheFramebufferRecord) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("F-3");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||||
|
ASSERT_GE(colorLocation, 0);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 texture was refused";
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "an SRGB8 colour attachment must be complete (natively or through the widening)";
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||||
|
|
||||||
|
// Phase 1: the three-channel format. Whatever the draw writes, GL reports alpha 1.0.
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
DrawQuad();
|
||||||
|
float pixel[4];
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||||
|
|
||||||
|
// Phase 2: THE RESPECIFY, while attached, with no re-attach and no rebind of the FBO.
|
||||||
|
// The only thing that moves between the two draws is the texture's storage.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8_ALPHA8 was refused";
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
DrawQuad();
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||||
|
// PUSH BUILDS ONLY, EVERY ARM OF THEM. The pre-handle half of the fix (an in-place
|
||||||
|
// regeneration takes the backend-id generation a re-mint takes) is Espryt code the
|
||||||
|
// pull build would share, and G1 keeps the pull library byte-identical to the P4a
|
||||||
|
// baseline - so it is compiled under MOBILEGL_PIPE_PUSH and the pull build keeps the
|
||||||
|
// pre-P4a hole until the same lines land on dev on their own. The peek returns true
|
||||||
|
// exactly where it could look, which for a case that already skipped off Espryt means
|
||||||
|
// "a push build"; what it writes (is the handle arm live) does not matter here.
|
||||||
|
bool framebufferArmLive = false;
|
||||||
|
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive)) {
|
||||||
|
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||||
|
<< "the draw's alpha never reached a four-channel attachment: the framebuffer record "
|
||||||
|
"(handle arm) or the FBO twin's memo (pre-handle arm) still describes the "
|
||||||
|
"three-channel storage the texture was attached with, so alpha stayed masked off (F-3)";
|
||||||
|
} else {
|
||||||
|
std::cout << "[ P4aSeamAudit ] texture respecify verdict DECLINED on the pull build (the "
|
||||||
|
"in-place regeneration bump is push-only by G1); alpha read "
|
||||||
|
<< pixel[3] << std::endl;
|
||||||
|
RecordProperty("p4a_seam_white_box", "declined");
|
||||||
|
RecordProperty("p4a_seam_white_box_reason", "texture respecify: pull build (G1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The renderbuffer twin. A renderbuffer's three storage setters bump no version at all:
|
||||||
|
// D-D2 closed the RESOURCE record by emitting from the entry point and left the framebuffer
|
||||||
|
// record - and with it the masks - describing the storage it was attached with.
|
||||||
|
TEST_F(P4aSeamAuditScenario, ARenderbufferRestoragedWhileAttachedReachesTheFramebufferRecord) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("F-3");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||||
|
ASSERT_GE(colorLocation, 0);
|
||||||
|
|
||||||
|
GLuint renderbuffer = 0;
|
||||||
|
glGenRenderbuffers(1, &renderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8, kSize, kSize);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8 renderbuffer was refused";
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "an SRGB8 renderbuffer attachment must be complete (natively or through the widening)";
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||||
|
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
DrawQuad();
|
||||||
|
float pixel[4];
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
EXPECT_NEAR(pixel[3], 1.0f, 0.02f) << "a three-channel attachment reports alpha 1.0";
|
||||||
|
|
||||||
|
// THE RE-STORAGE, while attached.
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_SRGB8_ALPHA8, kSize, kSize);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the re-storage to SRGB8_ALPHA8 was refused";
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
DrawQuad();
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
EXPECT_NEAR(pixel[1], 1.0f, 0.05f) << "the draw did not land at all";
|
||||||
|
// THE HANDLE ARM ONLY. On the pre-handle arm a renderbuffer's twin is reached only from
|
||||||
|
// inside the FBO walk, and nothing that walk's memo reads moves on glRenderbufferStorage
|
||||||
|
// - the frontend setters bump no version (D-D2), no framebuffer version sees them, and
|
||||||
|
// the twin that would bump the backend generation is exactly what the memo skips. That
|
||||||
|
// is pre-P4a code and D-D2's documented hole; the resource record is what closes it,
|
||||||
|
// so the verdict is taken where the record is consumed and declined by name elsewhere
|
||||||
|
// (measured: alpha 1.0 on the pull build and at 0x1ff / 0, the mask of the storage the
|
||||||
|
// renderbuffer was attached with).
|
||||||
|
bool framebufferArmLive = false;
|
||||||
|
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive) && framebufferArmLive) {
|
||||||
|
EXPECT_NEAR(pixel[3], 0.25f, 0.02f)
|
||||||
|
<< "the draw's alpha never reached the four-channel renderbuffer: the framebuffer "
|
||||||
|
"record still describes the storage it was attached with (F-3)";
|
||||||
|
} else {
|
||||||
|
std::cout << "[ P4aSeamAudit ] renderbuffer re-storage verdict DECLINED on the pre-handle arm "
|
||||||
|
"(D-D2's documented hole: no frontend version and no backend generation moves on a "
|
||||||
|
"renderbuffer re-storage until the FBO walk the memo skips); alpha read "
|
||||||
|
<< pixel[3] << std::endl;
|
||||||
|
RecordProperty("p4a_seam_white_box", "declined");
|
||||||
|
RecordProperty("p4a_seam_white_box_reason", "renderbuffer re-storage: pre-handle arm (D-D2)");
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &renderbuffer);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mirror direction, four channels -> three, and it needs the driver to READ the stored
|
||||||
|
// alpha because the readback fixup (which consults the frontend) would hide it: after the
|
||||||
|
// respecify to SRGB8 the widening discipline has to hold - the clear puts 1.0 into the
|
||||||
|
// carrier's alpha and the draw is masked away from it - so a GL_DST_ALPHA blend of white sees
|
||||||
|
// 1.0. On a stale record the draw wrote its 0.25 into the carrier and the blend saw that.
|
||||||
|
TEST_F(P4aSeamAuditScenario, ATextureRespecifiedToThreeChannelsWhileAttachedReachesTheFramebufferRecord) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessEspryt("F-3");
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kQuadVS, kColorFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||||
|
ASSERT_GE(colorLocation, 0);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the SRGB8_ALPHA8 texture was refused";
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
// Phase 1: four channels, the application owns alpha.
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||||
|
DrawQuad();
|
||||||
|
float pixel[4];
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
EXPECT_NEAR(pixel[3], 0.25f, 0.02f) << "a four-channel attachment stores the draw's alpha";
|
||||||
|
|
||||||
|
// Phase 2: THE RESPECIFY to three channels, while attached.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, kSize, kSize, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the respecify to SRGB8 was refused";
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||||
|
DrawQuad();
|
||||||
|
// dst = stored alpha; src factor GL_DST_ALPHA, dst factor GL_ZERO, source white =>
|
||||||
|
// the colour becomes (storedAlpha, storedAlpha, storedAlpha) - ThreeChannelAttachment's
|
||||||
|
// own probe, which nothing on the readback path can doctor.
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_DST_ALPHA, GL_ZERO);
|
||||||
|
glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
DrawQuad();
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
ReadPixelFloat(kSize / 2, kSize / 2, pixel);
|
||||||
|
// PUSH BUILDS ONLY, EVERY ARM OF THEM (the mirror). The pre-handle half of the fix (an in-place
|
||||||
|
// regeneration takes the backend-id generation a re-mint takes) is Espryt code the
|
||||||
|
// pull build would share, and G1 keeps the pull library byte-identical to the P4a
|
||||||
|
// baseline - so it is compiled under MOBILEGL_PIPE_PUSH and the pull build keeps the
|
||||||
|
// pre-P4a hole until the same lines land on dev on their own. The peek returns true
|
||||||
|
// exactly where it could look, which for a case that already skipped off Espryt means
|
||||||
|
// "a push build"; what it writes (is the handle arm live) does not matter here.
|
||||||
|
bool framebufferArmLive = false;
|
||||||
|
if (PeekEsprytFramebufferHandleArmIsLive(&framebufferArmLive)) {
|
||||||
|
EXPECT_NEAR(pixel[0], 1.0f, 0.05f)
|
||||||
|
<< "GL_DST_ALPHA read the stored alpha of a three-channel attachment and it was not "
|
||||||
|
"1.0: the framebuffer record (handle arm) or the FBO twin's memo (pre-handle arm) "
|
||||||
|
"still describes the four-channel storage the texture was attached with, so the "
|
||||||
|
"draw was let write alpha (F-3, mirror)";
|
||||||
|
} else {
|
||||||
|
std::cout << "[ P4aSeamAudit ] three-channel respecify verdict DECLINED on the pull build (the "
|
||||||
|
"in-place regeneration bump is push-only by G1); red read "
|
||||||
|
<< pixel[0] << std::endl;
|
||||||
|
RecordProperty("p4a_seam_white_box", "declined");
|
||||||
|
RecordProperty("p4a_seam_white_box_reason", "three-channel respecify: pull build (G1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// F-1 / F-1b: a program switch re-resolves the view set, and the texture sync list with it
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The sequence the audit named, and every step of it is ordinary: two programs sampling two
|
||||||
|
// different units, a texture bound to a unit's EMPTY 2D slot - the unit was already touched
|
||||||
|
// through another target, so the high-water mark does not move - while a program that does
|
||||||
|
// not sample it is in use, then the switch to the one that does. Nothing between the two
|
||||||
|
// draws touches a parameter, a level or a populated slot, which is exactly what leaves the
|
||||||
|
// record epoch - and the program-independent texture sync list keyed on it - unmoved on
|
||||||
|
// the tree the audit read: the second program sampled an unbound unit and drew black.
|
||||||
|
TEST_F(P4aSeamAuditScenario, ATextureBoundToAnEmptySlotUnderOneProgramIsSampledByTheNext) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint first = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||||
|
ASSERT_NE(first, 0u) << error;
|
||||||
|
const GLuint second = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||||
|
ASSERT_NE(second, 0u) << error;
|
||||||
|
glUseProgram(first);
|
||||||
|
glUniform1i(glGetUniformLocation(first, "uTex"), 0);
|
||||||
|
glUseProgram(second);
|
||||||
|
glUniform1i(glGetUniformLocation(second, "uTex"), 1);
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
// Every texture exists, complete, with its parameters set, BEFORE the first draw: a
|
||||||
|
// parameter or a level defined between the two draws would move the sampling-resolution
|
||||||
|
// generation and rescue the list by accident.
|
||||||
|
const GLuint red = MakeSolidTexture2D(255, 0, 0);
|
||||||
|
const GLuint green = MakeSolidTexture2D(0, 255, 0);
|
||||||
|
GLuint touch3D = 0;
|
||||||
|
glGenTextures(1, &touch3D);
|
||||||
|
glBindTexture(GL_TEXTURE_3D, touch3D);
|
||||||
|
const std::uint8_t blue[2 * 2 * 2 * 4] = {0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255,
|
||||||
|
0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255};
|
||||||
|
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, blue);
|
||||||
|
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_3D, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "texture setup left a GL error behind";
|
||||||
|
|
||||||
|
ColorFbo target = MakeColorFbo(kSize, kSize);
|
||||||
|
ASSERT_NE(target.fbo, 0u);
|
||||||
|
BindFbo(target);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
|
||||||
|
// Unit 1 is TOUCHED through its 3D slot; its 2D slot stays empty. Unit 0 holds red.
|
||||||
|
// The first program is in use BEFORE the first verb (the clear), so the very first
|
||||||
|
// view set that goes out is already resolved for it - measured: with no program in
|
||||||
|
// use at the clear the first set is [null, null], and the bind below then re-resolves
|
||||||
|
// to [red, null], a DIFFERENT set that moves the serial and rescues the case by
|
||||||
|
// accident.
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_3D, touch3D);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, red);
|
||||||
|
glUseProgram(first);
|
||||||
|
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
DrawQuad();
|
||||||
|
|
||||||
|
// THE BIND ONTO THE EMPTY SLOT, under a program that does not sample unit 1, and a
|
||||||
|
// draw with THAT program so the bind's own re-resolution of the view set happens under
|
||||||
|
// it (the bind generation fires bit 12 at the next verb; a switch inside the same verb
|
||||||
|
// gap would let that fire resolve under the second program by accident) ...
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, green);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
DrawQuad();
|
||||||
|
// ... and THE SWITCH to the one that does sample it. No other state moves.
|
||||||
|
glUseProgram(second);
|
||||||
|
DrawQuad();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the two draws left a GL error behind";
|
||||||
|
|
||||||
|
const Image image = ReadPixels(kSize, kSize);
|
||||||
|
ASSERT_FALSE(image.Empty());
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, kSize - 1 - kInset, kInset, kSize - 1 - kInset, "green", 0.0,
|
||||||
|
"the draw after the program switch"))
|
||||||
|
<< "black means the second program sampled an unbound unit: the texture bound to the "
|
||||||
|
"empty slot was never synced because the view set - and E's record epoch with it - "
|
||||||
|
"did not move on the program switch (F-1 / F-1b); red means the first program's "
|
||||||
|
"set was still in force";
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_3D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glUseProgram(0);
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
glDeleteTextures(1, &red);
|
||||||
|
glDeleteTextures(1, &green);
|
||||||
|
glDeleteTextures(1, &touch3D);
|
||||||
|
glDeleteProgram(first);
|
||||||
|
glDeleteProgram(second);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// F-2 / SD-4: the image window follows the program, through buffer images
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Public-GL half: both dispatches store what they should (every arm passes this - the
|
||||||
|
// server's window/high-water union takes the pre-handle bind for a unit the record does
|
||||||
|
// not cover, which is exactly why the seam was silent). White-box half, on Espryt's handle
|
||||||
|
// arm: after the first dispatch set_shader_images must have arrived with a window of ONE
|
||||||
|
// unit (SD-4: on the tree the audit read a buffer image never reached the record at all -
|
||||||
|
// the null -> program transition moved nothing bit 14 read), and after the switch to the
|
||||||
|
// program naming two units the window must be TWO (F-2: the two programs' image-unit
|
||||||
|
// counters are equal, so the switch alone moved nothing either).
|
||||||
|
TEST_F(P4aSeamAuditScenario, AProgramSwitchWithEqualImageUnitCountersMovesTheImageWindow) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ComputeImagesAreUsable()) GTEST_SKIP() << "no compute image units / buffer textures on this host";
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint one = MakeComputeProgram(kOneBufferImageCS, &error);
|
||||||
|
ASSERT_NE(one, 0u) << error;
|
||||||
|
const GLuint two = MakeComputeProgram(kTwoBufferImagesCS, &error);
|
||||||
|
ASSERT_NE(two, 0u) << error;
|
||||||
|
|
||||||
|
GLuint buffer0 = 0;
|
||||||
|
GLuint buffer1 = 0;
|
||||||
|
const GLuint image0 = MakeBufferTexture(&buffer0, 0u);
|
||||||
|
const GLuint image1 = MakeBufferTexture(&buffer1, 0u);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "buffer texture setup left a GL error behind";
|
||||||
|
|
||||||
|
// Both units bound BEFORE any dispatch, so the bind generation does not move between
|
||||||
|
// the two dispatches and the only thing that changes is the program in use.
|
||||||
|
glBindImageTexture(0, image0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);
|
||||||
|
glBindImageTexture(1, image1, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "binding the buffer images left a GL error behind";
|
||||||
|
|
||||||
|
const bool whiteBox = SamplerHandleArmIsLive("F-2 / SD-4");
|
||||||
|
|
||||||
|
glUseProgram(one);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the first dispatch leaked a GL error";
|
||||||
|
if (whiteBox) {
|
||||||
|
PipeShaderImageWindowPeek window{};
|
||||||
|
ASSERT_TRUE(PeekPipeShaderImageWindow(&window));
|
||||||
|
EXPECT_EQ(window.Start, 0u);
|
||||||
|
EXPECT_EQ(window.Count, 1u)
|
||||||
|
<< "set_shader_images never arrived for a program whose only image is a BUFFER "
|
||||||
|
"image (SD-4): the null -> program transition moved nothing bit 14 read";
|
||||||
|
}
|
||||||
|
|
||||||
|
glUseProgram(two);
|
||||||
|
glDispatchCompute(1, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the second dispatch leaked a GL error";
|
||||||
|
if (whiteBox) {
|
||||||
|
PipeShaderImageWindowPeek window{};
|
||||||
|
ASSERT_TRUE(PeekPipeShaderImageWindow(&window));
|
||||||
|
EXPECT_EQ(window.Start, 0u);
|
||||||
|
EXPECT_EQ(window.Count, 2u)
|
||||||
|
<< "the image window did not follow the program switch: two programs with equal "
|
||||||
|
"image-unit counters, and bit 14 mixed only the counter (F-2)";
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(ReadBufferTexel0(buffer0), 7u) << "the first program's store did not land";
|
||||||
|
EXPECT_EQ(ReadBufferTexel0(buffer1), 9u) << "the second program's store did not land";
|
||||||
|
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(one);
|
||||||
|
glDeleteProgram(two);
|
||||||
|
glDeleteTextures(1, &image0);
|
||||||
|
glDeleteTextures(1, &image1);
|
||||||
|
glDeleteBuffers(1, &buffer0);
|
||||||
|
glDeleteBuffers(1, &buffer1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
// F-4: the unit's driver sampler is the CSO's own twin on the handle arm
|
||||||
|
// -----------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Public-GL half: a glBindSampler'd object whose wrap differs from the texture's built-in
|
||||||
|
// sampler wins (GL 4.6 core 8.10) - every arm passes this, because the pre-handle program
|
||||||
|
// pass bound the object through its identity twin. White-box half, on Espryt's handle arm:
|
||||||
|
// the sampler the unit carries on the driver must be the twin Espryt holds AT THE CSO
|
||||||
|
// HANDLE bind_sampler_states named for the unit. On the tree the audit read that twin did
|
||||||
|
// not exist - the handle is content-addressed, the registry's twins were minted off
|
||||||
|
// lifetime ids - so the record arm bound nothing on every draw.
|
||||||
|
TEST_F(P4aSeamAuditScenario, ABoundSamplerObjectIsDrivenThroughItsCsoTwinOnTheHandleArm) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kQuadVS, kOutsideSampleFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
// The texture's built-in sampler REPEATS, so (1.5, 1.5) reads the red texel through it;
|
||||||
|
// the sampler object CLAMPS TO A WHITE BORDER, so the same coordinate reads white
|
||||||
|
// through it. White is a Vulkan palette border colour, so Magma needs no extension.
|
||||||
|
const GLuint red = MakeSolidTexture2D(255, 0, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||||
|
GLuint sampler = 0;
|
||||||
|
glGenSamplers(1, &sampler);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||||
|
const GLfloat white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
glSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR, white);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "sampler setup left a GL error behind";
|
||||||
|
|
||||||
|
ColorFbo target = MakeColorFbo(kSize, kSize);
|
||||||
|
ASSERT_NE(target.fbo, 0u);
|
||||||
|
BindFbo(target);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, red);
|
||||||
|
glBindSampler(0, sampler);
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||||
|
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
DrawQuad();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the draw left a GL error behind";
|
||||||
|
|
||||||
|
const Image image = ReadPixels(kSize, kSize);
|
||||||
|
ASSERT_FALSE(image.Empty());
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, kSize - 1 - kInset, kInset, kSize - 1 - kInset, "white", 0.0,
|
||||||
|
"the draw through the bound sampler object"))
|
||||||
|
<< "red means the texture's own REPEAT sampler applied instead of the bound object's "
|
||||||
|
"CLAMP_TO_BORDER";
|
||||||
|
|
||||||
|
if (SamplerHandleArmIsLive("F-4")) {
|
||||||
|
EsprytUnitSamplerPeek peek{};
|
||||||
|
ASSERT_TRUE(PeekEsprytUnitSampler(0, sampler, &peek));
|
||||||
|
std::cout << "[ P4aSeamAudit ] white-box: unit 0 driver sampler " << peek.BoundSamplerId
|
||||||
|
<< ", bind_sampler_states handle {" << peek.CsoHandleSlot << ", " << peek.CsoHandleGen
|
||||||
|
<< "} inside window " << (peek.UnitInsideWindow ? "yes" : "no") << ", CSO twin "
|
||||||
|
<< peek.CsoTwinSamplerId << ", identity twin " << peek.IdentityTwinSamplerId << std::endl;
|
||||||
|
EXPECT_TRUE(peek.UnitInsideWindow) << "bind_sampler_states did not describe unit 0";
|
||||||
|
EXPECT_NE(peek.CsoHandleSlot, 0u) << "bind_sampler_states names no CSO for a unit that carries "
|
||||||
|
"a sampler object";
|
||||||
|
EXPECT_NE(peek.CsoTwinSamplerId, 0u)
|
||||||
|
<< "Espryt holds no twin at the CSO handle bind_sampler_states named: the record arm's "
|
||||||
|
"lookup went to the identity-keyed registry with a content-addressed handle and "
|
||||||
|
"could never hit (F-4)";
|
||||||
|
EXPECT_EQ(peek.BoundSamplerId, peek.CsoTwinSamplerId)
|
||||||
|
<< "the driver sampler on unit 0 is not the CSO's twin, so it was put there by the "
|
||||||
|
"pre-handle program pass and not by the record arm (F-4)";
|
||||||
|
EXPECT_EQ(peek.IdentityTwinSamplerId, 0u)
|
||||||
|
<< "an identity-keyed twin was minted for the sampler object on the handle arm: the "
|
||||||
|
"pre-handle pass is still the one doing the binding";
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindSampler(0, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glUseProgram(0);
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
glDeleteSamplers(1, &sampler);
|
||||||
|
glDeleteTextures(1, &red);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ResourceSubsystemControlScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE P3a SUBSYSTEM A/B IS REAL (gate G12).
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// every later comparison is quietly taken against a switch that does nothing. This file is the
|
||||||
|
// entry that cannot let that happen.
|
||||||
|
//
|
||||||
|
// WHAT IT ASSERTS, per arm:
|
||||||
|
//
|
||||||
|
// on (MOBILEGL_PIPE_PUSH=0x1ff)
|
||||||
|
// The client emits map_persistent for every definition of a store past
|
||||||
|
// BufferObject::TryAdoptLargeStorage's 16 MiB threshold, so the window's
|
||||||
|
// map-persistent-roundtrips (`mpr=`) equals the number of definitions in it - one per
|
||||||
|
// storage definition, mint or decline (D-B2).
|
||||||
|
//
|
||||||
|
// off (MOBILEGL_PIPE_PUSH=0x7f, P2's default = P3a's subsystems cleared)
|
||||||
|
// The frontend dispatch falls through to the legacy BufferBackendOps arm, nothing is emitted
|
||||||
|
// through the resource family, and mpr= must read ZERO. This is the reading a dead switch
|
||||||
|
// fails: with bit 7 ignored, this arm would report the same non-zero count as the other one.
|
||||||
|
//
|
||||||
|
// both arms
|
||||||
|
// THE PIXELS MUST NOT MOVE. The arena is filled with one solid-colour quad and drawn, and
|
||||||
|
// both arms must read back that colour. "The counters moved and the picture did not" is the
|
||||||
|
// whole claim - a switch that changed what is drawn would not be an A/B, it would be a bug.
|
||||||
|
//
|
||||||
|
// WHY IT CAN SKIP. The counter is emitted by the client-side resource tracker (P3a package B), and
|
||||||
|
// this file is written against the P3a contract commit, before that package lands. Until then
|
||||||
|
// nothing emits map_persistent, mpr= is structurally zero in BOTH arms, and an assertion about the
|
||||||
|
// difference would be a statement about nothing. The build answers the question rather than a
|
||||||
|
// hand-maintained list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/
|
||||||
|
// for the counter's name and passes the answer in as MGITEST_PIPE_RESOURCE_EMITTER_PRESENT, with a
|
||||||
|
// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale. It
|
||||||
|
// is a CONTENT probe, not a filename probe, so the owning package keeps control of its own file
|
||||||
|
// layout - P3a's new client files are headers (D-N), and a glob for `ResourceTracker.cpp` would
|
||||||
|
// have kept this control skipping forever with a reason that had become false.
|
||||||
|
//
|
||||||
|
// DirectGLES ONLY, and that is the honest scope: P3a migrates Espryt's buffer and VAO paths.
|
||||||
|
// Magma's buffer path is P7 and registers no MGPipeResourceOps, so a DirectVulkan lane here would
|
||||||
|
// be measuring the client emitter against a backend that has not been asked to change - which is
|
||||||
|
// a real question, but it is P7's, not this control's.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set by the two ResourceSubsystemControl. ctest entries and by nothing else; a harness
|
||||||
|
// marker, never read by the library. Its absence means an ambient entry, where neither the
|
||||||
|
// stats channel nor a private log path is configured.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_RESOURCE_SUBSYSTEM_LANE";
|
||||||
|
constexpr const char* kLaneOn = "on";
|
||||||
|
constexpr const char* kLaneOff = "off";
|
||||||
|
|
||||||
|
// Past BufferObject::TryAdoptLargeStorage's 16 MiB threshold, so the store is offered for
|
||||||
|
// adoption at all; the vertex payload sits deep inside it so a clamped or aliased adopted
|
||||||
|
// range would miss it. Same shape as LargeArenaAdoptionScenario, deliberately: this
|
||||||
|
// control's workload has to be one the buffer path really takes.
|
||||||
|
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(20) * 1024 * 1024;
|
||||||
|
constexpr GLintptr kVertexOffset = GLintptr(16) * 1024 * 1024;
|
||||||
|
// Two definitions and several draws each, so "one per definition", "one per draw" and
|
||||||
|
// "none at all" are three different numbers.
|
||||||
|
//
|
||||||
|
// ONE ARENA DEFINED TWICE, not two arenas defined once each: the second definition
|
||||||
|
// RE-SPECIFIES a store whose bytes the VAO's attributes are already pointing into, and
|
||||||
|
// the attributes are not re-declared afterwards. That makes this control also the place
|
||||||
|
// where the respecify/retire path is exercised on BOTH arms of the A/B, which is what
|
||||||
|
// ID-9 asks for: `dev`'s d7655247 ("rebind VAOs when an adopted buffer is respecified -
|
||||||
|
// the immediate retire path forgot the buffer-id generation") arrived in
|
||||||
|
// feat/disaggregated with the 5cb826b0 merge, and the handle arm duplicates that retire
|
||||||
|
// core, so an arm that forgot the rebind must be visible somewhere. Here it is a dead
|
||||||
|
// draw or a fault, not a silent divergence. The first cut of this file routed around the
|
||||||
|
// path because the fix was not yet in this branch's history and the workload reproduced
|
||||||
|
// as a hard SIGSEGV in the vertex fetch; that detour is what ID-9 supersedes.
|
||||||
|
//
|
||||||
|
// The COUNT is unaffected by the change: two storage definitions either way, which is
|
||||||
|
// what ARCHITECTURE.md:474 prices.
|
||||||
|
constexpr int kDefinitionsInTheWindow = 2;
|
||||||
|
constexpr int kDrawsPerDefinition = 3;
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vertex> Quad(float r, float g, float b) {
|
||||||
|
return {
|
||||||
|
{-1.0f, -1.0f, r, g, b}, {1.0f, -1.0f, r, g, b}, {1.0f, 1.0f, r, g, b},
|
||||||
|
{-1.0f, -1.0f, r, g, b}, {1.0f, 1.0f, r, g, b}, {-1.0f, 1.0f, r, g, b},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LaneName() {
|
||||||
|
const char* lane = std::getenv(kLaneMarker);
|
||||||
|
return lane != nullptr ? std::string(lane) : std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceSubsystemControlScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
m_lane = LaneName();
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
// The VAO only. The arena is created and defined inside the counted window - the
|
||||||
|
// window a summary line reports is "since the previous line", so a definition
|
||||||
|
// taken in SetUp would be counted in a window this case does not control - and
|
||||||
|
// its attribute pointers are declared only once the store exists, because an
|
||||||
|
// attribute whose offset is 16 MiB into a store that has not been defined yet is
|
||||||
|
// a range no driver has to accept.
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
|
||||||
|
m_arena = 0;
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||||
|
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
|
||||||
|
void SkipUnlessTheLaneIsAssertableHere() {
|
||||||
|
if (m_lane.empty()) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the two ResourceSubsystemControl. ctest "
|
||||||
|
"entries set MGITEST_RESOURCE_SUBSYSTEM_LANE together with the "
|
||||||
|
"MOBILEGL_PIPE_PUSH bitmask that arm means, MOBILEGL_PIPE_STATS=1, "
|
||||||
|
"MOBILEGL_PIPE_STATS_PERIOD=1 and a private MOBILEGL_LOG_FILE_PATH. "
|
||||||
|
"None of that is configured in the ambient entries, and the ambient "
|
||||||
|
"log is shared, so a read here would race.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: there are no "
|
||||||
|
"subsystem bits to clear, CallClass::MapPersistentRoundtrips does "
|
||||||
|
"not exist and the summary line carries no mpr=. The entry is "
|
||||||
|
"registered here anyway so that `ctest -L integration-gpu` names the "
|
||||||
|
"same tests in the pull build and the push build (gate G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
|
||||||
|
"emits map_persistent, mpr= is structurally zero in BOTH arms and "
|
||||||
|
"the difference between them is not observable yet. P3a package B "
|
||||||
|
"owns the client-side resource tracker; this control arms itself "
|
||||||
|
"when it lands, whatever file that package puts the emitter in.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||||
|
"summary line is the only channel this module has for reading "
|
||||||
|
"PipeStats";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ONE storage definition - the NULL-data glBufferData past the adoption threshold,
|
||||||
|
// which is Minecraft's arena-creation idiom and the adoption point - then a few
|
||||||
|
// draws. Entirely inside one frame, so one summary window covers exactly this.
|
||||||
|
//
|
||||||
|
// The attribute pointers are declared ONCE, on the first definition, and never again:
|
||||||
|
// definition 0 creates the store, every later index RE-SPECIFIES it under the live
|
||||||
|
// VAO. Re-declaring them afterwards would re-sync the VAO by hand and hide the thing
|
||||||
|
// the second definition is here to exercise (see kDefinitionsInTheWindow above).
|
||||||
|
void DefineTheArenaAndDrawFromIt(int index, float r, float g, float b) {
|
||||||
|
if (index == 0) glGenBuffers(1, &m_arena);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
const std::vector<Vertex> vertices = Quad(r, g, b);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||||
|
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
if (index == 0) {
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(kVertexOffset));
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
}
|
||||||
|
glUseProgram(m_program);
|
||||||
|
for (int draw = 0; draw < kDrawsPerDefinition; ++draw) {
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string m_lane;
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_arena = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ONE case per lane, and it is a constraint rather than a preference: this case READS the
|
||||||
|
// library log, the log is a per-LANE resource (the library opens it fopen(path, "w"), so
|
||||||
|
// every process in a lane truncates it), and a second case here would race this one under
|
||||||
|
// `ctest -j` with a failure indistinguishable from "the counter was never emitted". The
|
||||||
|
// plumbing is asserted first, with its own message, inside this one process.
|
||||||
|
TEST_F(ResourceSubsystemControlScenario, ClearingTheP3aBitsStopsTheEmissionsAndNotThePixels) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessTheLaneIsAssertableHere();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
Gl().EndFrame(); // close the setup window: everything below is one window
|
||||||
|
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
for (int definition = 0; definition < kDefinitionsInTheWindow; ++definition) {
|
||||||
|
DefineTheArenaAndDrawFromIt(definition, 0.0f, 1.0f, 0.0f);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR))
|
||||||
|
<< "arena definition " << definition
|
||||||
|
<< " left a GL error behind (definition 0 creates the store, every later one "
|
||||||
|
"re-specifies it under the live VAO)";
|
||||||
|
}
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||||
|
|
||||||
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||||
|
ASSERT_TRUE(window.found)
|
||||||
|
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting "
|
||||||
|
"here), so either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not reach the "
|
||||||
|
"process, or no summary line was emitted at all because nothing reached "
|
||||||
|
"PipeStats::OnPresent.";
|
||||||
|
RecordProperty("stats_line", window.line.c_str());
|
||||||
|
|
||||||
|
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
|
||||||
|
ASSERT_GE(roundtrips, 0)
|
||||||
|
<< "the summary line carries no mpr= field, so this build's PipeStats has no "
|
||||||
|
"map-persistent-roundtrips counter to read: "
|
||||||
|
<< window.line;
|
||||||
|
|
||||||
|
if (m_lane == kLaneOn) {
|
||||||
|
EXPECT_EQ(roundtrips, static_cast<long long>(kDefinitionsInTheWindow))
|
||||||
|
<< "with bits 7|8 SET the resource family is the path a store definition takes, so "
|
||||||
|
"each of the " << kDefinitionsInTheWindow
|
||||||
|
<< " definitions in this window is one map_persistent emission (mint or decline - "
|
||||||
|
"both need an answer from the resource owner, D-B2). "
|
||||||
|
<< (kDefinitionsInTheWindow * kDrawsPerDefinition)
|
||||||
|
<< " would mean an acquisition per draw, and 0 would mean the emission never "
|
||||||
|
"happened on the arm that is supposed to do it. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
} else if (m_lane == kLaneOff) {
|
||||||
|
EXPECT_EQ(roundtrips, 0)
|
||||||
|
<< "with bits 7|8 CLEARED (MOBILEGL_PIPE_PUSH=0x7f, P2's default) the frontend "
|
||||||
|
"dispatch must fall through to the legacy BufferBackendOps arm and emit nothing "
|
||||||
|
"through the resource family, so mpr= must be zero. A non-zero count here is the "
|
||||||
|
"dead-switch reading: the bits are being ignored, both arms run the same code, "
|
||||||
|
"and every push-vs-pull number taken against this A/B is measuring one arm twice. "
|
||||||
|
"It reported: "
|
||||||
|
<< window.line;
|
||||||
|
} else {
|
||||||
|
FAIL() << "unknown " << kLaneMarker << " value '" << m_lane
|
||||||
|
<< "': the arms are on / off. Reading an unrecognised name as either would make "
|
||||||
|
"this lane assert the other arm's expectation while claiming to test this one.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and the picture is the same whichever arm ran. The arena is drawn with one solid
|
||||||
|
// colour, so both arms must read back exactly that.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, "green", 0.0,
|
||||||
|
"the arena draw [" + m_lane + "]"))
|
||||||
|
<< "the subsystem bits changed what is DRAWN, which is not an A/B - the handle path and "
|
||||||
|
"the legacy path must produce the same pixels from the same arena.";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -19,11 +19,26 @@
|
|||||||
//
|
//
|
||||||
// The assertion is deliberately on the WHOLE grown range, so a partial write names the byte the
|
// The assertion is deliberately on the WHOLE grown range, so a partial write names the byte the
|
||||||
// stale extent stopped at.
|
// stale extent stopped at.
|
||||||
|
//
|
||||||
|
// P3a ADDS THE COST OF THAT REGROWTH (gate G10). ARCHITECTURE.md:474 prices a persistently mapped
|
||||||
|
// store at "one round trip per STORAGE DEFINITION, not one per store" - and, emphatically, not one
|
||||||
|
// per draw. `map-persistent-roundtrips` (`mpr=` in the summary line) counts every map_persistent
|
||||||
|
// EMISSION, mint or decline (D-B2), so the claim is directly countable: N definitions of an
|
||||||
|
// adopted store must publish exactly N, whatever the workload does between them. A regression that
|
||||||
|
// re-acquires per dispatch reports N x dispatches, which is the failure this case exists to name;
|
||||||
|
// a regression that stops emitting reports 0.
|
||||||
|
//
|
||||||
|
// The second case therefore respecifies a store LARGE ENOUGH TO BE ADOPTED
|
||||||
|
// (BufferObject::TryAdoptLargeStorage's 16 MiB threshold), several times, with several dispatches
|
||||||
|
// between the definitions, and reads the one window that covers exactly that workload. It skips -
|
||||||
|
// visibly, with the reason - on a tree where nothing emits the counter yet.
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
#include "../Harness/ScenarioFixture.h"
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
#ifdef GLAPI
|
#ifdef GLAPI
|
||||||
@@ -50,6 +65,31 @@ void main() {
|
|||||||
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
|
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
|
||||||
constexpr int kLargeElements = 24; // 96 bytes - what the second iteration grows to
|
constexpr int kLargeElements = 24; // 96 bytes - what the second iteration grows to
|
||||||
|
|
||||||
|
// The G10 case's sizes. Every one of them is past BufferObject::TryAdoptLargeStorage's
|
||||||
|
// 16 MiB threshold, because a store below it is never offered for adoption at all and the
|
||||||
|
// window would then be asserting that nothing happened. They GROW, which is the scenario's
|
||||||
|
// subject: each glBufferData is a new storage definition, so each is one acquisition.
|
||||||
|
constexpr int kAdoptedDefinitions = 3;
|
||||||
|
constexpr int kAdoptedBaseElements = 5 * 1024 * 1024; // 20 MiB of uint
|
||||||
|
constexpr int kAdoptedGrowthElements = 1024 * 1024; // + 4 MiB per definition
|
||||||
|
// Enough dispatches per definition that "one per definition" and "one per dispatch" are
|
||||||
|
// different numbers by a wide margin (3 vs 12), and few enough to stay cheap.
|
||||||
|
constexpr int kDispatchesPerDefinition = 4;
|
||||||
|
// Only the first elements are dispatched over: the point of the large store is the
|
||||||
|
// ADOPTION, not the compute cost.
|
||||||
|
constexpr int kDispatchedElements = 6;
|
||||||
|
|
||||||
|
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
|
||||||
|
// never read by the library. Its absence means an ambient entry, where neither the stats
|
||||||
|
// channel nor a private log path is configured - and where the shared log makes a read
|
||||||
|
// race a neighbour's bring-up.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
class StorageBufferRegrowScenario : public ScenarioTest {
|
class StorageBufferRegrowScenario : public ScenarioTest {
|
||||||
protected:
|
protected:
|
||||||
void SetUp() override {
|
void SetUp() override {
|
||||||
@@ -114,6 +154,56 @@ void main() {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Minecraft arena idiom, and the adoption point: a NULL-data definition of a
|
||||||
|
// store past the threshold. No host-side vector, so a 28 MiB definition costs
|
||||||
|
// nothing on this side of the API.
|
||||||
|
void DefineAdoptedStore(int elements) {
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
||||||
|
static_cast<GLsizeiptr>(static_cast<GLsizeiptr>(elements) *
|
||||||
|
static_cast<GLsizeiptr>(sizeof(unsigned int))),
|
||||||
|
nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GTEST_SKIP() returns from the function it is written in, so this cannot report
|
||||||
|
// through a return value; the caller pairs it with `if (IsSkipped()) return;`.
|
||||||
|
void SkipUnlessTheRoundtripCounterIsReadableHere() {
|
||||||
|
if (std::getenv(kLaneMarker) == nullptr) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the MapPersistentRoundtrips. ctest entry "
|
||||||
|
"sets MGITEST_MPR_LANE together with MOBILEGL_PIPE_PUSH's P3a mask, "
|
||||||
|
"MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH. None of that is configured in the ambient "
|
||||||
|
"entries, and the ambient log is shared, so a read here would race a "
|
||||||
|
"neighbour's bring-up.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH, so "
|
||||||
|
"CallClass::MapPersistentRoundtrips does not exist (PipeStats.h "
|
||||||
|
"declares it inside the push guard, because growing the enum in a "
|
||||||
|
"pull build resizes the counter arrays and the name table - a G1 "
|
||||||
|
"break for a counter that could never leave zero) and the summary "
|
||||||
|
"line carries no mpr=. The entry is registered here anyway so that "
|
||||||
|
"`ctest -L integration-gpu` names the same tests in the pull build "
|
||||||
|
"and the push build (gate G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_RESOURCE_EMITTER_PRESENT")) {
|
||||||
|
GTEST_SKIP() << "subsystem not implemented on this tree: no source under "
|
||||||
|
"MobileGL/MG_Impl/Pipe/ names MapPersistentRoundtrips, so nothing "
|
||||||
|
"emits map_persistent and mpr= is structurally zero. P3a package B "
|
||||||
|
"owns the client-side resource tracker; this entry arms itself when "
|
||||||
|
"it lands, whatever file that package puts the emitter in.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||||
|
"summary line is the only channel this module has for reading "
|
||||||
|
"PipeStats";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
unsigned int m_program = 0;
|
||||||
GLuint m_buffer = 0;
|
GLuint m_buffer = 0;
|
||||||
std::string m_buildLog;
|
std::string m_buildLog;
|
||||||
@@ -153,4 +243,68 @@ void main() {
|
|||||||
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// G10. N storage definitions of an adopted store cost N map-persistent round trips - not one
|
||||||
|
// per draw, and not zero.
|
||||||
|
//
|
||||||
|
// ONE case in this lane, and that is a constraint rather than a preference: it READS the
|
||||||
|
// library log, the log is a per-lane resource (the library opens it fopen(path, "w"), so every
|
||||||
|
// process in a lane truncates it), and a second reading entry in the same lane would race this
|
||||||
|
// one under `ctest -j` with a failure that looks exactly like "the counter was never emitted".
|
||||||
|
// The plumbing is therefore asserted first, with its own message, inside this one process.
|
||||||
|
TEST_F(StorageBufferRegrowScenario, NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessTheRoundtripCounterIsReadableHere();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
|
||||||
|
Gl().EndFrame(); // close the setup window: everything below is one window
|
||||||
|
|
||||||
|
for (int definition = 0; definition < kAdoptedDefinitions; ++definition) {
|
||||||
|
DefineAdoptedStore(kAdoptedBaseElements + definition * kAdoptedGrowthElements);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "definition " << definition << " of the adopted store failed";
|
||||||
|
for (int dispatch = 0; dispatch < kDispatchesPerDefinition; ++dispatch) {
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glDispatchCompute(static_cast<GLuint>(kDispatchedElements), 1, 1);
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The store is still the one the last definition made, and it still works: a counter
|
||||||
|
// assertion over a workload that silently stopped functioning would be measuring nothing.
|
||||||
|
std::vector<unsigned int> values(static_cast<std::size_t>(kDispatchedElements), 0xDEADBEEFu);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
||||||
|
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
for (int i = 0; i < kDispatchedElements; ++i) {
|
||||||
|
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
||||||
|
<< "the adopted store's own dispatch did not write element " << i;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gl().EndFrame(); // the swap that emits the window covering exactly the loop above
|
||||||
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||||
|
ASSERT_TRUE(window.found) << "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD "
|
||||||
|
"before getting here), so either MOBILEGL_PIPE_STATS / "
|
||||||
|
"MOBILEGL_PIPE_STATS_PERIOD did not reach the process or no summary "
|
||||||
|
"line was emitted at all because nothing reached PipeStats::OnPresent.";
|
||||||
|
RecordProperty("stats_line", window.line.c_str());
|
||||||
|
|
||||||
|
const long long roundtrips = PipeStatsWindow::CounterOrAbsent(window, "mpr");
|
||||||
|
ASSERT_GE(roundtrips, 0)
|
||||||
|
<< "the summary line carries no mpr= field, so this build's PipeStats has no "
|
||||||
|
"map-persistent-roundtrips counter to read: " << window.line;
|
||||||
|
EXPECT_EQ(roundtrips, static_cast<long long>(kAdoptedDefinitions))
|
||||||
|
<< "an adopted store costs ONE map_persistent per STORAGE DEFINITION "
|
||||||
|
"(ARCHITECTURE.md:474). This window defined the store " << kAdoptedDefinitions
|
||||||
|
<< " times and dispatched " << kDispatchesPerDefinition << " times against each of them, so "
|
||||||
|
<< kAdoptedDefinitions << " is the whole cost. "
|
||||||
|
<< (kAdoptedDefinitions * kDispatchesPerDefinition)
|
||||||
|
<< " would mean an acquisition per DRAW - the regression this counter exists to catch - and 0 "
|
||||||
|
"would mean nothing emitted map_persistent at all. It reported: "
|
||||||
|
<< window.line;
|
||||||
|
|
||||||
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||||
|
}
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,775 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A TEXTURE'S PARAMETERS TAKE EFFECT EVEN WHEN IT HAS NO SAMPLER VIEW (gate G9), the
|
||||||
|
// one scenario ROADMAP.md:20 names by hand and requires to be RED before P4a lands.
|
||||||
|
//
|
||||||
|
// THE DESIGN STATEMENT IT TESTS, ARCHITECTURE.md:100 (D10), verbatim: "SetTextureParams 按资源寻址、
|
||||||
|
// 与 sampler view 分开(D10):只作 FBO attachment / image 单元 / glCopyImageSubData 端点的纹理没有
|
||||||
|
// sampler view,但 Espryt 对 attachment 也同步纹理参数,且 RequireImageBindableStorage 需要在前端参数
|
||||||
|
// 版本不动时强制重同步." A texture that is only an attachment, only an image-unit binding or only a
|
||||||
|
// copy endpoint has NO sampler view at all - so a design that carried texture parameters on the
|
||||||
|
// view would silently drop them for exactly those textures. P4a addresses set_texture_params by
|
||||||
|
// RESOURCE, independently of any binding, which is what makes the record exist for every texture
|
||||||
|
// the moment its parameters move.
|
||||||
|
//
|
||||||
|
// THE FOUR CASES, and BRIEF-P4A.md D-E3 is where their expected verdicts come from:
|
||||||
|
//
|
||||||
|
// AnAttachmentOnlyTexturesSwizzleReachesTheDriver green today, green after
|
||||||
|
// AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver D-E3 says RED today
|
||||||
|
// AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint green today, green after
|
||||||
|
// ACopyImageEndpointOnlyTexturesParamsReachTheDriver green today, green after
|
||||||
|
//
|
||||||
|
// [MEASURED, AND IT DOES NOT MATCH D-E3] All four are GREEN on the P4a contract commit (08192d72),
|
||||||
|
// on llvmpipe, in the push build. The second one is green FOR A REASON THAT IS ITSELF THE FINDING,
|
||||||
|
// and it is written here rather than in a review comment because the next person to try to make it
|
||||||
|
// red needs to know why they cannot:
|
||||||
|
//
|
||||||
|
// A texture parameter's only public-GL observable is a SAMPLE - nothing about an attachment, an
|
||||||
|
// image binding or a copy endpoint reads a swizzle or an aspect mode - and a sample puts the
|
||||||
|
// texture on SyncNeccessaryTextures' UNIT list. That walk calls SyncTextureParamsToBackend for
|
||||||
|
// every entry whose IsDrawSyncClean is false (DirectGLES.cpp:1896-1898), and IsDrawSyncClean is
|
||||||
|
// false whenever the frontend's parameter version has moved since the last sync
|
||||||
|
// (Managers.h:1399-1416, `m_syncedTextureParamsVersion != paramsVersion`). So the very act of
|
||||||
|
// observing the parameter repairs the state the observation was meant to catch: the gap between
|
||||||
|
// "the parameter moved on a read-attachment-only texture" and "the driver was told" is REAL, and
|
||||||
|
// it is closed by the next sampler binding, which is also the only thing that can see it.
|
||||||
|
//
|
||||||
|
// What that means for the gate, stated plainly so nobody reads a green here as evidence of
|
||||||
|
// anything it is not:
|
||||||
|
//
|
||||||
|
// * the four cases are a REGRESSION NET around D10, not the evidence for the change. They pin
|
||||||
|
// the design statement: a texture's parameters take effect however the texture is reached, and
|
||||||
|
// in particular they would go RED if any of the four reachability paths were ever made to
|
||||||
|
// depend on the texture having a sampler VIEW - which is exactly the coupling P4a's
|
||||||
|
// resource-addressed set_texture_params removes and the thing a later phase could reintroduce;
|
||||||
|
// * the "落地前必须红" artefact ROADMAP.md:20 asks for is NOT produced by the public-GL half
|
||||||
|
// of this file, and no public-GL integration scenario on a monolith tree can produce it.
|
||||||
|
// Producing it needs an observation of the DRIVER's texture object taken while the texture is
|
||||||
|
// still read-attachment-only. ID-19 rules that G9 is therefore a WHITE-BOX assertion, and this
|
||||||
|
// file now carries the SCENARIO half of it (the unit half is package D's,
|
||||||
|
// MG_Test/SanityTest.cpp's DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachThe
|
||||||
|
// DriverWithNoSamplerView).
|
||||||
|
//
|
||||||
|
// THE WHITE-BOX HALF, and what it adds to the four cases below. Each case, at the point where its
|
||||||
|
// texture is reachable ONLY its own way and BEFORE the observing sample, takes three readings
|
||||||
|
// through MG_IntegrationTest/Harness/PipeApplyPeek.h and asserts all three:
|
||||||
|
//
|
||||||
|
// (a) the APPLIER holds a set_texture_params record for this texture, at a non-zero ParamsSerial,
|
||||||
|
// carrying the field the case moved;
|
||||||
|
// (b) ESPRYT's applied value for the same texture - read back from the DRIVER, through the twin's
|
||||||
|
// own ES name - is that value ALREADY, not after the first sampler view;
|
||||||
|
// (c) Espryt holds NO SAMPLER VIEW for this texture yet, which is what turns (b) from "applied"
|
||||||
|
// into "applied WITHOUT one" and is the whole claim D10 makes.
|
||||||
|
//
|
||||||
|
// (c) is the assertion the public-GL half structurally cannot make: making it there would create
|
||||||
|
// the view. (b) is the half that goes red on a backend that DEFERS - a tree where the parameter
|
||||||
|
// push is gated on a sampler view existing is green on all four public-GL cases forever, because
|
||||||
|
// the sample that observes the parameter is also what mints the view and repairs the state.
|
||||||
|
//
|
||||||
|
// WHAT THE THREE READINGS DO **NOT** COVER, so the next reader does not over-trust them
|
||||||
|
// (esprytobj re-review N-9, carried here by request). D's unit probe drives
|
||||||
|
// SyncTextureParamsToBackend directly, so the only deferral shape IT can see is one INSIDE that
|
||||||
|
// function. These three run through the real per-frame paths and therefore also see a deferral
|
||||||
|
// introduced ABOVE it - in SyncNeccessaryTextures, in the attachment walk, or in E's per-unit walk.
|
||||||
|
// Between them the two halves cover both, and neither covers both alone.
|
||||||
|
//
|
||||||
|
// A READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES - it is not a
|
||||||
|
// GTEST_SKIP, and that is a deliberate departure from the shape the review sketched. These four
|
||||||
|
// cases are dual-purpose: they are also the END-TO-END regression net around D10, and that net is
|
||||||
|
// the ONLY thing measuring D10 on exactly the arms where the peek cannot look (the pull build,
|
||||||
|
// which has no applier at all; the 0x1ff and 0 lanes, where the texture family is switched off;
|
||||||
|
// Magma, which has no Espryt twin). Skipping the case there would delete the one verdict those
|
||||||
|
// lanes carry in order to report the absence of a second one. The decline is printed, recorded as
|
||||||
|
// a test property and named, so a lane that silently stopped taking the reading is visible in the
|
||||||
|
// log rather than in a count.
|
||||||
|
//
|
||||||
|
// WHY THE SECOND ONE IS THE RED, mechanically (scout-espryt-framebuffer.md 2.6, re-opened at the
|
||||||
|
// base ref). Today a texture's parameters ride on the UNIT BINDING and on the DRAW attachment set:
|
||||||
|
//
|
||||||
|
// bound to a sampler unit <= the high-water mark SyncNeccessaryTextures' unit list -> synced
|
||||||
|
// attachment of the DRAW framebuffer SyncNeccessaryTextures' FBO list -> synced
|
||||||
|
// attachment of the READ framebuffer ONLY SyncCurrentFBO -> SyncToBackend ->
|
||||||
|
// SyncAttachmentObject, which calls
|
||||||
|
// SyncMipmapsToBackend at Managers.cpp:7161
|
||||||
|
// and NOTHING ELSE -> NOT synced
|
||||||
|
// bound to an image unit SyncImageTextureBinding ->
|
||||||
|
// SyncTextureObjectToBackend, and
|
||||||
|
// RequireImageBindableStorage additionally
|
||||||
|
// forces m_forceTextureParamsResync -> synced
|
||||||
|
// a glCopyImageSubData endpoint MakeGLESCopyImageEndpoint ->
|
||||||
|
// SyncTextureObjectToBackend -> synced
|
||||||
|
//
|
||||||
|
// SyncNeccessaryTextures' attachment list reads GetFramebufferBindingSlotChecked(Draw) only
|
||||||
|
// (DirectGLES.cpp:1944), so a texture that is exclusively a READ attachment gets its STORAGE synced
|
||||||
|
// and its PARAMETERS never. P4a closes that gap deliberately (D-E3): the record is addressed by
|
||||||
|
// resource, and Espryt's SyncAttachmentObject applies parameters for ANY attachment, draw or read.
|
||||||
|
// It is a behaviour change and it is the deliverable, not a drive-by dev fix (ROADMAP.md:98).
|
||||||
|
//
|
||||||
|
// HOW EACH CASE OBSERVES "REACHED THE DRIVER", and why the observation is always a LATER SAMPLE.
|
||||||
|
// A texture parameter is by definition a sampling parameter: nothing about an attachment, an image
|
||||||
|
// binding or a copy endpoint reads a swizzle or a depth/stencil aspect mode, so the only thing that
|
||||||
|
// can see one is a sample. Each case therefore does the same three things -
|
||||||
|
//
|
||||||
|
// 1. put the texture through ONE of the five reachability paths above, and only that one,
|
||||||
|
// 2. move a parameter while it is reachable ONLY that way (through the DSA entry points
|
||||||
|
// glTextureParameteri / glTextureSubImage2D, so no step of the setup ever binds the texture to
|
||||||
|
// a sampler unit - a bind would put it on the unit list and answer the question by accident),
|
||||||
|
// 3. sample it once, at the end, and read the colour back.
|
||||||
|
//
|
||||||
|
// - and the difference between them is step 1 alone. A case that is red says: the parameter set
|
||||||
|
// while the texture was reachable only that way did not survive to the sample.
|
||||||
|
//
|
||||||
|
// DIRECTGLES ONLY. The gap is Espryt's - it is a statement about SyncAttachmentObject and
|
||||||
|
// SyncNeccessaryTextures - and P4a does not touch MG_Backend/DirectVulkan (D-Q). Magma answers the
|
||||||
|
// same GL question through an entirely different path, so a red or a green there would be evidence
|
||||||
|
// about P7's work rather than about this gate; the cases SKIP on any other backend, naming that.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeApplyPeek.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
constexpr int kTextureSize = 4;
|
||||||
|
|
||||||
|
constexpr const char* kQuadVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = aPos * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Samples texel (0,0) with an explicit fetch: no filtering, no derivatives, no wrap - so
|
||||||
|
// the colour that comes back is the texel as the driver's swizzle presents it and nothing
|
||||||
|
// else can move it.
|
||||||
|
constexpr const char* kFetchFS = R"(#version 330 core
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The stencil aspect of a packed depth/stencil texture is an UNSIGNED INTEGER texture, so
|
||||||
|
// it needs a usampler2D. The case that uses it turns "the stencil value is what was
|
||||||
|
// cleared" into a colour, because a colour is the only thing this harness can read back.
|
||||||
|
constexpr const char* kStencilFetchFS = R"(#version 330 core
|
||||||
|
uniform usampler2D uTex;
|
||||||
|
uniform uint uExpected;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
uint value = texelFetch(uTex, ivec2(0, 0), 0).r;
|
||||||
|
oColor = (value == uExpected) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextureParamsWithoutASamplerViewScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (Gl().BackendName() != "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "DirectGLES only: this scenario is about Espryt's own reachability "
|
||||||
|
"table - SyncNeccessaryTextures' attachment list walks the DRAW "
|
||||||
|
"slot only (DirectGLES.cpp:1944) and SyncAttachmentObject syncs "
|
||||||
|
"storage and not parameters (Managers.cpp:7161). "
|
||||||
|
<< Gl().BackendName()
|
||||||
|
<< " answers the same GL question through a different path, so a "
|
||||||
|
"verdict here would be evidence about that backend rather than "
|
||||||
|
"about this gate (P4a touches no DirectVulkan source but "
|
||||||
|
"MagmaPipeArms.h, D-Q).";
|
||||||
|
}
|
||||||
|
std::string error;
|
||||||
|
m_fetchProgram = CompileProgram(kQuadVS, kFetchFS, &error);
|
||||||
|
ASSERT_NE(m_fetchProgram, 0u) << error;
|
||||||
|
|
||||||
|
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
|
||||||
|
glGenBuffers(1, &m_quadBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the scene setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||||
|
if (m_fetchProgram != 0) glDeleteProgram(m_fetchProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct State Access is how every setup step below touches a texture, and it is the
|
||||||
|
// whole reason the cases can claim "this texture never had a sampler view": the classic
|
||||||
|
// entry points (glTexImage2D, glTexParameteri) all require the texture to be BOUND to a
|
||||||
|
// unit first, and a bind is exactly what puts it on SyncNeccessaryTextures' unit list.
|
||||||
|
// A case that used them would sync the parameters through the path it is trying to
|
||||||
|
// exclude and would be green for the wrong reason, on every tree, forever.
|
||||||
|
bool DirectStateAccessIsAvailable() {
|
||||||
|
GLuint probe = 0;
|
||||||
|
glCreateTextures(GL_TEXTURE_2D, 1, &probe);
|
||||||
|
const bool ok = FirstGLError() == GLenum(GL_NO_ERROR) && probe != 0;
|
||||||
|
if (probe != 0) glDeleteTextures(1, &probe);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkipWithoutDirectStateAccess() {
|
||||||
|
if (!DirectStateAccessIsAvailable()) {
|
||||||
|
GTEST_SKIP() << "glCreateTextures is not usable here, and every case in this file "
|
||||||
|
"needs the DSA entry points: the classic ones bind the texture to "
|
||||||
|
"a unit, which is the reachability path these cases exist to "
|
||||||
|
"exclude. A case that fell back to them would be green for the "
|
||||||
|
"wrong reason rather than measuring anything.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 4x4 RGBA8 texture, one solid colour, created and filled WITHOUT EVER BINDING IT.
|
||||||
|
GLuint MakeSolidTextureWithoutBinding(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
std::vector<std::uint8_t> texels(kTextureSize * kTextureSize * 4);
|
||||||
|
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||||
|
texels[i] = r;
|
||||||
|
texels[i + 1] = g;
|
||||||
|
texels[i + 2] = b;
|
||||||
|
texels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
GLuint texture = 0;
|
||||||
|
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||||
|
glTextureStorage2D(texture, 1, GL_RGBA8, kTextureSize, kTextureSize);
|
||||||
|
glTextureSubImage2D(texture, 0, 0, 0, kTextureSize, kTextureSize, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE OBSERVATION, shared by every case: bind the texture to a unit for the first time
|
||||||
|
// in its life and read one texel back through the default framebuffer.
|
||||||
|
Image SampleAndRead(GLuint program, GLuint texture) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::AssertionResult WholeViewportIs(const Image& image, const char* expected,
|
||||||
|
const std::string& when) {
|
||||||
|
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
|
||||||
|
image.Height() - kInset, expected, 0.0, when);
|
||||||
|
}
|
||||||
|
|
||||||
|
// R -> ZERO and G -> ONE, so a RED texel samples as GREEN if and only if the swizzle
|
||||||
|
// reached the driver, and as RED if it did not. Two colours the harness can name, from
|
||||||
|
// one parameter change, with no third outcome that could be mistaken for either.
|
||||||
|
void SwizzleRedIntoGreen(GLuint texture) {
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_R, GL_ZERO);
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_G, GL_ONE);
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_B, GL_ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which of the two parameters a case moved, and therefore which one the white-box
|
||||||
|
// reading has to find on both sides of the seam. Two, because they are the two the
|
||||||
|
// four cases use and because a peek that reported "some parameter" would be green for
|
||||||
|
// a backend that applied the wrong one.
|
||||||
|
enum class MovedParameter { Swizzle, DepthStencilMode };
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
// G9's WHITE-BOX READING (ID-19). Called by every case at the point where its
|
||||||
|
// texture is reachable only its own way and BEFORE the observing sample - which is
|
||||||
|
// the whole of the design, because the sample repairs what it observes.
|
||||||
|
//
|
||||||
|
// `expectedSwizzle` is the four GL enums the case set (or left at their defaults);
|
||||||
|
// `expectedDepthStencilMode` is GL_DEPTH_COMPONENT or GL_STENCIL_INDEX. Both are
|
||||||
|
// always passed and `moved` says which one is the case's subject, so a reader of a
|
||||||
|
// failure can see the untouched half beside the moved one.
|
||||||
|
void TakeTheWhiteBoxReadingBeforeAnySample(GLuint texture, GLenum target,
|
||||||
|
MovedParameter moved,
|
||||||
|
const GLint expectedSwizzle[4],
|
||||||
|
GLint expectedDepthStencilMode,
|
||||||
|
const char* whatMadeItReachable) {
|
||||||
|
const char* const movedName =
|
||||||
|
moved == MovedParameter::Swizzle ? "GL_TEXTURE_SWIZZLE_*"
|
||||||
|
: "GL_DEPTH_STENCIL_TEXTURE_MODE";
|
||||||
|
|
||||||
|
PipeTextureParamsRecordPeek record{};
|
||||||
|
if (!PeekPipeTextureParamsRecord(static_cast<unsigned>(texture), &record)) {
|
||||||
|
DeclineTheWhiteBoxReading(
|
||||||
|
"no set_texture_params record for this texture in the applier. Either "
|
||||||
|
"there is no applier here (a PULL build: MGPipeApplierState is "
|
||||||
|
"#if MOBILEGL_PIPE_PUSH), or this lane's MOBILEGL_PIPE_PUSH leaves "
|
||||||
|
"kMGPipeSubsystemTextureResources (bit 10) clear, or no backend "
|
||||||
|
"registered MGPipeResourceOps so the client never emitted (c0f). The "
|
||||||
|
"end-to-end half of this case below is unaffected and still decides it.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From here the reading WAS taken, so everything is a hard assertion: a record
|
||||||
|
// that exists and does not carry the parameter is exactly the finding.
|
||||||
|
EXPECT_NE(record.ParamsSerial, 0u)
|
||||||
|
<< "the applier holds a resource record for texture " << texture
|
||||||
|
<< " at handle {" << record.Slot << ", " << record.Gen
|
||||||
|
<< "} but its ParamsSerial is 0, i.e. NO set_texture_params has ever been "
|
||||||
|
"applied to it - and this case moved " << movedName << " while the texture "
|
||||||
|
"was " << whatMadeItReachable
|
||||||
|
<< ". A parameter change on a texture with no sampler view has to produce a "
|
||||||
|
"record addressed BY RESOURCE (D10, D-E1); a zero here means the client "
|
||||||
|
"never emitted one, which is the coupling P4a exists to remove reappearing "
|
||||||
|
"on the emitter's side of the seam.";
|
||||||
|
|
||||||
|
if (moved == MovedParameter::Swizzle) {
|
||||||
|
for (int channel = 0; channel < 4; ++channel) {
|
||||||
|
EXPECT_EQ(record.Swizzle[channel], static_cast<int>(expectedSwizzle[channel]))
|
||||||
|
<< "the applier's set_texture_params record for texture " << texture
|
||||||
|
<< " carries the wrong swizzle in channel " << channel
|
||||||
|
<< " (record 0x" << std::hex << record.Swizzle[channel] << ", expected 0x"
|
||||||
|
<< expectedSwizzle[channel] << std::dec
|
||||||
|
<< "). The record is what Espryt reads, so a wrong value here is a "
|
||||||
|
"wrong value everywhere downstream of it.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EXPECT_EQ(record.DepthStencilMode, static_cast<int>(expectedDepthStencilMode))
|
||||||
|
<< "the applier's set_texture_params record for texture " << texture
|
||||||
|
<< " carries GL_DEPTH_STENCIL_TEXTURE_MODE 0x" << std::hex
|
||||||
|
<< record.DepthStencilMode << ", expected 0x" << expectedDepthStencilMode
|
||||||
|
<< std::dec << ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
// (c) - and it is checked BEFORE (b) is read, because (b) reads the driver and a
|
||||||
|
// reader of a failure needs to know the view question was answered on the state
|
||||||
|
// this case built rather than on anything the peek did.
|
||||||
|
bool hasSamplerView = true;
|
||||||
|
if (!PeekEsprytHasSamplerViewForTexture(static_cast<unsigned>(texture),
|
||||||
|
&hasSamplerView)) {
|
||||||
|
DeclineTheWhiteBoxReading(
|
||||||
|
"Espryt holds no twin for this texture, so neither the sampler-view "
|
||||||
|
"question nor the applied-value one can be asked here. On a backend other "
|
||||||
|
"than DirectGLES that is the designed state (P4a touches no DirectVulkan "
|
||||||
|
"source but MagmaPipeArms.h, D-Q).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(hasSamplerView)
|
||||||
|
<< "Espryt already holds a SAMPLER VIEW for texture " << texture
|
||||||
|
<< ", which was " << whatMadeItReachable
|
||||||
|
<< " and has never been bound to a sampler unit in this case. The whole claim "
|
||||||
|
"of D10 is that a texture reached this way has no view, so if one exists "
|
||||||
|
"the reading below cannot separate 'applied by resource' from 'applied "
|
||||||
|
"through the view' and this case has stopped measuring G9.";
|
||||||
|
|
||||||
|
EsprytAppliedTextureParamsPeek applied{};
|
||||||
|
if (!PeekEsprytAppliedTextureParams(static_cast<unsigned>(texture),
|
||||||
|
static_cast<unsigned>(target), &applied)) {
|
||||||
|
DeclineTheWhiteBoxReading(
|
||||||
|
"Espryt's applied value could not be read back from the driver (no twin, "
|
||||||
|
"no ES name yet, or a target this peek has no binding query for).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[ TextureParamsWithoutASamplerView ] white-box: texture " << texture
|
||||||
|
<< " -> applier handle {" << record.Slot << ", " << record.Gen
|
||||||
|
<< "} paramsSerial " << record.ParamsSerial << ", Espryt ES name "
|
||||||
|
<< applied.BackendTextureId << ", sampler view: none, " << movedName
|
||||||
|
<< " applied before any sample" << std::endl;
|
||||||
|
|
||||||
|
if (moved == MovedParameter::Swizzle) {
|
||||||
|
for (int channel = 0; channel < 4; ++channel) {
|
||||||
|
EXPECT_EQ(applied.Swizzle[channel], static_cast<int>(expectedSwizzle[channel]))
|
||||||
|
<< "ESPRYT HAS NOT APPLIED THE SWIZZLE YET. Channel " << channel
|
||||||
|
<< " of the driver texture (ES name " << applied.BackendTextureId
|
||||||
|
<< ") reads 0x" << std::hex << applied.Swizzle[channel] << ", the "
|
||||||
|
<< "application set 0x" << expectedSwizzle[channel] << std::dec
|
||||||
|
<< ", and the applier's record already carries the right value - so "
|
||||||
|
"the record reached the server and the server has not pushed it. "
|
||||||
|
"The texture was " << whatMadeItReachable
|
||||||
|
<< " and has NO sampler view (asserted above), which makes this "
|
||||||
|
"exactly the deferred-to-first-view shape G9 exists to catch: the "
|
||||||
|
"sample at the end of this case would repair it, and the "
|
||||||
|
"end-to-end assertion below would then pass on a driver that was "
|
||||||
|
"told late. That is the half no public-GL case can see.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!applied.DepthStencilModeIsReadable) {
|
||||||
|
DeclineTheWhiteBoxReading(
|
||||||
|
"this driver would not answer glGetTexParameteriv("
|
||||||
|
"GL_DEPTH_STENCIL_TEXTURE_MODE), so the applied aspect mode cannot be "
|
||||||
|
"read back. The record half above was still asserted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(applied.DepthStencilMode, static_cast<int>(expectedDepthStencilMode))
|
||||||
|
<< "ESPRYT HAS NOT APPLIED THE DEPTH/STENCIL ASPECT MODE YET. The driver "
|
||||||
|
"texture (ES name " << applied.BackendTextureId << ") reads 0x"
|
||||||
|
<< std::hex << applied.DepthStencilMode << ", the application set 0x"
|
||||||
|
<< expectedDepthStencilMode << std::dec
|
||||||
|
<< ", and the applier's record already carries the right value. The "
|
||||||
|
"texture was " << whatMadeItReachable
|
||||||
|
<< " and has no sampler view, so this is D-E3's gap measured directly "
|
||||||
|
"rather than through a sample that would repair it: a driver left at "
|
||||||
|
"GL_DEPTH_COMPONENT samples the DEPTH bits where the application asked "
|
||||||
|
"for stencil.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printed, recorded and named, never silent - a lane that stopped taking the reading
|
||||||
|
// must be visible in the log. See this file's header for why it is not a GTEST_SKIP.
|
||||||
|
void DeclineTheWhiteBoxReading(const std::string& why) {
|
||||||
|
std::cout << "[ TextureParamsWithoutASamplerView ] white-box reading DECLINED: "
|
||||||
|
<< why << std::endl;
|
||||||
|
RecordProperty("g9_white_box", "declined");
|
||||||
|
RecordProperty("g9_white_box_reason", why.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_fetchProgram = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_quadBuffer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The swizzle SwizzleRedIntoGreen leaves behind, as GL enums: R -> ZERO, G -> ONE,
|
||||||
|
// B -> ZERO and A untouched at its GL default. Written once here because both the
|
||||||
|
// applier record and the driver read-back are compared against it.
|
||||||
|
constexpr GLint kRedIntoGreenSwizzle[4] = {GL_ZERO, GL_ONE, GL_ZERO, GL_ALPHA};
|
||||||
|
// A texture whose aspect mode was never touched, i.e. the GL initial value - which is
|
||||||
|
// also what a zeroed MGPTextureParams::DepthStencilMode decodes to (MGPipeTypes.h).
|
||||||
|
constexpr GLint kUntouchedDepthStencilMode = GL_DEPTH_COMPONENT;
|
||||||
|
// ...and the identity swizzle, for the case whose subject is the aspect mode: the moved
|
||||||
|
// half is asserted, and the untouched half is carried so a failure prints both.
|
||||||
|
constexpr GLint kUntouchedSwizzle[4] = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// 1. DRAW ATTACHMENT ONLY. Green today (SyncNeccessaryTextures' FBO list walks the draw
|
||||||
|
// slot and calls SyncTextureObjectToBackend, which syncs parameters) and green after.
|
||||||
|
// It is the regression net for the half of D-E3 that already works: P4a moves the
|
||||||
|
// parameter sync from the sync list onto the record, and this case is what says the
|
||||||
|
// move did not lose the case that used to be covered.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(TextureParamsWithoutASamplerViewScenario, AnAttachmentOnlyTexturesSwizzleReachesTheDriver) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipWithoutDirectStateAccess();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
// The parameter moves while the texture is reachable ONLY as a draw attachment...
|
||||||
|
SwizzleRedIntoGreen(texture);
|
||||||
|
// ...and a frame runs with it bound that way, so whatever the draw path syncs, syncs.
|
||||||
|
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||||
|
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
Gl().EndFrame();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the draw-attachment frame left a GL error";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
|
||||||
|
// G9's white-box reading, taken here: the texture has been a draw attachment and
|
||||||
|
// nothing else, and the sample below has not happened yet.
|
||||||
|
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle,
|
||||||
|
kRedIntoGreenSwizzle, kUntouchedDepthStencilMode,
|
||||||
|
"an attachment of the DRAW framebuffer and "
|
||||||
|
"nothing else");
|
||||||
|
|
||||||
|
const Image image = SampleAndRead(m_fetchProgram, texture);
|
||||||
|
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||||
|
"a texture that was only ever a DRAW attachment, sampled "
|
||||||
|
"after its swizzle moved"))
|
||||||
|
<< "the swizzle set while this texture was reachable only as a draw-framebuffer "
|
||||||
|
"attachment did not reach the driver: a red texel with R->ZERO, G->ONE must "
|
||||||
|
"sample as green.";
|
||||||
|
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// 2. READ ATTACHMENT ONLY - THE MANDATORY RED (ROADMAP.md:20, D-E3, G9).
|
||||||
|
//
|
||||||
|
// The parameter is GL_DEPTH_STENCIL_TEXTURE_MODE and the texture is a packed
|
||||||
|
// depth/stencil one, because that is the parameter whose absence is not a mis-filtered
|
||||||
|
// picture but the WRONG ASPECT: a driver left at the GL default samples the depth bits
|
||||||
|
// where the application asked for stencil, and the value that comes back is not the
|
||||||
|
// stencil that was written. RecreateBackendTexture's own comment says exactly this -
|
||||||
|
// "the mode makes it visible because falling back to the default silently samples the
|
||||||
|
// wrong aspect rather than merely mis-filtering" (Managers.cpp:4826-4833).
|
||||||
|
//
|
||||||
|
// The framebuffer is bound to GL_READ_FRAMEBUFFER and the DRAW target is left on the
|
||||||
|
// default framebuffer for the whole window, which is the ONE thing that separates this
|
||||||
|
// case from the one above.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||||
|
AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipWithoutDirectStateAccess();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint stencilProgram = CompileProgram(kQuadVS, kStencilFetchFS, &error);
|
||||||
|
ASSERT_NE(stencilProgram, 0u) << error;
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||||
|
glTextureStorage2D(texture, 1, GL_DEPTH24_STENCIL8, kTextureSize, kTextureSize);
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
if (FirstGLError() != GLenum(GL_NO_ERROR)) {
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(stencilProgram);
|
||||||
|
GTEST_SKIP() << "this driver would not create an immutable DEPTH24_STENCIL8 texture, "
|
||||||
|
"so there is no packed depth/stencil aspect here to sample and the "
|
||||||
|
"case cannot answer";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a stencil value through the texture AS A DRAW ATTACHMENT once, so that there is
|
||||||
|
// something in the stencil aspect to read. This is setup, not the window: the window
|
||||||
|
// below never makes it a draw attachment again.
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
|
||||||
|
glDrawBuffer(GL_NONE);
|
||||||
|
glReadBuffer(GL_NONE);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(stencilProgram);
|
||||||
|
GTEST_SKIP() << "a depth-stencil-only framebuffer is incomplete on this driver, so "
|
||||||
|
"the stencil aspect cannot be written and the case cannot answer";
|
||||||
|
}
|
||||||
|
constexpr GLint kStencil = 42;
|
||||||
|
glViewport(0, 0, kTextureSize, kTextureSize);
|
||||||
|
glStencilMask(0xFFu);
|
||||||
|
glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.5f, kStencil);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
Gl().EndFrame();
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "clearing the stencil aspect left a GL error";
|
||||||
|
|
||||||
|
// ---- the window: the texture is reachable ONLY as a READ attachment ----
|
||||||
|
//
|
||||||
|
// The DRAW binding is the default framebuffer throughout, so SyncNeccessaryTextures'
|
||||||
|
// attachment list - which walks the DRAW slot only - never sees this texture, and
|
||||||
|
// SyncCurrentFBO's read path reaches it through SyncAttachmentObject, which syncs
|
||||||
|
// storage and not parameters. That is the gap.
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
|
glTextureParameteri(texture, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||||
|
// A frame with the read binding live, and it has to contain a REAL DRAW: SyncCurrentFBO
|
||||||
|
// and the whole sync-list walk run at the validate point, so a frame that only cleared
|
||||||
|
// and swapped would never reach the read-side path this case is about.
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(m_fetchProgram);
|
||||||
|
glUniform1i(glGetUniformLocation(m_fetchProgram, "uTex"), 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
Gl().EndFrame();
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the read-attachment frame left a GL error";
|
||||||
|
|
||||||
|
// G9's white-box reading, and this is the case it matters most for: the aspect
|
||||||
|
// mode was set while the texture was reachable ONLY as a read attachment, and the
|
||||||
|
// observation below is a sample that would repair an unsynced parameter on its way
|
||||||
|
// to reporting it.
|
||||||
|
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D,
|
||||||
|
MovedParameter::DepthStencilMode,
|
||||||
|
kUntouchedSwizzle, GL_STENCIL_INDEX,
|
||||||
|
"an attachment of the READ framebuffer and "
|
||||||
|
"nothing else");
|
||||||
|
|
||||||
|
// ---- the observation ----
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(stencilProgram);
|
||||||
|
glUniform1i(glGetUniformLocation(stencilProgram, "uTex"), 0);
|
||||||
|
glUniform1ui(glGetUniformLocation(stencilProgram, "uExpected"),
|
||||||
|
static_cast<GLuint>(kStencil));
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
const GLenum sampleError = FirstGLError();
|
||||||
|
std::cout << "[ TextureParamsWithoutASamplerView ] read-attachment-only "
|
||||||
|
"GL_DEPTH_STENCIL_TEXTURE_MODE=GL_STENCIL_INDEX, expecting stencil "
|
||||||
|
<< kStencil << "; sample GL error 0x" << std::hex << sampleError << std::dec
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||||
|
"a texture that was only ever a READ attachment, sampled "
|
||||||
|
"through its stencil aspect"))
|
||||||
|
<< "GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX was set while this texture was "
|
||||||
|
"reachable ONLY as an attachment of the READ framebuffer, and the sample did not "
|
||||||
|
"come back as the stencil value that was cleared into it. The parameter did not "
|
||||||
|
"reach the driver, and - because the sample itself would have repaired an "
|
||||||
|
"unsynced parameter (see this file's header: the unit sync list calls "
|
||||||
|
"SyncTextureParamsToBackend whenever the params version moved) - a red here means "
|
||||||
|
"something stronger than the D-E3 gap: a reachability path that does not sync "
|
||||||
|
"parameters AT ALL, i.e. the sampler-view coupling ARCHITECTURE.md:100 (D10) "
|
||||||
|
"exists to remove. Read the case's own stdout line for the GL error the sample "
|
||||||
|
"raised before assuming an aspect-mode bug.";
|
||||||
|
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(stencilProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// 3. IMAGE UNIT ONLY, across a storage RE-MINT. Green today and green after, and the
|
||||||
|
// reason it is green is the thing P4a must not lose: glBindImageTexture drives
|
||||||
|
// RequireImageBindableStorage, which re-mints the storage in a possibly WIDENED carrier
|
||||||
|
// and sets m_forceTextureParamsResync (Managers.cpp:4787) precisely because the
|
||||||
|
// frontend's parameter version does not move across that transition. A parameter sync
|
||||||
|
// gated only on the frontend version would leave the driver at its defaults and sample
|
||||||
|
// the carrier's surplus channels raw.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||||
|
AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipWithoutDirectStateAccess();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||||
|
|
||||||
|
// The parameter moves while the texture is reachable only as an image-unit binding...
|
||||||
|
SwizzleRedIntoGreen(texture);
|
||||||
|
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
const GLenum bindError = FirstGLError();
|
||||||
|
if (bindError != GLenum(GL_NO_ERROR)) {
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
GTEST_SKIP() << "glBindImageTexture is not usable here (GL error 0x" << std::hex
|
||||||
|
<< bindError << std::dec
|
||||||
|
<< "), so the RequireImageBindableStorage transition this case is about "
|
||||||
|
"cannot be reached";
|
||||||
|
}
|
||||||
|
// ...and a frame runs with the image binding live, which is what drives the re-mint.
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
Gl().EndFrame();
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
|
||||||
|
// G9's white-box reading. RGBA8 is a core image format, so no widening carrier is
|
||||||
|
// minted and the driver's swizzle is the application's own - the composition
|
||||||
|
// RecreateBackendTexture applies for a NON-core format would show up here as a
|
||||||
|
// legitimate difference and this case deliberately does not use one.
|
||||||
|
TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle,
|
||||||
|
kRedIntoGreenSwizzle, kUntouchedDepthStencilMode,
|
||||||
|
"an image-unit binding and nothing else, across "
|
||||||
|
"a RequireImageBindableStorage re-mint");
|
||||||
|
|
||||||
|
const Image image = SampleAndRead(m_fetchProgram, texture);
|
||||||
|
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||||
|
"a texture that was only ever an image-unit binding, sampled "
|
||||||
|
"after its swizzle moved and its storage was re-minted"))
|
||||||
|
<< "the swizzle did not survive the RequireImageBindableStorage re-mint. The re-mint "
|
||||||
|
"creates a new driver texture at the ES defaults without moving the frontend's "
|
||||||
|
"parameter version, so the forced resync (Managers.cpp:4787) is the only thing "
|
||||||
|
"that puts the application's parameters back.";
|
||||||
|
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// 4. glCopyImageSubData ENDPOINT ONLY. Green today (MakeGLESCopyImageEndpoint calls
|
||||||
|
// SyncTextureObjectToBackend, DirectGLES.cpp:7588) and green after. The endpoint is the
|
||||||
|
// DESTINATION, so the case also proves the copy itself still lands: a swizzled read of
|
||||||
|
// the copied texel is only meaningful if the texel arrived.
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
TEST_F(TextureParamsWithoutASamplerViewScenario,
|
||||||
|
ACopyImageEndpointOnlyTexturesParamsReachTheDriver) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipWithoutDirectStateAccess();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint source = MakeSolidTextureWithoutBinding(255, 0, 0);
|
||||||
|
const GLuint destination = MakeSolidTextureWithoutBinding(0, 0, 255);
|
||||||
|
|
||||||
|
// The parameter moves while the destination is reachable only as a copy endpoint...
|
||||||
|
SwizzleRedIntoGreen(destination);
|
||||||
|
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, destination, GL_TEXTURE_2D, 0, 0, 0,
|
||||||
|
0, kTextureSize, kTextureSize, 1);
|
||||||
|
const GLenum copyError = FirstGLError();
|
||||||
|
if (copyError != GLenum(GL_NO_ERROR)) {
|
||||||
|
GLuint cleanup[2] = {source, destination};
|
||||||
|
glDeleteTextures(2, cleanup);
|
||||||
|
GTEST_SKIP() << "glCopyImageSubData is not usable here (GL error 0x" << std::hex
|
||||||
|
<< copyError << std::dec << "), so there is no copy endpoint to be";
|
||||||
|
}
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
// G9's white-box reading, on the DESTINATION - the endpoint whose parameters moved.
|
||||||
|
TakeTheWhiteBoxReadingBeforeAnySample(destination, GL_TEXTURE_2D,
|
||||||
|
MovedParameter::Swizzle, kRedIntoGreenSwizzle,
|
||||||
|
kUntouchedDepthStencilMode,
|
||||||
|
"a glCopyImageSubData destination and nothing "
|
||||||
|
"else");
|
||||||
|
|
||||||
|
// ...and the destination now holds the source's RED texel, which the swizzle must turn
|
||||||
|
// into GREEN when it is finally sampled.
|
||||||
|
const Image image = SampleAndRead(m_fetchProgram, destination);
|
||||||
|
EXPECT_TRUE(WholeViewportIs(image, "green",
|
||||||
|
"a texture that was only ever a glCopyImageSubData endpoint, "
|
||||||
|
"sampled after its swizzle moved"))
|
||||||
|
<< "the three readings are distinct and each names its own cause: GREEN is the pass "
|
||||||
|
"(the swizzle reached the driver); RED means the copy landed and the swizzle did "
|
||||||
|
"not; BLUE means neither happened, i.e. the destination is still its own original "
|
||||||
|
"texel and glCopyImageSubData wrote nothing. The swizzle is what turns any texel "
|
||||||
|
"into (0,1,0), so the colour separates the two failures rather than merging them.";
|
||||||
|
|
||||||
|
GLuint cleanup[2] = {source, destination};
|
||||||
|
glDeleteTextures(2, cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp
|
||||||
|
// Copyright (c) 2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - THE TEXTURE UPLOAD SHAPE, RECORDED (BRIEF-P4A.md D-D4). NOT A GATE IN P4a.
|
||||||
|
//
|
||||||
|
// WHAT IT IS FOR. SSIM is completely blind to the difference between "one union box" and "N
|
||||||
|
// separate rects", and that difference is the Mali upload cliff: Mali prices an upload by the
|
||||||
|
// number of JOBS, and ~100 one-rect sprite jobs against one union box measured +6 ms/frame
|
||||||
|
// (ARCHITECTURE.md:249-251). Every P4a gate can be green while the emission shape has silently
|
||||||
|
// inverted, so the shape needs a number - and there are TWO numbers, deliberately:
|
||||||
|
//
|
||||||
|
// tex[emit= box= rect= jobs=] the SERVER's count, Espryt's own, which has existed since P2
|
||||||
|
// emit[ctu=] the CLIENT's count of the same records (CallClass::
|
||||||
|
// ClientTextureUploadEmissions, minted by the P4a contract commit)
|
||||||
|
//
|
||||||
|
// The two agreeing is the whole reason both are printed (D-L). An emission-shape divergence between
|
||||||
|
// the client that decides the rect model and the server that pays the GPU cost is then a difference
|
||||||
|
// of two published numbers rather than something only a device can see.
|
||||||
|
//
|
||||||
|
// WHY IT IS RECORDED AND NOT GATED, and this is a scope decision rather than a hedge. ROADMAP.md:23
|
||||||
|
// puts "dirty 归属反转(按存储属主键控的发射游标)" in the P3b/P4b cell: P4a lands the flat drain
|
||||||
|
// list, and the per-storage-owner emission cursor with view/owner index remapping - which is what
|
||||||
|
// actually decides the shape for a texture uploaded through a VIEW - is the next phase's. Gating a
|
||||||
|
// shape the phase has not finished deciding would either pin today's shape as if it were the
|
||||||
|
// answer, or fail on a change that is the point of the next phase. So P4a BUILDS the scenario (it
|
||||||
|
// is meaningless without P4a's records) and runs it as a RECORDED comparison; P3b/P4b turns it into
|
||||||
|
// a gate with the Mali frame-time delta published beside it (D-D4).
|
||||||
|
//
|
||||||
|
// WHAT IT THEREFORE ASSERTS, and it is not nothing:
|
||||||
|
//
|
||||||
|
// 1. the numbers could be READ AT ALL - the counters exist, the window covers the workload, and
|
||||||
|
// the workload really uploaded (a run that uploaded nothing would record four zeroes and look
|
||||||
|
// exactly like a healthy run whose emitter had been switched off);
|
||||||
|
// 2. the internal ARITHMETIC of the server's own bracket holds: emit == box + rect, and
|
||||||
|
// jobs >= emit, because a box emission is one job and a rect-list emission is N;
|
||||||
|
// 3. the client and the server agree on the RECORD COUNT when both are non-zero (ctu == emit).
|
||||||
|
// Not "when the client is non-zero": on the P4a contract tree the client emits nothing and
|
||||||
|
// that is a SKIP-shaped observation, not a divergence.
|
||||||
|
//
|
||||||
|
// Everything else - which shape each texture took, and whether that is the right shape - is
|
||||||
|
// RECORDED with RecordProperty and printed, for MEASUREMENTS.md and for the P3b/P4b gate to be
|
||||||
|
// written against.
|
||||||
|
//
|
||||||
|
// THE WORKLOAD is the shape the decision is about: one texture receiving MANY SMALL SCATTERED
|
||||||
|
// SUB-REGIONS per frame (the sprite-atlas / chunk-renderer shape), and one receiving a single large
|
||||||
|
// contiguous one. The first is where the box-versus-rect choice is made - MipmapStorage's 96-rect
|
||||||
|
// cascade and the summedArea*4 >= unionArea*3 union-box fallback - and the second is the control
|
||||||
|
// that must always be one box whatever the policy is.
|
||||||
|
//
|
||||||
|
// DIRECTGLES ONLY. The server-side counters are Espryt's (Managers.cpp:6360-6395); Magma's upload
|
||||||
|
// path is P7 and contributes nothing to them, so a DirectVulkan lane would record a bracket of
|
||||||
|
// zeroes and call it a shape.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/PipeStatsWindow.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set by the TextureUploadShape. ctest entry and by nothing else; a harness marker, never
|
||||||
|
// read by the library.
|
||||||
|
constexpr const char* kLaneMarker = "MGITEST_TEXTURE_UPLOAD_SHAPE_LANE";
|
||||||
|
|
||||||
|
constexpr int kInset = 2;
|
||||||
|
constexpr int kAtlasSize = 64;
|
||||||
|
// Enough scattered rects that the box-versus-rect policy has a real decision to make: the
|
||||||
|
// rect cascade caps at MipmapStorage::kMaxDirtyRects = 96, and the union-box fallback fires
|
||||||
|
// on summedArea*4 >= unionArea*3, so a handful of rects would take neither branch
|
||||||
|
// interestingly.
|
||||||
|
constexpr int kScatteredRects = 40;
|
||||||
|
constexpr int kRectSize = 2;
|
||||||
|
constexpr int kFrames = 3;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = aPos * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = texture(uTex, vUv); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool BuildMarkerIsSet(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextureUploadShapeScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
|
||||||
|
glGenBuffers(1, &m_quadBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
if (m_scattered != 0) glDeleteTextures(1, &m_scattered);
|
||||||
|
if (m_contiguous != 0) glDeleteTextures(1, &m_contiguous);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkipUnlessTheLaneIsAssertableHere() {
|
||||||
|
if (std::getenv(kLaneMarker) == nullptr) {
|
||||||
|
GTEST_SKIP() << "runs only in its own lane: the TextureUploadShape. ctest entry "
|
||||||
|
"sets " << kLaneMarker
|
||||||
|
<< " together with MOBILEGL_PIPE_STATS=1, "
|
||||||
|
"MOBILEGL_PIPE_STATS_PERIOD=1 and a private "
|
||||||
|
"MOBILEGL_LOG_FILE_PATH. None of that is configured in the "
|
||||||
|
"ambient entries, and the ambient log is shared, so a read here "
|
||||||
|
"would race.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Gl().BackendName() != "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "DirectGLES only: the upload-shape counters are Espryt's "
|
||||||
|
"(Managers.cpp:6360-6395) and " << Gl().BackendName()
|
||||||
|
<< " contributes nothing to them, so this lane would record a "
|
||||||
|
"bracket of zeroes and call it a shape.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) {
|
||||||
|
GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: the client's "
|
||||||
|
"half of the comparison (CallClass::ClientTextureUploadEmissions, "
|
||||||
|
"the ctu= field) does not exist there, and a one-sided reading is "
|
||||||
|
"not the comparison this scenario is for. The entry is registered "
|
||||||
|
"in every build so that `ctest -L integration-gpu` names the same "
|
||||||
|
"tests in the pull build and the push build (gate G2).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PipeStatsWindow::LibraryLogPath().empty()) {
|
||||||
|
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's "
|
||||||
|
"summary line is the only channel this module has for reading "
|
||||||
|
"PipeStats";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeAtlas(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||||
|
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kAtlasSize) * kAtlasSize * 4);
|
||||||
|
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||||
|
texels[i] = r;
|
||||||
|
texels[i + 1] = g;
|
||||||
|
texels[i + 2] = b;
|
||||||
|
texels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kAtlasSize, kAtlasSize, 0, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image DrawSampled(GLuint texture) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
return ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_quadBuffer = 0;
|
||||||
|
GLuint m_scattered = 0;
|
||||||
|
GLuint m_contiguous = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(TextureUploadShapeScenario, TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
SkipUnlessTheLaneIsAssertableHere();
|
||||||
|
if (IsSkipped()) return;
|
||||||
|
|
||||||
|
m_scattered = MakeAtlas(0, 255, 0);
|
||||||
|
m_contiguous = MakeAtlas(0, 255, 0);
|
||||||
|
// The first draw of each texture uploads its whole level, which is not the shape this
|
||||||
|
// scenario is about; it happens in the SETUP window, before the one that is read.
|
||||||
|
DrawSampled(m_scattered);
|
||||||
|
DrawSampled(m_contiguous);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
// ---- the counted window ----
|
||||||
|
Image lastScattered;
|
||||||
|
Image lastContiguous;
|
||||||
|
for (int frame = 0; frame < kFrames; ++frame) {
|
||||||
|
// MANY SMALL SCATTERED RECTS: the shape whose box-versus-rect decision is the whole
|
||||||
|
// subject. They are spread over the atlas on a coarse stride so that their union
|
||||||
|
// box is most of the texture and their summed area is a small fraction of it -
|
||||||
|
// which is the input the summedArea*4 >= unionArea*3 fallback is written for.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_scattered);
|
||||||
|
const std::uint8_t patch[kRectSize * kRectSize * 4] = {
|
||||||
|
0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255};
|
||||||
|
for (int rect = 0; rect < kScatteredRects; ++rect) {
|
||||||
|
const int x = ((rect * 7) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||||
|
const int y = ((rect * 5) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, kRectSize, kRectSize, GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE, patch);
|
||||||
|
}
|
||||||
|
lastScattered = DrawSampled(m_scattered);
|
||||||
|
|
||||||
|
// ONE LARGE CONTIGUOUS REGION: the control. Whatever the policy is, this is one
|
||||||
|
// box and one job, and a reading where it is not says the policy has stopped
|
||||||
|
// looking at the region at all.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_contiguous);
|
||||||
|
std::vector<std::uint8_t> band(static_cast<std::size_t>(kAtlasSize) * 8 * 4);
|
||||||
|
for (std::size_t i = 0; i < band.size(); i += 4) {
|
||||||
|
band[i] = 0;
|
||||||
|
band[i + 1] = 255;
|
||||||
|
band[i + 2] = 0;
|
||||||
|
band[i + 3] = 255;
|
||||||
|
}
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kAtlasSize, 8, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
band.data());
|
||||||
|
lastContiguous = DrawSampled(m_contiguous);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the upload workload left a GL error behind";
|
||||||
|
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||||
|
|
||||||
|
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||||
|
ASSERT_TRUE(window.found)
|
||||||
|
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||||
|
<< ", so the shape could not be read at all";
|
||||||
|
RecordProperty("stats_line", window.line.c_str());
|
||||||
|
|
||||||
|
const long long emissions = PipeStatsWindow::CounterOrAbsent(window, "emit");
|
||||||
|
const long long box = PipeStatsWindow::CounterOrAbsent(window, "box");
|
||||||
|
const long long rect = PipeStatsWindow::CounterOrAbsent(window, "rect");
|
||||||
|
const long long jobs = PipeStatsWindow::CounterOrAbsent(window, "jobs");
|
||||||
|
const long long clientEmissions = PipeStatsWindow::CounterOrAbsent(window, "ctu");
|
||||||
|
ASSERT_GE(emissions, 0) << "the summary line carries no tex[emit=]: " << window.line;
|
||||||
|
ASSERT_GE(box, 0) << "no box=: " << window.line;
|
||||||
|
ASSERT_GE(rect, 0) << "no rect=: " << window.line;
|
||||||
|
ASSERT_GE(jobs, 0) << "no jobs=: " << window.line;
|
||||||
|
|
||||||
|
// THE RECORD. This is the deliverable of this scenario in P4a: a number, in the ctest
|
||||||
|
// XML and in the log, for MEASUREMENTS.md and for the P3b/P4b gate to be written
|
||||||
|
// against. Printed as well as recorded, because a RecordProperty is invisible in a
|
||||||
|
// console run.
|
||||||
|
std::cout << "[ TextureUploadShape ] backend=" << Gl().BackendName() << " frames=" << kFrames
|
||||||
|
<< " scattered_rects_per_frame=" << kScatteredRects
|
||||||
|
<< " server[emit=" << emissions << " box=" << box << " rect=" << rect
|
||||||
|
<< " jobs=" << jobs << "] client[ctu=" << clientEmissions << "]" << std::endl;
|
||||||
|
RecordProperty("server_emissions", static_cast<int>(emissions));
|
||||||
|
RecordProperty("server_box_emissions", static_cast<int>(box));
|
||||||
|
RecordProperty("server_rect_emissions", static_cast<int>(rect));
|
||||||
|
RecordProperty("server_upload_jobs", static_cast<int>(jobs));
|
||||||
|
RecordProperty("client_emissions", static_cast<int>(clientEmissions));
|
||||||
|
|
||||||
|
// 1. the workload really uploaded. Without this the three assertions below are all
|
||||||
|
// 0 == 0 and a run whose emitter was switched off records the same "healthy" shape
|
||||||
|
// as one that worked.
|
||||||
|
ASSERT_GT(emissions, 0)
|
||||||
|
<< "the server counted no texture upload emission at all over " << kFrames
|
||||||
|
<< " frames of " << kScatteredRects
|
||||||
|
<< " sub-regions each plus a contiguous band. Either the uploads never reached the "
|
||||||
|
"backend or the counter stopped counting; in both cases every shape number below "
|
||||||
|
"would be a zero that means nothing. "
|
||||||
|
<< window.line;
|
||||||
|
|
||||||
|
// 2. the server bracket's own arithmetic.
|
||||||
|
EXPECT_EQ(box + rect, emissions)
|
||||||
|
<< "tex[box=] + tex[rect=] must be tex[emit=]: every emission takes exactly one of "
|
||||||
|
"the two shapes. " << window.line;
|
||||||
|
EXPECT_GE(jobs, emissions)
|
||||||
|
<< "tex[jobs=] must be at least tex[emit=]: a box emission is one driver upload job "
|
||||||
|
"and a rect-list emission is N. " << window.line;
|
||||||
|
|
||||||
|
// 3. the two sides agree, WHEN THERE ARE TWO SIDES - and "there are two sides" is
|
||||||
|
// answered by the BUILD, not by the number (review F-m7).
|
||||||
|
//
|
||||||
|
// ctu= IS ALWAYS PRESENT IN A PUSH BUILD: PipeStats.cpp writes the field whether or
|
||||||
|
// not anything ever incremented the counter, so `clientEmissions > 0` conflated
|
||||||
|
// three different trees - "no client emitter exists", "the emitter exists and
|
||||||
|
// emitted nothing", and "the counter was not published at all" - into one branch
|
||||||
|
// that asserts nothing and prints a sentence that is only true of the first. Once
|
||||||
|
// package B's texture emitter lands, an emitter that STOPPED emitting would read
|
||||||
|
// exactly like no emitter at all and this case would have gone green over it, which
|
||||||
|
// is the failure mode the whole scenario exists to make impossible.
|
||||||
|
//
|
||||||
|
// So the discriminator is MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT, the
|
||||||
|
// build's own content probe for a MG_Impl/Pipe source that emits
|
||||||
|
// CallClass::ClientTextureUploadEmissions - the same mechanism as every other arming
|
||||||
|
// decision in this package - and each side of it asserts something real.
|
||||||
|
const bool clientEmitterExists =
|
||||||
|
BuildMarkerIsSet("MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT");
|
||||||
|
ASSERT_GE(clientEmissions, 0)
|
||||||
|
<< "the summary line carries no ctu= field at all, in a push build, where PipeStats "
|
||||||
|
"publishes it unconditionally. The client half of the comparison cannot be read: "
|
||||||
|
<< window.line;
|
||||||
|
RecordProperty("client_emitter_present", clientEmitterExists ? 1 : 0);
|
||||||
|
if (clientEmitterExists) {
|
||||||
|
EXPECT_GT(clientEmissions, 0)
|
||||||
|
<< "a MG_Impl/Pipe source emits CallClass::ClientTextureUploadEmissions on this "
|
||||||
|
"tree, and the SERVER counted " << emissions
|
||||||
|
<< " texture upload emissions for this workload, but the client counted NONE. An "
|
||||||
|
"emitter that has stopped emitting reads exactly like no emitter at all in "
|
||||||
|
"this field, which is why this case asks the build rather than the number. "
|
||||||
|
<< window.line;
|
||||||
|
EXPECT_EQ(clientEmissions, emissions)
|
||||||
|
<< "the CLIENT counted " << clientEmissions
|
||||||
|
<< " texture upload records and the SERVER counted " << emissions
|
||||||
|
<< " for the same workload in the same window. The two counting the same records "
|
||||||
|
"is the entire reason both are published (D-L): a divergence here is an "
|
||||||
|
"emission-shape divergence that SSIM is blind to and that costs ~+6 ms/frame "
|
||||||
|
"on Mali when it goes the wrong way. "
|
||||||
|
<< window.line;
|
||||||
|
} else {
|
||||||
|
// Not merely "not asserted": on a tree with no client emitter the counter must be
|
||||||
|
// ZERO, and a non-zero one would mean the probe is looking for the wrong symbol -
|
||||||
|
// i.e. that the arming decision above is wrong and every future run of this case
|
||||||
|
// is mis-armed.
|
||||||
|
EXPECT_EQ(clientEmissions, 0)
|
||||||
|
<< "no MG_Impl/Pipe source emits CallClass::ClientTextureUploadEmissions on this "
|
||||||
|
"tree, yet the client counted " << clientEmissions
|
||||||
|
<< " of them. Something is incrementing that counter which this build's probe "
|
||||||
|
"cannot see, so the probe is looking for the wrong symbol and this case's "
|
||||||
|
"arming decision is unreliable in both directions. " << window.line;
|
||||||
|
std::cout << "[ TextureUploadShape ] no P4a client emitter has landed on this tree "
|
||||||
|
"(the build's ClientTextureUploadEmissions probe found none), so this "
|
||||||
|
"run records the SERVER shape only and pins ctu=0. That is the expected "
|
||||||
|
"reading on the contract tree and it is not a divergence."
|
||||||
|
<< std::endl;
|
||||||
|
RecordProperty("client_side", "absent");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pixels, so that a recorded shape cannot be the shape of a workload that drew
|
||||||
|
// nothing.
|
||||||
|
EXPECT_TRUE(RegionIsMostly(lastScattered, kInset, lastScattered.Width() - kInset, kInset,
|
||||||
|
lastScattered.Height() - kInset, "green", 0.0,
|
||||||
|
"the scattered-rect atlas"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(lastContiguous, kInset, lastContiguous.Width() - kInset, kInset,
|
||||||
|
lastContiguous.Height() - kInset, "green", 0.0,
|
||||||
|
"the contiguous-band atlas"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -34,11 +34,39 @@
|
|||||||
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
|
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
|
||||||
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
|
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
|
||||||
X(GetBoundVertexArray, BindVertexElements) \
|
X(GetBoundVertexArray, BindVertexElements) \
|
||||||
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
|
/* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \
|
||||||
/* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \
|
/* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \
|
||||||
/* inventory is re-vendored carrying the target argument (deferred out of P1: */ \
|
/* target argument), but by supplying the target from the EMISSION SITE, which */ \
|
||||||
/* the extractor lives in MobileGL-CS). Named for the plan's explicit */ \
|
/* knows it exactly. The split, target by target, in BufferTarget's OWN */ \
|
||||||
/* replacement of the DrawIndirect/Parameter pair. */ \
|
/* 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. */ \
|
||||||
X(GetBufferBindingSlot, SetIndirectBuffers) \
|
X(GetBufferBindingSlot, SetIndirectBuffers) \
|
||||||
X(GetBufferBindingPoint, SetShaderBuffers) \
|
X(GetBufferBindingPoint, SetShaderBuffers) \
|
||||||
X(GetBufferBindingPointCount, SetShaderBuffers) \
|
X(GetBufferBindingPointCount, SetShaderBuffers) \
|
||||||
@@ -71,9 +99,10 @@
|
|||||||
X(GetProgramForDispatch, SetDispatchProgram) \
|
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||||
X(GetProgramForDraw, SetDrawProgram) \
|
X(GetProgramForDraw, SetDrawProgram) \
|
||||||
X(GetProgramObject, CreateShaderState) \
|
X(GetProgramObject, CreateShaderState) \
|
||||||
/* Not in ComputePipelineStateHash today even though Vulkan makes it pipeline */ \
|
/* ANSWERED by P2: it is in the pipeline half. SetProvokingVertexMode calls */ \
|
||||||
/* state; recorded here so the G7 chunk table has to answer for it before it */ \
|
/* BumpVersions(), and the chunk table's rule is exactly that, so it rides */ \
|
||||||
/* freezes (section 10.3-5). */ \
|
/* pipeline chunk P4 - a strict superset of what ComputePipelineStateHash used */ \
|
||||||
|
/* to hash (MGPipeRenderStateSpans.cpp records the provenance). */ \
|
||||||
X(GetProvokingVertexMode, CreateRenderState) \
|
X(GetProvokingVertexMode, CreateRenderState) \
|
||||||
X(GetRenderStateParameters, CreateRenderState) \
|
X(GetRenderStateParameters, CreateRenderState) \
|
||||||
X(GetRenderStateParametersVersion, BindRenderState) \
|
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||||
@@ -128,4 +157,124 @@
|
|||||||
X(handle-ify (wire handle), kStructuralHandle) \
|
X(handle-ify (wire handle), kStructuralHandle) \
|
||||||
X(Buffer ops delta, ResourceRespecify)
|
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 IS A DIFFERENT ARGUMENT AND IT IS WORTH WRITING DOWN, because it looks like the
|
||||||
|
// easy one. GetMaxTouchedTextureUnit is a plain Int, and set_sampler_views' Count IS that
|
||||||
|
// value plus one (the second merge rule: a high-water mark is directly the count argument).
|
||||||
|
// But the set is SUPPRESSED on an unchanged content hash and is emitted only when bit 12 fires,
|
||||||
|
// and bit 12's shutter is Mix(textureContent, GetTextureBindGeneration()) - which does NOT
|
||||||
|
// move on a redundant re-bind of the object a unit already holds, while the high-water mark
|
||||||
|
// DOES (see NoteUnitTouched in DirtySurface.def). So the applier's Count can lag the frontend's
|
||||||
|
// high-water mark by exactly the case the suppressor exists to swallow, and the field keeps
|
||||||
|
// being pulled. Narrowing that is P3b/P4b's, with the backend debounce it takes over.
|
||||||
|
//
|
||||||
|
// FOUR ACCESSORS THAT MAP TO A P4a CALL ARE DELIBERATELY NOT HERE, for GetPixelStoreParameters'
|
||||||
|
// reason - a row here says "this field is supplied", and for these it would be a half-truth:
|
||||||
|
// GetActiveTextureUnit - glActiveTexture's selector. set_sampler_views carries the RESOLVED
|
||||||
|
// per-unit set and no active-unit selector at all; nothing on the wire carries it.
|
||||||
|
// GetTextureContextId - a context identity the backend keys its own tables on. No call
|
||||||
|
// carries it and none should: it is the server's question about the client, not state.
|
||||||
|
// GetTextureBindGeneration / GetSamplingResolutionGeneration - frontend SHUTTERS. What
|
||||||
|
// replaces them server-side is the applier's own Serial, which is a different value with a
|
||||||
|
// different owner; claiming the sets supply the generations would make the fill loop 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.
|
||||||
|
#define MGP_COVERAGE_EMITTED_LIST(X) \
|
||||||
|
X(GetBlendColor, SetDynamicState) \
|
||||||
|
X(GetBlendEquationIndexed, CreateRenderState) \
|
||||||
|
X(GetBlendFuncIndexed, CreateRenderState) \
|
||||||
|
X(GetBoundVertexArray, BindVertexElements) \
|
||||||
|
X(GetClampReadColor, SetDynamicState) \
|
||||||
|
X(GetClearColor, SetDynamicState) \
|
||||||
|
X(GetClearDepth, SetDynamicState) \
|
||||||
|
X(GetClearStencil, SetDynamicState) \
|
||||||
|
X(GetColorMaskIndexed, CreateRenderState) \
|
||||||
|
X(GetCullFaceMode, CreateRenderState) \
|
||||||
|
X(GetCurrentVertexAttribute, SetVertexAttribDefaults) \
|
||||||
|
X(GetDepthFunc, CreateRenderState) \
|
||||||
|
X(GetDepthMask, CreateRenderState) \
|
||||||
|
X(GetDepthRangeIndexed, SetDynamicState) \
|
||||||
|
X(GetFramebufferBindingSlot, SetFramebufferState) \
|
||||||
|
X(GetImageTextureBinding, SetShaderImages) \
|
||||||
|
X(GetLineWidth, SetDynamicState) \
|
||||||
|
X(GetLogicOp, CreateRenderState) \
|
||||||
|
X(GetMaxTouchedTextureUnit, SetSamplerViews) \
|
||||||
|
X(GetMinSampleShadingValue, CreateRenderState) \
|
||||||
|
X(GetPatchDefaultInnerLevel, SetPatchState) \
|
||||||
|
X(GetPatchDefaultOuterLevel, SetPatchState) \
|
||||||
|
X(GetPatchVertices, SetPatchState) \
|
||||||
|
X(GetPipelineStateVersion, BindRenderState) \
|
||||||
|
X(GetPolygonModeFront, CreateRenderState) \
|
||||||
|
X(GetPolygonOffsetFactor, SetDynamicState) \
|
||||||
|
X(GetPolygonOffsetUnits, SetDynamicState) \
|
||||||
|
X(GetPrimitiveRestartIndex, SetDynamicState) \
|
||||||
|
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||||
|
X(GetProgramForDraw, SetDrawProgram) \
|
||||||
|
X(GetProvokingVertexMode, CreateRenderState) \
|
||||||
|
X(GetRenderStateParameters, CreateRenderState) \
|
||||||
|
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||||
|
X(GetScissorBox, SetDynamicState) \
|
||||||
|
X(GetStencilState, CreateRenderState) \
|
||||||
|
X(GetTextureUnitObject, SetSamplerViews) \
|
||||||
|
X(GetViewport, SetDynamicState) \
|
||||||
|
X(GetViewportIndexed, SetDynamicState) \
|
||||||
|
X(IsCapabilityEnabled, CreateRenderState) \
|
||||||
|
X(IsCapabilityEnabledIndexed, CreateRenderState)
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Pipe/DirtySurface.def
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
// X(Mutator, Answer)
|
||||||
|
#define MGP_DIRTY_SURFACE_LIST(X) \
|
||||||
|
/* ---- the reverse channel: 836 of the 926 calls, 90% of the surface ---- */ \
|
||||||
|
X(RecordError, kReverseChannel) \
|
||||||
|
/* ---- immediate publish points: the same body reaches the backend ---- */ \
|
||||||
|
X(SetActiveTextureUnit, kImmediate) \
|
||||||
|
X(BeginTransformFeedback, kImmediate) \
|
||||||
|
X(EndTransformFeedback, kImmediate) \
|
||||||
|
X(SetTransformFeedbackPaused, kImmediate) \
|
||||||
|
X(MarkTransformFeedbackObjectForDeletion, kImmediate) \
|
||||||
|
/* ---- the render state. Derived from RenderState.cpp and gated by --check: */ \
|
||||||
|
/* a setter that calls BumpVersions() on every path publishes BOTH counters; */ \
|
||||||
|
/* one that also has a bare ++m_version path publishes only NEW_RENDER_STATE. */ \
|
||||||
|
X(SetBlendEquation, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetBlendEquationIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetBlendFunc, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetBlendFuncIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
/* SetCapability's ClipDistance0..7 arms write ClipDistanceEnabledMask (dynamic */ \
|
||||||
|
/* chunk D7) and deliberately do NOT BumpVersions, so NEW_PIPELINE_STATE does */ \
|
||||||
|
/* not fire at all for glEnable(GL_CLIP_DISTANCE0): set_dynamic_state publishes */ \
|
||||||
|
/* it, and NEW_RENDER_STATE is the only answer that holds on every arm. */ \
|
||||||
|
X(SetCapability, NEW_RENDER_STATE) \
|
||||||
|
X(SetCapabilityIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetColorMask, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetColorMaskIndexed, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetCullFaceMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetDepthFunc, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetDepthMask, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetFrontFaceMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetLogicOp, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetMinSampleShadingValue, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetPolygonMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetProvokingVertexMode, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetSampleCoverage, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetSampleMaskValue, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
/* SetStencilFunc writes Func (pipeline chunk P2/P3) AND Ref/ValueMask (dynamic */ \
|
||||||
|
/* D3/D4), and ++m_pipelineStateVersion is CONDITIONAL on Func moving - which is */ \
|
||||||
|
/* what keeps a glStencilFunc that moves only the reference from evicting a */ \
|
||||||
|
/* cached pipeline, and is why only NEW_RENDER_STATE fires on every call. */ \
|
||||||
|
/* SetStencilOp is wholly pipeline, SetStencilMask wholly dynamic. */ \
|
||||||
|
X(SetStencilFunc, NEW_RENDER_STATE) \
|
||||||
|
X(SetStencilOp, NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetStencilMask, NEW_RENDER_STATE) \
|
||||||
|
X(SetBlendColor, NEW_RENDER_STATE) \
|
||||||
|
X(SetClampReadColor, NEW_RENDER_STATE) \
|
||||||
|
X(SetClearColor, NEW_RENDER_STATE) \
|
||||||
|
X(SetClearDepth, NEW_RENDER_STATE) \
|
||||||
|
X(SetClearStencil, NEW_RENDER_STATE) \
|
||||||
|
X(SetClipControl, NEW_RENDER_STATE) \
|
||||||
|
X(SetDepthRange, NEW_RENDER_STATE) \
|
||||||
|
X(SetDepthRangeIndexed, NEW_RENDER_STATE) \
|
||||||
|
X(SetHint, NEW_RENDER_STATE) \
|
||||||
|
X(SetLineWidth, NEW_RENDER_STATE) \
|
||||||
|
X(SetPointFadeThresholdSize, NEW_RENDER_STATE) \
|
||||||
|
X(SetPointSize, NEW_RENDER_STATE) \
|
||||||
|
X(SetPointSpriteCoordOrigin, NEW_RENDER_STATE) \
|
||||||
|
X(SetPolygonOffset, NEW_RENDER_STATE) \
|
||||||
|
X(SetPolygonOffsetClamped, NEW_RENDER_STATE) \
|
||||||
|
X(SetPrimitiveRestartIndex, NEW_RENDER_STATE) \
|
||||||
|
X(SetScissorBox, NEW_RENDER_STATE) \
|
||||||
|
X(SetScissorBoxIndexed, NEW_RENDER_STATE) \
|
||||||
|
X(SetViewport, NEW_RENDER_STATE) \
|
||||||
|
X(SetViewportIndexed, NEW_RENDER_STATE) \
|
||||||
|
/* ---- the other value-class bits ---- */ \
|
||||||
|
/* kPulledPartialShutter, NOT kPulledEveryVerb, and NOT a bare NEW_PIXEL_PACK: */ \
|
||||||
|
/* RenderState::SetPixelStoreParam writes BOTH halves - eight Pack arms and eight */ \
|
||||||
|
/* Unpack arms - while the tracker's bit 2 is a byte compare of the PACK half alone */ \
|
||||||
|
/* (Tracker.h), because set_pixel_pack_state deliberately has no unpack counterpart */ \
|
||||||
|
/* (ARCHITECTURE.md 4.6). So glPixelStorei(GL_PACK_ALIGNMENT, 8) DOES move bit 2 and */ \
|
||||||
|
/* glPixelStorei(GL_UNPACK_ALIGNMENT, 8) moves nothing at all, and a shutter narrowed */ \
|
||||||
|
/* to bit 2 would under-fire for eight of the sixteen arms. What is true on every path */ \
|
||||||
|
/* is the pull: GetPixelStoreParameters is one of the two Coverage.def rows an emitted */ \
|
||||||
|
/* call does not supply completely (PipeFill.cpp), so the residual fill copies both */ \
|
||||||
|
/* halves at every verb of the class. The bit is named anyway because P2 already EMITS */ \
|
||||||
|
/* set_pixel_pack_state off it: a row that said "no shutter exists" about the only */ \
|
||||||
|
/* mutator behind a shipped call would be a false answer to the one question D16 hands */ \
|
||||||
|
/* P3a. Splitting this setter into a pack half and an unpack half is what would let the */ \
|
||||||
|
/* pack half answer NEW_PIXEL_PACK outright; that is P3's move, not P2's. */ \
|
||||||
|
X(SetPixelStoreParam, kPulledPartialShutter|NEW_PIXEL_PACK) \
|
||||||
|
X(SetPatchDefaultInnerLevel, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetPatchDefaultOuterLevel, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
/* Also an immediate publish point, but it has a real bit and the bit is */ \
|
||||||
|
/* the more useful answer: set_patch_state carries it whatever the caller */ \
|
||||||
|
/* does next. */ \
|
||||||
|
X(SetPatchVertices, NEW_PATCH_STATE|NEW_RENDER_STATE|NEW_PIPELINE_STATE) \
|
||||||
|
X(SetCurrentVertexAttributeFloat, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
||||||
|
X(SetCurrentVertexAttributeInt, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
||||||
|
X(SetCurrentVertexAttributeUint, NEW_VERTEX_ATTRIB_DEFAULTS) \
|
||||||
|
/* ---- object class ---- */ \
|
||||||
|
X(BumpTextureBindGeneration, NEW_SAMPLER_VIEWS) \
|
||||||
|
/* P3a, FOUND BY THE WIDENED SCAN ROOT and not by anything before it: every caller of */ \
|
||||||
|
/* this one is inside MG_State/GLState (SamplerObject::BumpVersion for any sampler */ \
|
||||||
|
/* parameter, TextureObjectBase's shape bump), which the scan did not read, so it had */ \
|
||||||
|
/* no row at all while its sibling above did. NEW_SAMPLERS is the bit whose shutter */ \
|
||||||
|
/* mixes the texture-params aggregate with exactly this generation, and the bump is */ \
|
||||||
|
/* unconditional on every path that reaches it - the early-outs are in the setters */ \
|
||||||
|
/* above it, which is the redundant-write guard this file's rule explicitly allows. */ \
|
||||||
|
X(BumpSamplingResolutionGeneration, NEW_SAMPLERS) \
|
||||||
|
/* P3a, and the reason the scan now reads MGP_NOTE_MUTATION as a publish mechanism of */ \
|
||||||
|
/* its own: this function is not a pGLContext-> mutator at all - every texture and */ \
|
||||||
|
/* sampler bind entry point routes THROUGH it - and it moves two pushed PipeInputs */ \
|
||||||
|
/* fields with two different answers. */ \
|
||||||
|
/* GetTextureBindGeneration moves only on the bindingChanged arm, so NEW_SAMPLER_ */ \
|
||||||
|
/* VIEWS fires on SOME of the paths that mutate and not all: a redundant re-bind */ \
|
||||||
|
/* of the object a unit already holds advances the high-water mark alone. That is */ \
|
||||||
|
/* precisely what kPulledPartialShutter is for. */ \
|
||||||
|
/* GetMaxTouchedTextureUnit has no shutter at all; it is in its verb class's may-read */ \
|
||||||
|
/* mask and the residual fill copies it at every verb. */ \
|
||||||
|
/* Both are ALSO published inline by MGP_NOTE_MUTATION when the write happens inside a */ \
|
||||||
|
/* verb already in flight - a backend binding its own synthesised fallback texture - */ \
|
||||||
|
/* which is the window no shutter and no pull can cover, and the reason those sites */ \
|
||||||
|
/* had to come inside this gate rather than stay a recorded blind spot. */ \
|
||||||
|
X(NoteUnitTouched, kPulledPartialShutter|NEW_SAMPLER_VIEWS) \
|
||||||
|
/* NOT NEW_SO_TARGETS, and this one was false on EVERY path: GLContext::SetNamed */ \
|
||||||
|
/* TransformFeedbackBinding either binds a BufferState binding point (index == the */ \
|
||||||
|
/* bound XFB object) or writes a saved-bindings entry, and NEW_SO_TARGETS mixes the */ \
|
||||||
|
/* buffer-CONTENT aggregate with the transform-feedback generation - the first moves */ \
|
||||||
|
/* only at BufferObject.cpp's content sites, the second only in BeginTransformFeedback. */ \
|
||||||
|
/* A binding moves neither. It reaches the backend the same way every other buffer */ \
|
||||||
|
/* binding point does, through GetBufferBindingPoint in the verb class's may-read mask, */ \
|
||||||
|
/* so the honest answer is the pull. Narrowing it is P3b's, when it takes the subsystem */ \
|
||||||
|
/* over and the binding points get a generation of their own. */ \
|
||||||
|
X(SetNamedTransformFeedbackBinding, 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 */ \
|
||||||
|
/* begins with one of the ten words the pattern matched. */ \
|
||||||
|
/* UseProgram is bit 6's whole subject: the shutter is */ \
|
||||||
|
/* Mix(GetCurrentProgram()->GetLifetimeId(), GetLinkVersion()) and glUseProgram is */ \
|
||||||
|
/* what moves the object it reads through. Two call sites. AND SINCE THE FABLE */ \
|
||||||
|
/* SEAM ROUND (F-1 / F-2) IT IS BITS 12 AND 14's TOO: set_sampler_views is */ \
|
||||||
|
/* resolved for the program in use and set_shader_images' window is the highest */ \
|
||||||
|
/* image unit the program in use names, so both shutters mix the same identity */ \
|
||||||
|
/* bit 6 reads, and a glUseProgram alone moves all three. Undecided for the same */ \
|
||||||
|
/* reason as bit 6 (the taint below), marked the same way. */ \
|
||||||
|
/* BindVertexArray is bit 5's, for the same reason one level down: the shutter mixes */ \
|
||||||
|
/* the bound VAO's identity with its configuration version, and this is the bind. */ \
|
||||||
|
/* Three call sites. */ \
|
||||||
|
X(UseProgram, NEW_SHADER|NEW_SAMPLER_VIEWS|NEW_SHADER_IMAGES) \
|
||||||
|
X(BindVertexArray, NEW_VERTEX_ELEMENTS) \
|
||||||
|
/* NOT NEW_SHADER, and the derivation refutes it outright rather than leaving it a */ \
|
||||||
|
/* judgement: this mutator writes m_boundProgramPipeline (plus the pipeline name table) */ \
|
||||||
|
/* and bit 6's shutter reads m_currentProgram's lifetime id and link version - disjoint */ \
|
||||||
|
/* sets, on every path. That is not an oversight in the shutter either: it reads */ \
|
||||||
|
/* GetCurrentProgram() and DELIBERATELY NOT GetProgramForDraw(), because the tracker */ \
|
||||||
|
/* must not force a compile just to answer "did the shader move", and flattening a */ \
|
||||||
|
/* pipeline into its composite is exactly the compile it would force. What a bind moves */ \
|
||||||
|
/* is which program the validate point will flatten, and that field - */ \
|
||||||
|
/* GetProgramForDraw - is in the may-read mask of every class that draws and is copied */ \
|
||||||
|
/* by the residual fill at every verb of those classes, EMITTED-AND-STILL-PULLED like */ \
|
||||||
|
/* GetBoundVertexArray. So the pull is what holds on every path, and it is the answer. */ \
|
||||||
|
X(BindProgramPipelineObject, kPulledEveryVerb) \
|
||||||
|
/* No shutter at all, and none is needed: the transform-feedback binding reaches the */ \
|
||||||
|
/* backend through GetBoundTransformFeedbackLifetimeId and its siblings, which are in */ \
|
||||||
|
/* 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. */ \
|
||||||
|
X(MarkBufferObjectForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkProgramForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkProgramPipelineForDeletion, kUnpublishedDestroy) \
|
||||||
|
X(MarkRenderbufferObjectForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkSamplerObjectForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkShaderForDeletion, kUnpublishedDestroy) \
|
||||||
|
X(MarkTextureObjectForDeletion, kExplicitDestroy) \
|
||||||
|
X(MarkVertexArrayForDeletion, kExplicitDestroy) \
|
||||||
|
/* ---- no backend read point observes these at all ---- */ \
|
||||||
|
/* GL_ANY_SAMPLES_PASSED conditional rendering is resolved wholly in the */ \
|
||||||
|
/* frontend: IsConditionalRenderActive / GetConditionalRenderQuery have no */ \
|
||||||
|
/* reader under MG_Backend and no Coverage.def row. */ \
|
||||||
|
X(BeginConditionalRender, kNoBackendRead) \
|
||||||
|
X(EndConditionalRender, kNoBackendRead) \
|
||||||
|
/* ---- pulled at every verb of the class, so the next verb publishes them */ \
|
||||||
|
/* unconditionally. The transform-feedback accounting counters reach the */ \
|
||||||
|
/* backend through GetTransformFeedbackCapturedVertices and friends, which */ \
|
||||||
|
/* are in the kDraw and kXfbSpan may-read masks. */ \
|
||||||
|
X(AddTransformFeedbackAccountedCaptureDraw, kPulledEveryVerb) \
|
||||||
|
X(AddTransformFeedbackCapturedVertices, kPulledEveryVerb) \
|
||||||
|
X(AddTransformFeedbackGeometryCaptureDraw, kPulledEveryVerb) \
|
||||||
|
X(AddTransformFeedbackInputPrimitives, kPulledEveryVerb) \
|
||||||
|
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.
|
||||||
|
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X) \
|
||||||
|
X(UseProgram, NEW_SHADER) \
|
||||||
|
X(UseProgram, NEW_SAMPLER_VIEWS) \
|
||||||
|
X(UseProgram, NEW_SHADER_IMAGES) \
|
||||||
|
X(BindVertexArray, NEW_VERTEX_ELEMENTS)
|
||||||
|
|
||||||
|
// clang-format on
|
||||||
@@ -26,6 +26,50 @@
|
|||||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
|
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
|
||||||
// row, never by marking the field sticky.
|
// 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.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
|
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
|
||||||
//
|
//
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|||||||
@@ -55,10 +55,75 @@ namespace MobileGL::MG_Pipe {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
|
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
|
||||||
// 4.5.2). Generated by G7 from the field list ComputePipelineStateHash already hashes;
|
// 4.5.2): MG_Pipe/MGPipeRenderStateSpans.{h,cpp}, which landed with P2 and computes
|
||||||
// MGPipeRenderStateSpans.cpp and the setter-consistency test land with P2, which is
|
// every chunk boundary with offsetof. Include that header to use it; what stays here
|
||||||
// when the chunk table can be filled with real offsets.
|
// is the generated member list at the bottom of this file, which is what the chunk
|
||||||
struct MGPipeRenderStateSpans;
|
// table was derived from.
|
||||||
|
|
||||||
|
// ---- MOBILEGL_PIPE_PUSH's runtime bitmask (Config.h Features.PipePush) ----
|
||||||
|
//
|
||||||
|
// One bit per SUBSYSTEM, so an A/B is per subsystem rather than all-or-nothing, and
|
||||||
|
// bit 63 for the one BEHAVIOUR the design has to be measured against. Bits are
|
||||||
|
// allocated in ROADMAP order and never reused: an operator's recorded 0x7f has to keep
|
||||||
|
// meaning what it meant.
|
||||||
|
//
|
||||||
|
// A clear subsystem bit means "keep pulling", which after P2 is only a valid control
|
||||||
|
// while MOBILEGL_PIPE_LEGACY_MEMOS compiles the pre-handle arm beside it.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemRenderState = 1ull << 0;
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemPixelPack = 1ull << 1;
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemPatchState = 1ull << 2;
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemVertexAttribDefaults = 1ull << 3;
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemResidualValues = 1ull << 4;
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemEsprytSlots = 1ull << 5; // Track H, Espryt 0b
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemMagmaVertexInput = 1ull << 6; // Track H, Magma subsystem 4
|
||||||
|
// P3a's two. Resources is the seven BufferBackendOps hooks turned into the handle-shaped
|
||||||
|
// resource_* family; VertexInput is vertex elements, vertex buffers and the index buffer.
|
||||||
|
// They are separate bits because they are separate A/Bs: a buffer path that regressed and
|
||||||
|
// a vertex path that regressed are different findings, and clearing one must not disarm
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// THREE OF THEM HAVE 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, modelled on the bit-8-requires-bit-7 refusal it already
|
||||||
|
// ships, and lazy rather than at bring-up because a pre-flight child dying on a signal
|
||||||
|
// makes a whole lane SKIP green: bit 11 requires bit 10 because
|
||||||
|
// every MGPBoundView::Texture and MGPImageView::Res names a Texture handle and only bit 10
|
||||||
|
// puts one in the slot table; bit 9 requires bit 10 because MGPSurface::Res does; and bit
|
||||||
|
// 10 requires bit 7 because a buffer texture's BufferForTexBuffer names a Buffer handle.
|
||||||
|
// The mirror pairs (10 without 11, 10 without 9, 7 without 10) are all fine, and are
|
||||||
|
// stated as such because an unreachable branch that says something different is how the
|
||||||
|
// reachable one drifts. Bit 12 depends on nothing.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemFramebuffer = 1ull << 9; // set_framebuffer_state
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemTextureResources = 1ull << 10; // texture + renderbuffer
|
||||||
|
// resource_*, set_texture_params
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemSamplers = 1ull << 11; // sampler CSO, sampler view,
|
||||||
|
// the three unit sets
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemPrograms = 1ull << 12; // shader CSO, draw/dispatch
|
||||||
|
// program, global constants
|
||||||
|
// bits 13..62 reserved for the later phases, allocated in ROADMAP order.
|
||||||
|
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
|
||||||
|
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
|
||||||
|
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
|
||||||
|
inline constexpr Uint64 kMGPipeBehaviourNoCsoContentAddressing = 1ull << 63;
|
||||||
|
// The default of a push build with the knob unset (ConfigLoader.cpp). Each phase's
|
||||||
|
// constant STAYS, because it is the A/B control for the phase after it: P3a's
|
||||||
|
// "everything P2 had and nothing of mine" arm is spelled MOBILEGL_PIPE_PUSH=0x7f.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8
|
||||||
|
// P4a's, and the two above are NOT edited: 0x1ff is P4a's T2 arm and its "everything P3a
|
||||||
|
// had and nothing of mine" control, exactly as 0x7f was P3a's.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP4a = 0x1fffull; // bits 0..12
|
||||||
|
static_assert(kMGPipeSubsystemsMigratedAtP4a ==
|
||||||
|
(kMGPipeSubsystemsMigratedAtP3a | kMGPipeSubsystemFramebuffer |
|
||||||
|
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers |
|
||||||
|
kMGPipeSubsystemPrograms),
|
||||||
|
"the P4a phase constant and P4a's four subsystem bits have drifted");
|
||||||
|
|
||||||
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
|
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
|
||||||
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// a pipeline object, and the server never learns it is a composite - it is just another
|
// 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
|
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
||||||
// lifetime bookkeeping out of the ordinary program slot allocator.
|
// 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.
|
||||||
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
||||||
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
||||||
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The 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.
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
namespace {
|
||||||
|
// Half-local chunk index -> global chunk index. The halves alternate, so this is
|
||||||
|
// arithmetic rather than a table.
|
||||||
|
constexpr SizeT GlobalPipelineChunk(SizeT halfIndex) { return halfIndex * 2 + 1; }
|
||||||
|
constexpr SizeT GlobalDynamicChunk(SizeT halfIndex) { return halfIndex * 2; }
|
||||||
|
|
||||||
|
const Uint8* BytesOf(const RenderStateParameters& params) {
|
||||||
|
return reinterpret_cast<const Uint8*>(¶ms);
|
||||||
|
}
|
||||||
|
Uint8* BytesOf(RenderStateParameters& params) { return reinterpret_cast<Uint8*>(¶ms); }
|
||||||
|
|
||||||
|
SizeT BlobBytes(Uint32 chunkMask, SizeT halfCount, SizeT (*toGlobal)(SizeT)) {
|
||||||
|
SizeT total = 0;
|
||||||
|
for (SizeT i = 0; i < halfCount; ++i) {
|
||||||
|
if ((chunkMask & (1u << i)) == 0) continue;
|
||||||
|
total += MGPipeRenderStateChunkAt(toGlobal(i)).Length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gather(const RenderStateParameters& params, Uint32 chunkMask, void* dst, SizeT halfCount,
|
||||||
|
SizeT (*toGlobal)(SizeT)) {
|
||||||
|
Uint8* out = static_cast<Uint8*>(dst);
|
||||||
|
const Uint8* src = BytesOf(params);
|
||||||
|
for (SizeT i = 0; i < halfCount; ++i) {
|
||||||
|
if ((chunkMask & (1u << i)) == 0) continue;
|
||||||
|
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
|
||||||
|
std::memcpy(out, src + chunk.Offset, chunk.Length);
|
||||||
|
out += chunk.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scatter(const void* src, Uint32 chunkMask, RenderStateParameters& dst, SizeT halfCount,
|
||||||
|
SizeT (*toGlobal)(SizeT)) {
|
||||||
|
const Uint8* in = static_cast<const Uint8*>(src);
|
||||||
|
Uint8* out = BytesOf(dst);
|
||||||
|
for (SizeT i = 0; i < halfCount; ++i) {
|
||||||
|
if ((chunkMask & (1u << i)) == 0) continue;
|
||||||
|
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
|
||||||
|
std::memcpy(out + chunk.Offset, in, chunk.Length);
|
||||||
|
in += chunk.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 ChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b,
|
||||||
|
SizeT halfCount, SizeT (*toGlobal)(SizeT)) {
|
||||||
|
const Uint8* left = BytesOf(a);
|
||||||
|
const Uint8* right = BytesOf(b);
|
||||||
|
Uint32 mask = 0;
|
||||||
|
for (SizeT i = 0; i < halfCount; ++i) {
|
||||||
|
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
|
||||||
|
if (std::memcmp(left + chunk.Offset, right + chunk.Offset, chunk.Length) != 0) {
|
||||||
|
mask |= 1u << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Uint32 AllChunks(SizeT halfCount) {
|
||||||
|
return halfCount >= 32 ? ~Uint32{0} : static_cast<Uint32>((Uint64{1} << halfCount) - 1);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The two arrays generated/PipeSpanTable.inc declares. Every entry is
|
||||||
|
// MGPipeRenderStateChunkAt(), so a boundary can only be written once.
|
||||||
|
const MGPStateChunk kMGPipePipelineChunks[kMGPipePipelineChunkCount] = {
|
||||||
|
MGPipeRenderStateChunkAt(GlobalPipelineChunk(0)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(1)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalPipelineChunk(2)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(3)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalPipelineChunk(4)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(5)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalPipelineChunk(6)),
|
||||||
|
};
|
||||||
|
static_assert(sizeof(kMGPipePipelineChunks) / sizeof(kMGPipePipelineChunks[0]) == kMGPipePipelineChunkCount,
|
||||||
|
"kMGPipePipelineChunks lost an entry");
|
||||||
|
|
||||||
|
const MGPStateChunk kMGPipeDynamicChunks[kMGPipeDynamicChunkCount] = {
|
||||||
|
MGPipeRenderStateChunkAt(GlobalDynamicChunk(0)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(1)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalDynamicChunk(2)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(3)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalDynamicChunk(4)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(5)),
|
||||||
|
MGPipeRenderStateChunkAt(GlobalDynamicChunk(6)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(7)),
|
||||||
|
};
|
||||||
|
static_assert(sizeof(kMGPipeDynamicChunks) / sizeof(kMGPipeDynamicChunks[0]) == kMGPipeDynamicChunkCount,
|
||||||
|
"kMGPipeDynamicChunks lost an entry");
|
||||||
|
|
||||||
|
void MGPipeGatherPipelineBytes(const RenderStateParameters& params, void* dst) {
|
||||||
|
Gather(params, AllChunks(kMGPipePipelineChunkCount), dst, kMGPipePipelineChunkCount,
|
||||||
|
GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeScatterPipelineBytes(const void* src, RenderStateParameters& dst) {
|
||||||
|
Scatter(src, AllChunks(kMGPipePipelineChunkCount), dst, kMGPipePipelineChunkCount,
|
||||||
|
GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT MGPipePipelineChunkBlobBytes(Uint32 chunkMask) {
|
||||||
|
return BlobBytes(chunkMask, kMGPipePipelineChunkCount, GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeGatherPipelineChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst) {
|
||||||
|
Gather(params, chunkMask, dst, kMGPipePipelineChunkCount, GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeScatterPipelineChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst) {
|
||||||
|
Scatter(src, chunkMask, dst, kMGPipePipelineChunkCount, GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT MGPipeDynamicChunkBlobBytes(Uint32 chunkMask) {
|
||||||
|
return BlobBytes(chunkMask, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeGatherDynamicChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst) {
|
||||||
|
Gather(params, chunkMask, dst, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeScatterDynamicChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst) {
|
||||||
|
Scatter(src, chunkMask, dst, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipeDynamicChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b) {
|
||||||
|
return ChunksThatMoved(a, b, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 MGPipePipelineChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b) {
|
||||||
|
return ChunksThatMoved(a, b, kMGPipePipelineChunkCount, GlobalPipelineChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 MGPipeHashPipelineBytes(const void* bytes) {
|
||||||
|
return static_cast<Uint64>(
|
||||||
|
XXH64(bytes, kMGPipePipelineChunkBytes, kMGPipeRenderStateChunkTableSeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters& params) {
|
||||||
|
// 396 bytes on the stack. A streaming XXH64_state_t would allocate; gathering first
|
||||||
|
// is also what CsoCache wants, because the same bytes are what a hash hit memcmps
|
||||||
|
// against before the handle is reused.
|
||||||
|
Uint8 gathered[kMGPipePipelineChunkBytes];
|
||||||
|
MGPipeGatherPipelineBytes(params, gathered);
|
||||||
|
return MGPipeHashPipelineBytes(gathered);
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Pipe/MGPipeRenderStateSpans.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include "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.
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
|
||||||
|
namespace MGPipeRenderStateChunkDetail {
|
||||||
|
using RSP = RenderStateParameters;
|
||||||
|
using SFS = StencilFaceState;
|
||||||
|
|
||||||
|
inline constexpr SizeT kStencilFace0 = offsetof(RSP, StencilStates);
|
||||||
|
inline constexpr SizeT kStencilFace1 = kStencilFace0 + sizeof(SFS);
|
||||||
|
// The pipeline half of one stencil face is [Func, Ref) + [FailOp, end); the dynamic
|
||||||
|
// half is [Ref, FailOp) - Ref and ValueMask are VK_DYNAMIC_STATE_STENCIL_REFERENCE /
|
||||||
|
// _COMPARE_MASK and WriteMask is _WRITE_MASK, which is why glStencilFunc changing only
|
||||||
|
// the reference must not evict a cached pipeline (RenderState.cpp SetStencilFunc).
|
||||||
|
inline constexpr SizeT kFaceDynamicBegin = offsetof(SFS, Ref);
|
||||||
|
inline constexpr SizeT kFaceDynamicEnd = offsetof(SFS, FailOp);
|
||||||
|
} // namespace MGPipeRenderStateChunkDetail
|
||||||
|
|
||||||
|
// 15 chunks, 16 boundaries, strictly ascending, [0, sizeof(RenderStateParameters)).
|
||||||
|
inline constexpr SizeT kMGPipeRenderStateChunkCount = 15;
|
||||||
|
|
||||||
|
inline constexpr Array<SizeT, kMGPipeRenderStateChunkCount + 1> kMGPipeRenderStateChunkBoundaries = {
|
||||||
|
// D0 dynamic: Viewports[16], LineWidth, PointSize
|
||||||
|
SizeT{0},
|
||||||
|
// P0 pipeline: PatchVertices, PatchDefaultOuterLevel, PatchDefaultInnerLevel
|
||||||
|
offsetof(RenderStateParameters, PatchVertices),
|
||||||
|
// D1 dynamic: PolygonOffsetFactor/Units/Clamp, ClipOrigin, ClipDepthMode
|
||||||
|
offsetof(RenderStateParameters, PolygonOffsetFactor),
|
||||||
|
// P1 pipeline: BlendStates[8], LogicOp, DepthTestEnabled, DepthFunc, DepthMask,
|
||||||
|
// ColorMasks[8], FramebufferSrgbEnabled, DepthClampEnabled,
|
||||||
|
// TextureCubeMapSeamlessEnabled
|
||||||
|
offsetof(RenderStateParameters, BlendStates),
|
||||||
|
// D2 dynamic: ClearColor, ClearDepth, ClearStencil, BlendColor, DepthRanges[16]
|
||||||
|
offsetof(RenderStateParameters, ClearColor),
|
||||||
|
// P2 pipeline: SampleCoverageValue, SampleCoverageInvert, SampleMaskValue,
|
||||||
|
// MinSampleShadingValue, StencilStates[0].Func
|
||||||
|
offsetof(RenderStateParameters, SampleCoverageValue),
|
||||||
|
// D3 dynamic: StencilStates[0].{Ref, ValueMask, WriteMask}
|
||||||
|
MGPipeRenderStateChunkDetail::kStencilFace0 + MGPipeRenderStateChunkDetail::kFaceDynamicBegin,
|
||||||
|
// P3 pipeline: StencilStates[0].{FailOp, PassDepthFailOp, PassDepthPassOp},
|
||||||
|
// StencilStates[1].Func
|
||||||
|
MGPipeRenderStateChunkDetail::kStencilFace0 + MGPipeRenderStateChunkDetail::kFaceDynamicEnd,
|
||||||
|
// D4 dynamic: StencilStates[1].{Ref, ValueMask, WriteMask}
|
||||||
|
MGPipeRenderStateChunkDetail::kStencilFace1 + MGPipeRenderStateChunkDetail::kFaceDynamicBegin,
|
||||||
|
// P4 pipeline: StencilStates[1].{FailOp, PassDepthFailOp, PassDepthPassOp},
|
||||||
|
// CullFaceEnabled, CullFaceModeSetting, FrontFaceModeSetting,
|
||||||
|
// ProvokingVertexModeSetting
|
||||||
|
MGPipeRenderStateChunkDetail::kStencilFace1 + MGPipeRenderStateChunkDetail::kFaceDynamicEnd,
|
||||||
|
// D5 dynamic: the four hints, PointFadeThresholdSize, PointSpriteCoordOrigin,
|
||||||
|
// ClampReadColor
|
||||||
|
offsetof(RenderStateParameters, LineSmoothHint),
|
||||||
|
// P5 pipeline: PolygonModeFront, PolygonModeBack
|
||||||
|
offsetof(RenderStateParameters, PolygonModeFront),
|
||||||
|
// D6 dynamic: PrimitiveRestartIndex
|
||||||
|
offsetof(RenderStateParameters, PrimitiveRestartIndex),
|
||||||
|
// P6 pipeline: the 20 capability bools ColorLogicOpEnabled..ProgramPointSizeEnabled,
|
||||||
|
// ScissorTestEnabledMask
|
||||||
|
offsetof(RenderStateParameters, ColorLogicOpEnabled),
|
||||||
|
// D7 dynamic: ScissorBoxes[16], ScissorBoxWrittenMask, ClipDistanceEnabledMask
|
||||||
|
offsetof(RenderStateParameters, ScissorBoxes),
|
||||||
|
sizeof(RenderStateParameters),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chunk 0 is dynamic and they alternate, which is not a coincidence: every boundary above
|
||||||
|
// is a transition between a run of BumpVersions()-written members and a run of
|
||||||
|
// ++m_version-only members, so two adjacent chunks of the same half would mean a boundary
|
||||||
|
// that separates nothing.
|
||||||
|
constexpr Bool MGPipeRenderStateChunkIsPipeline(SizeT index) { return (index % 2) == 1; }
|
||||||
|
|
||||||
|
constexpr MGPStateChunk MGPipeRenderStateChunkAt(SizeT index) {
|
||||||
|
return MGPStateChunk{static_cast<Uint16>(kMGPipeRenderStateChunkBoundaries[index]),
|
||||||
|
static_cast<Uint16>(kMGPipeRenderStateChunkBoundaries[index + 1] -
|
||||||
|
kMGPipeRenderStateChunkBoundaries[index])};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MGPipeRenderStateChunkDetail {
|
||||||
|
constexpr SizeT CountHalf(Bool pipeline) {
|
||||||
|
SizeT count = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
if (MGPipeRenderStateChunkIsPipeline(i) == pipeline) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
constexpr SizeT BytesOfHalf(Bool pipeline) {
|
||||||
|
SizeT bytes = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
if (MGPipeRenderStateChunkIsPipeline(i) == pipeline) {
|
||||||
|
bytes += MGPipeRenderStateChunkAt(i).Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
} // namespace MGPipeRenderStateChunkDetail
|
||||||
|
|
||||||
|
inline constexpr SizeT kMGPipePipelineChunkCount = MGPipeRenderStateChunkDetail::CountHalf(true);
|
||||||
|
inline constexpr SizeT kMGPipeDynamicChunkCount = MGPipeRenderStateChunkDetail::CountHalf(false);
|
||||||
|
// The CSO's content-addressed identity is exactly this many bytes; CsoCache stores them
|
||||||
|
// per entry and memcmps them on a hash hit.
|
||||||
|
inline constexpr SizeT kMGPipePipelineChunkBytes = MGPipeRenderStateChunkDetail::BytesOfHalf(true);
|
||||||
|
inline constexpr SizeT kMGPipeDynamicChunkBytes = MGPipeRenderStateChunkDetail::BytesOfHalf(false);
|
||||||
|
|
||||||
|
// Bumped by hand when something about the table changes that its BYTES do not show -
|
||||||
|
// the halves' membership, the meaning of a chunk, the gather order.
|
||||||
|
inline constexpr Uint64 kMGPipeRenderStateChunkTableVersion = 1;
|
||||||
|
|
||||||
|
// What actually seeds MGPipeComputePipelineSubsetHash. The version above is a promise a
|
||||||
|
// reader has to keep; this is the part that keeps itself. Folding the boundary table into
|
||||||
|
// the seed means a moved boundary invalidates every persisted key whether or not anyone
|
||||||
|
// remembered to bump the version - and it does so WITHOUT a static_assert on the
|
||||||
|
// boundaries, which would turn G7's negative control (which moves a boundary on purpose
|
||||||
|
// and must still compile) into a build break.
|
||||||
|
namespace MGPipeRenderStateChunkDetail {
|
||||||
|
constexpr Uint64 BoundaryChecksum() {
|
||||||
|
Uint64 hash = 0xcbf29ce484222325ull; // FNV-1a, 64-bit
|
||||||
|
for (SizeT i = 0; i <= kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
hash = (hash ^ static_cast<Uint64>(kMGPipeRenderStateChunkBoundaries[i])) * 0x100000001b3ull;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
} // namespace MGPipeRenderStateChunkDetail
|
||||||
|
inline constexpr Uint64 kMGPipeRenderStateChunkTableSeed =
|
||||||
|
kMGPipeRenderStateChunkTableVersion ^ MGPipeRenderStateChunkDetail::BoundaryChecksum();
|
||||||
|
|
||||||
|
// ---- the trip wires. A mistake in the table is a build break, here. ----
|
||||||
|
static_assert(kMGPipeRenderStateChunkBoundaries[0] == 0,
|
||||||
|
"the chunk table must start at byte 0 of RenderStateParameters");
|
||||||
|
static_assert(kMGPipeRenderStateChunkBoundaries[kMGPipeRenderStateChunkCount] ==
|
||||||
|
sizeof(RenderStateParameters),
|
||||||
|
"the chunk table must cover RenderStateParameters to its last byte");
|
||||||
|
static_assert(kMGPipePipelineChunkCount == 7);
|
||||||
|
static_assert(kMGPipeDynamicChunkCount == 8);
|
||||||
|
static_assert(kMGPipePipelineChunkCount + kMGPipeDynamicChunkCount == kMGPipeRenderStateChunkCount);
|
||||||
|
static_assert(kMGPipePipelineChunkBytes + kMGPipeDynamicChunkBytes == sizeof(RenderStateParameters),
|
||||||
|
"the two halves must partition the block exactly - no gap, no overlap");
|
||||||
|
static_assert(kMGPipeRenderStateChunkCount <= 32,
|
||||||
|
"a chunk index has to fit the Uint32 ChunkMask of MGPRenderStateDesc/MGPDynamicState");
|
||||||
|
|
||||||
|
// Sorted, non-overlapping and complete: because every chunk is [b[i], b[i+1]) the only
|
||||||
|
// way to violate that is a non-ascending boundary, so this is the whole check.
|
||||||
|
constexpr Bool MGPipeRenderStateChunkBoundariesAscend() {
|
||||||
|
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
if (!(kMGPipeRenderStateChunkBoundaries[i] < kMGPipeRenderStateChunkBoundaries[i + 1])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (kMGPipeRenderStateChunkBoundaries[i + 1] > 0xffffu) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(MGPipeRenderStateChunkBoundariesAscend(),
|
||||||
|
"the chunk boundaries must strictly ascend and fit MGPStateChunk's Uint16 fields");
|
||||||
|
|
||||||
|
// The measured sizes. They are DERIVED above; these two assertions only pin what the P2
|
||||||
|
// brief and MEASUREMENTS.md quote, so a table change that moves them is loud.
|
||||||
|
static_assert(kMGPipePipelineChunkBytes == 396, "the pipeline subset is 396 bytes");
|
||||||
|
static_assert(kMGPipeDynamicChunkBytes == 772, "the dynamic subset is 772 bytes");
|
||||||
|
|
||||||
|
// ---- global chunk bits, so nothing downstream hand-maintains a second table ----
|
||||||
|
|
||||||
|
// The GLOBAL chunk indices (bit i is chunk i of the 15) whose byte range overlaps
|
||||||
|
// [offset, offset + size). It falls straight out of the boundary table, which is the
|
||||||
|
// whole point: the applier scopes its derivation by the chunks a scatter actually moved
|
||||||
|
// (D5/D8), and a hand-written member -> chunk mapping is exactly the second table that
|
||||||
|
// would go stale the first time a boundary moves.
|
||||||
|
constexpr Uint32 MGPipeRenderStateChunkBitsCovering(SizeT offset, SizeT size) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
const SizeT begin = kMGPipeRenderStateChunkBoundaries[i];
|
||||||
|
const SizeT end = kMGPipeRenderStateChunkBoundaries[i + 1];
|
||||||
|
if (offset < end && begin < offset + size) bits |= Uint32{1} << i;
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wire masks are HALF-LOCAL (bit i of MGPRenderStateDesc::ChunkMask is pipeline chunk
|
||||||
|
// i); these widen them to the global indices the boundary table is written in. The
|
||||||
|
// halves alternate with chunk 0 dynamic, so the two conversions are arithmetic.
|
||||||
|
constexpr Uint32 MGPipeGlobalChunkBitsOfPipelineMask(Uint32 pipelineMask) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipePipelineChunkCount; ++i) {
|
||||||
|
if (((pipelineMask >> i) & 1u) != 0) bits |= Uint32{1} << (i * 2 + 1);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
constexpr Uint32 MGPipeGlobalChunkBitsOfDynamicMask(Uint32 dynamicMask) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeDynamicChunkCount; ++i) {
|
||||||
|
if (((dynamicMask >> i) & 1u) != 0) bits |= Uint32{1} << (i * 2);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
inline constexpr Uint32 kMGPipeAllGlobalChunks =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipeRenderStateChunkCount) - 1);
|
||||||
|
|
||||||
|
// The two conversions must agree with MGPipeRenderStateChunkIsPipeline, and together they
|
||||||
|
// must cover the table exactly - a widening that dropped or doubled a chunk would make
|
||||||
|
// the applier's scoping silently wrong rather than loud.
|
||||||
|
namespace MGPipeRenderStateChunkDetail {
|
||||||
|
inline constexpr Uint32 kAllPipelineHalfBits =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1);
|
||||||
|
inline constexpr Uint32 kAllDynamicHalfBits =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipeDynamicChunkCount) - 1);
|
||||||
|
inline constexpr Uint32 kWidenedPipeline = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineHalfBits);
|
||||||
|
inline constexpr Uint32 kWidenedDynamic = MGPipeGlobalChunkBitsOfDynamicMask(kAllDynamicHalfBits);
|
||||||
|
} // namespace MGPipeRenderStateChunkDetail
|
||||||
|
static_assert((MGPipeRenderStateChunkDetail::kWidenedPipeline &
|
||||||
|
MGPipeRenderStateChunkDetail::kWidenedDynamic) == 0,
|
||||||
|
"the two half-local -> global widenings must not overlap");
|
||||||
|
static_assert((MGPipeRenderStateChunkDetail::kWidenedPipeline |
|
||||||
|
MGPipeRenderStateChunkDetail::kWidenedDynamic) == kMGPipeAllGlobalChunks,
|
||||||
|
"the two half-local -> global widenings must cover the whole chunk table");
|
||||||
|
static_assert(MGPipeRenderStateChunkBitsCovering(0, sizeof(RenderStateParameters)) == kMGPipeAllGlobalChunks,
|
||||||
|
"every chunk must be covered by the whole block");
|
||||||
|
|
||||||
|
// ---- the operations everything else is written against ----
|
||||||
|
|
||||||
|
// The 396 pipeline bytes of `params`, in ascending chunk order, into `dst`.
|
||||||
|
void MGPipeGatherPipelineBytes(const RenderStateParameters& params, void* dst);
|
||||||
|
// The inverse: `src` is kMGPipePipelineChunkBytes bytes in the same order.
|
||||||
|
void MGPipeScatterPipelineBytes(const void* src, RenderStateParameters& dst);
|
||||||
|
// Incremental create_render_state: only the pipeline chunks named by `chunkMask` (bit i
|
||||||
|
// is pipeline chunk i, 0-based within the pipeline half), concatenated ascending.
|
||||||
|
SizeT MGPipePipelineChunkBlobBytes(Uint32 chunkMask);
|
||||||
|
void MGPipeGatherPipelineChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst);
|
||||||
|
void MGPipeScatterPipelineChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst);
|
||||||
|
|
||||||
|
// set_dynamic_state: bit i of `chunkMask` is dynamic chunk i, 0-based within the dynamic
|
||||||
|
// half; the blob is those chunks concatenated in ascending order.
|
||||||
|
SizeT MGPipeDynamicChunkBlobBytes(Uint32 chunkMask);
|
||||||
|
void MGPipeGatherDynamicChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst);
|
||||||
|
void MGPipeScatterDynamicChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst);
|
||||||
|
// Which dynamic chunks differ between two blocks - the chunk-level suppressor's answer.
|
||||||
|
Uint32 MGPipeDynamicChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b);
|
||||||
|
// Which pipeline chunks differ - the incremental-create mask against a base CSO.
|
||||||
|
Uint32 MGPipePipelineChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b);
|
||||||
|
|
||||||
|
// XXH64 over the seven pipeline chunks in ascending order, seeded with the table version.
|
||||||
|
// Runs ONLY when m_pipelineStateVersion moved, i.e. never in the steady state.
|
||||||
|
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters& params);
|
||||||
|
// The same hash over already-gathered bytes (CsoCache holds them, so it does not re-gather).
|
||||||
|
Uint64 MGPipeHashPipelineBytes(const void* bytes);
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
+635
-38
@@ -145,15 +145,152 @@ namespace MobileGL::MG_Pipe {
|
|||||||
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
||||||
"MGPCaps gained padding or a member; update the wire format");
|
"MGPCaps gained padding or a member; update the wire format");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// MGPResourceDesc's two discriminators (P4a, D-A3 / D-A4)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPResourceDesc::Target. P3a minted no enum for this list because it had exactly one
|
||||||
|
// producer and used the leading member's value (0) for it; P4a's texture family is the
|
||||||
|
// second producer, so the list is written out here, beside the field, in the order the
|
||||||
|
// field's own comment already wrote it.
|
||||||
|
//
|
||||||
|
// TexRect IS A THIRTEENTH ENUMERATOR AND THE BRIEF'S LIST HAS TWELVE. MobileGL's
|
||||||
|
// TextureTarget has TextureRectangle (MG_State/GLState/TextureState/TextureEnum.h), the
|
||||||
|
// table below may not have a `default:` arm, and folding rectangle onto Tex2D would erase
|
||||||
|
// a distinction the frontend keeps and both backends switch on (Espryt's
|
||||||
|
// MapToBackendTextureTarget lowers Tex1D the same way and Tex1D still has its own
|
||||||
|
// enumerator here). It is appended AFTER TexBuffer so every value the design document
|
||||||
|
// names keeps the number it was given.
|
||||||
|
enum class MGPipeResourceTarget : Uint8 {
|
||||||
|
Buffer = 0,
|
||||||
|
Tex1D,
|
||||||
|
Tex2D,
|
||||||
|
Tex3D,
|
||||||
|
Tex1DArray,
|
||||||
|
Tex2DArray,
|
||||||
|
TexCube,
|
||||||
|
TexCubeArray,
|
||||||
|
Tex2DMS,
|
||||||
|
Tex2DMSArray,
|
||||||
|
Renderbuffer,
|
||||||
|
TexBuffer,
|
||||||
|
TexRect,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// P3a's constant, moved here from MG_Impl/Pipe/ResourceTracker.h with the enum: the ack
|
||||||
|
// predicate at the bottom of this header now names the buffer target explicitly (D-A2) and
|
||||||
|
// may not reach into MG_Impl to do it. The static_assert is what keeps the two spellings
|
||||||
|
// from drifting; nothing may open-code either.
|
||||||
|
inline constexpr Uint8 kMGPipeResourceTargetBuffer =
|
||||||
|
static_cast<Uint8>(MGPipeResourceTarget::Buffer);
|
||||||
|
static_assert(kMGPipeResourceTargetBuffer == static_cast<Uint8>(MGPipeResourceTarget::Buffer),
|
||||||
|
"P3a's kMGPipeResourceTargetBuffer and MGPipeResourceTarget::Buffer have drifted");
|
||||||
|
|
||||||
|
// A sentinel the table below returns for a TextureTarget enumerator it does not name. It
|
||||||
|
// is NOT a legal Target value - it does not fit the field's Uint8 - so an unmapped
|
||||||
|
// enumerator is a build break at the static_assert rather than a descriptor that quietly
|
||||||
|
// describes the wrong kind of storage. Exactly kMGPipeBindUnmapped's shape.
|
||||||
|
inline constexpr Uint32 kMGPipeResourceTargetUnmapped = 0x100u;
|
||||||
|
|
||||||
|
// The one table. No `default:` arm on purpose - that is what makes the static_assert
|
||||||
|
// below able to see an unnamed enumerator, and it is the shape
|
||||||
|
// MGPipeBindMaskForBufferTarget already uses for BufferTarget.
|
||||||
|
constexpr Uint32 MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget target) {
|
||||||
|
switch (target) {
|
||||||
|
case MobileGL::TextureTarget::Texture1D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex1D);
|
||||||
|
case MobileGL::TextureTarget::Texture2D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2D);
|
||||||
|
case MobileGL::TextureTarget::Texture3D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex3D);
|
||||||
|
case MobileGL::TextureTarget::TextureCubeMap:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexCube);
|
||||||
|
// Its own enumerator rather than Tex2D: see the enum's comment.
|
||||||
|
case MobileGL::TextureTarget::TextureRectangle:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexRect);
|
||||||
|
case MobileGL::TextureTarget::Texture2DMultisample:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMS);
|
||||||
|
case MobileGL::TextureTarget::TextureBuffer:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexBuffer);
|
||||||
|
case MobileGL::TextureTarget::Texture1DArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex1DArray);
|
||||||
|
case MobileGL::TextureTarget::Texture2DArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DArray);
|
||||||
|
case MobileGL::TextureTarget::TextureCubeMapArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexCubeArray);
|
||||||
|
case MobileGL::TextureTarget::Texture2DMultisampleArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMSArray);
|
||||||
|
// NOT TEXTURE TARGETS. Listed rather than defaulted so the completeness assert still
|
||||||
|
// sees them, and mapped to the sentinel because no descriptor may carry either: the
|
||||||
|
// count is the enum's bound and Unknown is what an unresolved GL enum becomes.
|
||||||
|
case MobileGL::TextureTarget::TextureTargetCount:
|
||||||
|
case MobileGL::TextureTarget::Unknown:
|
||||||
|
return kMGPipeResourceTargetUnmapped;
|
||||||
|
}
|
||||||
|
return kMGPipeResourceTargetUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Bool MGPipeEveryTextureTargetIsMapped() {
|
||||||
|
for (SizeT i = 0; i < static_cast<SizeT>(MobileGL::TextureTarget::TextureTargetCount); ++i) {
|
||||||
|
if (MGPipeResourceTargetForTextureTarget(static_cast<MobileGL::TextureTarget>(i)) ==
|
||||||
|
kMGPipeResourceTargetUnmapped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(MGPipeEveryTextureTargetIsMapped(),
|
||||||
|
"a TextureTarget enumerator has no MGPResourceDesc::Target row: add it to "
|
||||||
|
"MGPipeResourceTargetForTextureTarget, and give it an enumerator of its own "
|
||||||
|
"rather than folding it onto a neighbour (D-A3)");
|
||||||
|
static_assert(MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget::Texture2D) !=
|
||||||
|
MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget::TextureRectangle),
|
||||||
|
"a rectangle texture is not a 2D texture on the wire; both backends switch on "
|
||||||
|
"the difference");
|
||||||
|
|
||||||
|
// MGPResourceDesc::BindMask's twelve bits, in the order the field's comment names them.
|
||||||
|
//
|
||||||
|
// THEY LIVED IN MG_Impl/Pipe/ResourceTracker.h THROUGH P3a, with that file's own note
|
||||||
|
// saying "the integrator moves them beside the field when a second producer appears
|
||||||
|
// (P4a's texture family)". P4a is that producer: a texture sets kMGPipeBindSampler,
|
||||||
|
// kMGPipeBindShaderImage, kMGPipeBindRenderTarget and kMGPipeBindDepthStencil, which are
|
||||||
|
// the four bits nothing set before. The mask is STICKY - ORed, never cleared - and is
|
||||||
|
// emitted on both resource_create and every resource_respecify.
|
||||||
|
enum MGPipeBindBit : Uint16 {
|
||||||
|
kMGPipeBindNone = 0,
|
||||||
|
kMGPipeBindVertex = 1u << 0,
|
||||||
|
kMGPipeBindIndex = 1u << 1,
|
||||||
|
kMGPipeBindConstant = 1u << 2,
|
||||||
|
kMGPipeBindShaderBuffer = 1u << 3,
|
||||||
|
kMGPipeBindIndirect = 1u << 4,
|
||||||
|
kMGPipeBindSampler = 1u << 5,
|
||||||
|
kMGPipeBindShaderImage = 1u << 6,
|
||||||
|
kMGPipeBindRenderTarget = 1u << 7,
|
||||||
|
kMGPipeBindDepthStencil = 1u << 8,
|
||||||
|
kMGPipeBindStreamOutput = 1u << 9,
|
||||||
|
kMGPipeBindAtomic = 1u << 10,
|
||||||
|
// THE D-B7 SWITCH. With kCapNeedsHostIndexBytes set the server mirrors this
|
||||||
|
// resource's bytes so it can rewrite restart indices and flatten multi-draws
|
||||||
|
// (ARCHITECTURE.md 10.3). Getting it wrong is invisible in monolith and silently
|
||||||
|
// disables both under split, which is why it is set from a table rather than from a
|
||||||
|
// special case at the emission site.
|
||||||
|
kMGPipeBindElementArray = 1u << 11,
|
||||||
|
};
|
||||||
|
|
||||||
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
|
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
|
||||||
// share one create/respecify shape (section 4.5.1).
|
// share one create/respecify shape (section 4.5.1).
|
||||||
struct MGPResourceDesc {
|
struct MGPResourceDesc {
|
||||||
MGPipeHandle Resource;
|
MGPipeHandle Resource;
|
||||||
Uint8 Target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
|
// MGPipeResourceTarget: Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer |
|
||||||
|
// TexBuffer | TexRect. Never open-coded; the texture half comes from
|
||||||
|
// MGPipeResourceTargetForTextureTarget above.
|
||||||
|
Uint8 Target;
|
||||||
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
|
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
|
||||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
// MGPipeBindBit, above: VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|
|
||||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
|
// SHADER_IMAGE|RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The
|
||||||
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
|
// ELEMENT_ARRAY bit is the D-B7 switch: with kCapNeedsHostIndexBytes set the server
|
||||||
|
// mirrors this resource.
|
||||||
Uint16 BindMask;
|
Uint16 BindMask;
|
||||||
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
||||||
Uint32 Width, Height, Depth;
|
Uint32 Width, Height, Depth;
|
||||||
@@ -161,19 +298,99 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Uint8 FixedSampleLocations, Immutable;
|
Uint8 FixedSampleLocations, Immutable;
|
||||||
Uint32 Usage; // BufferUsage
|
Uint32 Usage; // BufferUsage
|
||||||
Uint32 StorageFlags; // glBufferStorage flags
|
Uint32 StorageFlags; // glBufferStorage flags
|
||||||
Uint8 HasDefinedContent; // false after a NULL-data respecify
|
Uint8 HasDefinedContent; // false after a NULL-data respecify - STORAGE-DEFINING
|
||||||
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
|
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
|
||||||
Uint16 Pad0;
|
// 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. 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 THREE HELPERS BELOW, never by touching the fields: the
|
||||||
|
// presence byte and the pair are one value in three pieces, and an open-coded reader
|
||||||
|
// that forgets the presence byte reads level 0 of upload target 0 as a real scope.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
Uint8 HasRespecifiedLevel;
|
||||||
|
Uint8 Pad0;
|
||||||
|
// ImageBindableHint and BindMask above are the two METADATA fields the rule exists
|
||||||
|
// for: a respecify that moves only them - every storage-defining field equal to the
|
||||||
|
// stored descriptor - is a metadata update, with no reallocation ack and no
|
||||||
|
// PendingUploads clear. The rule, the full storage-defining field set and the third
|
||||||
|
// field allowed to differ (GlNameForDiag) are stated beside
|
||||||
|
// MGPipeResourceRespecifyNeedsAck below (P4a, ID-18 M4), which is where the applier
|
||||||
|
// and both twins read them from.
|
||||||
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
|
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
|
||||||
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
|
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
|
||||||
// cannot hold one.
|
// cannot hold one.
|
||||||
Uint32 GlNameForDiag;
|
Uint32 GlNameForDiag;
|
||||||
Uint32 Pad1;
|
// 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
|
||||||
|
// that type, because PipeApply.h includes THIS header and not the other way round.
|
||||||
|
Uint16 RespecifiedUploadTarget;
|
||||||
|
Uint16 RespecifiedLevel;
|
||||||
MGPipeHandle ViewOf; // storage owner for a texture view
|
MGPipeHandle ViewOf; // storage owner for a texture view
|
||||||
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
|
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
|
||||||
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
|
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPResourceDesc, 88);
|
MGP_ASSERT_POD(MGPResourceDesc, 88);
|
||||||
|
// The scope fields went into the two existing pads, so the descriptor did not grow and this
|
||||||
|
// number did not move. That was the deciding argument against giving MGPResourceDesc an
|
||||||
|
// MGPBlobRef for the OTHER uncarried companion (initialBytes): a blobref is 24 bytes, the
|
||||||
|
// pads are 6, and it would have taken the struct to 112. The scope needs 5 and fits.
|
||||||
|
static_assert(offsetof(MGPResourceDesc, RespecifiedUploadTarget) + sizeof(Uint16) ==
|
||||||
|
offsetof(MGPResourceDesc, RespecifiedLevel),
|
||||||
|
"the respecify scope pair must stay adjacent and in MGPRespecifiedLevel's order");
|
||||||
|
|
||||||
|
// ---- the only supported reads of the respecify scope --------------------------------
|
||||||
|
//
|
||||||
|
// Three fields, one value. Open-coding them is how a reader that forgets the presence byte
|
||||||
|
// turns "whole resource" into "upload target 0, level 0" - a real scope, and the wrong one.
|
||||||
|
|
||||||
|
// True when this respecify replaces the whole resource, which is every glBufferData, every
|
||||||
|
// glBufferStorage, every glTexStorage* and every texture view. The applier drops every
|
||||||
|
// pending upload for it.
|
||||||
|
inline constexpr Bool MGPipeRespecifyIsWholeResource(const MGPResourceDesc& desc) {
|
||||||
|
return desc.HasRespecifiedLevel == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The single (uploadTarget, level) a per-level respecify replaces. Reading either half of a
|
||||||
|
// whole-resource descriptor is a caller error; both answer 0 so that a misuse is at least
|
||||||
|
// deterministic rather than whatever the pad happened to hold.
|
||||||
|
inline constexpr Uint16 MGPipeRespecifiedUploadTargetOf(const MGPResourceDesc& desc) {
|
||||||
|
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedUploadTarget;
|
||||||
|
}
|
||||||
|
inline constexpr Uint16 MGPipeRespecifiedLevelOf(const MGPResourceDesc& desc) {
|
||||||
|
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two writers. A producer sets the scope with one call so the presence byte cannot be
|
||||||
|
// left behind, and clears it with the other; a descriptor built by value-initialization is
|
||||||
|
// already whole-resource, which is the safe default and the only one P5 produces.
|
||||||
|
inline constexpr void MGPipeSetRespecifiedLevel(MGPResourceDesc& desc, Uint16 uploadTarget,
|
||||||
|
Uint16 level) {
|
||||||
|
desc.HasRespecifiedLevel = 1;
|
||||||
|
desc.RespecifiedUploadTarget = uploadTarget;
|
||||||
|
desc.RespecifiedLevel = level;
|
||||||
|
}
|
||||||
|
inline constexpr void MGPipeClearRespecifiedLevel(MGPResourceDesc& desc) {
|
||||||
|
desc.HasRespecifiedLevel = 0;
|
||||||
|
desc.RespecifiedUploadTarget = 0;
|
||||||
|
desc.RespecifiedLevel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
|
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
|
||||||
|
|
||||||
struct MGPFenceWait {
|
struct MGPFenceWait {
|
||||||
@@ -231,8 +448,13 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
|
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
|
||||||
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
|
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
|
||||||
// values, sample coverage, hints and the point-size family. This is what keeps
|
// values, hints, the point-size family and the primitive-restart index. This is what
|
||||||
// glViewport from evicting Magma's pipeline memo (D-B1).
|
// keeps glViewport from evicting Magma's pipeline memo (D-B1).
|
||||||
|
//
|
||||||
|
// SAMPLE COVERAGE IS NOT IN IT, and this comment used to say it was. P2's rule is that
|
||||||
|
// a byte is pipeline state if and only if a public RenderState setter that calls
|
||||||
|
// BumpVersions() writes it, and SetSampleCoverage does - so SampleCoverageValue and
|
||||||
|
// SampleCoverageInvert are in pipeline chunk P2 (MGPipeRenderStateSpans.h).
|
||||||
struct MGPDynamicState {
|
struct MGPDynamicState {
|
||||||
Uint32 ChunkMask;
|
Uint32 ChunkMask;
|
||||||
Uint16 Version;
|
Uint16 Version;
|
||||||
@@ -245,11 +467,29 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means
|
// 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 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.
|
// 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.
|
||||||
struct MGPVertexElements {
|
struct MGPVertexElements {
|
||||||
MGPipeHandle Cso;
|
MGPipeHandle Cso;
|
||||||
Uint32 AttributeCount;
|
Uint32 AttributeCount;
|
||||||
Uint32 BindingPointCount;
|
Uint32 BindingPointCount;
|
||||||
MGPBlobRef Blob; // VertexAttribute[] followed by VertexBufferBindingPoint[]
|
// MGPVertexAttribWire[AttributeCount] followed by
|
||||||
|
// MGPVertexBindingPointWire[BindingPointCount] - the WIRE views of MGPipeValueTypes.h,
|
||||||
|
// not VertexAttribute / VertexBufferBindingPoint, which cannot travel at all (each
|
||||||
|
// holds a SharedPtr<BufferObject>). The layout is stated once at
|
||||||
|
// MGPipeValueTypes.h's P3a block and unpacked once in PipeApply.cpp.
|
||||||
|
MGPBlobRef Blob;
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPVertexElements, 40);
|
MGP_ASSERT_POD(MGPVertexElements, 40);
|
||||||
|
|
||||||
@@ -281,18 +521,49 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGP_ASSERT_POD(MGPSamplerView, 36);
|
MGP_ASSERT_POD(MGPSamplerView, 36);
|
||||||
|
|
||||||
// Per texture OBJECT, independent of any view.
|
// Per texture OBJECT, independent of any view.
|
||||||
|
//
|
||||||
|
// P4a, D-E1: 32 -> 40 bytes. BuiltinSampler is the SamplerCso carrying the
|
||||||
|
// SamplerParameters of the SamplerObject every ITextureObject owns
|
||||||
|
// (TextureState/TextureObject.h's m_sampler, constructed by TextureObjectBase's
|
||||||
|
// constructor). GL 4.6 core table 23.18 makes filter/wrap/compare/border SAMPLER state,
|
||||||
|
// and Espryt pushes it with glTexParameter* onto the TEXTURE rather than with
|
||||||
|
// glBindSampler onto the unit - behaviour P4a preserves exactly. Naming the CSO rather
|
||||||
|
// than widening this payload with a filter/wrap/border block is what keeps ONE authority
|
||||||
|
// for one value: SyncTextureParamsToBackend reads this record, SyncBuiltinSamplerToBackend
|
||||||
|
// reads that CSO's SamplerParameters, and the two pushes stay two pushes.
|
||||||
struct MGPTextureParams {
|
struct MGPTextureParams {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res; // 0
|
||||||
Uint16 BaseLevel, MaxLevel;
|
// Kind SamplerCso. kMGPipeNullHandle is ILLEGAL - every texture object owns a sampler
|
||||||
Uint8 Swizzle[4];
|
// object, so a null here is Fatal{ProtocolCorruption} rather than "no sampler".
|
||||||
Uint8 DepthStencilMode;
|
MGPipeHandle BuiltinSampler; // 8
|
||||||
|
Uint16 BaseLevel, MaxLevel; // 16
|
||||||
|
Uint8 Swizzle[4]; // 20
|
||||||
|
Uint8 DepthStencilMode; // 24
|
||||||
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
||||||
// override that the frontend params version does not move for.
|
// override that the frontend params version does not move for.
|
||||||
Uint8 ForceResync;
|
Uint8 ForceResync; // 25
|
||||||
Uint8 Pad0[2];
|
// Mirrors m_forceSamplerResync, which had no wire spelling at all before P4a. What it
|
||||||
Float MinLod, MaxLod, LodBias;
|
// guards is not mis-filtering but an INCOMPLETE texture sampling (0,0,0,1) after a
|
||||||
|
// driver re-mint, which is why it is a second bit and not folded into ForceResync.
|
||||||
|
Uint8 SamplerResync; // 26
|
||||||
|
Uint8 Pad0; // 27
|
||||||
|
Float MinLod, MaxLod, LodBias; // 28
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPTextureParams, 32);
|
MGP_ASSERT_POD(MGPTextureParams, 40);
|
||||||
|
|
||||||
|
// MGPTextureParams::DepthStencilMode's two legal values, and the ONLY spelling of them
|
||||||
|
// (P4a, ID-12 / esprytobj DV-2). The frontend keeps a GLenum - GL_DEPTH_COMPONENT 0x1902,
|
||||||
|
// GL_STENCIL_INDEX 0x1901 - and a Uint8 cannot hold one, so the aspect is NUMBERED here
|
||||||
|
// rather than truncated there. The GLenum -> byte helper belongs to the client emitter;
|
||||||
|
// this header owns the two numbers, so the emitter and both backends cannot disagree.
|
||||||
|
//
|
||||||
|
// 0 IS DEPTH, AND THAT IS THE WHOLE REASON FOR THIS ORDER RATHER THAN THE ENUM'S LOW BYTE.
|
||||||
|
// GL_DEPTH_COMPONENT is the GL initial value of GL_DEPTH_STENCIL_TEXTURE_MODE and a
|
||||||
|
// texture that never asks for the stencil aspect never emits the call at all, so A ZEROED
|
||||||
|
// RECORD MUST DECODE TO EXACTLY WHAT AN UNTOUCHED TEXTURE ALREADY HAS. Numbering by the
|
||||||
|
// low byte would have made depth 0x02 and stencil 0x01 and left zero meaning nothing.
|
||||||
|
inline constexpr Uint8 kMGPipeDepthStencilModeDepth = 0; // GL_DEPTH_COMPONENT
|
||||||
|
inline constexpr Uint8 kMGPipeDepthStencilModeStencil = 1; // GL_STENCIL_INDEX
|
||||||
|
|
||||||
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
|
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
|
||||||
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
|
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
|
||||||
@@ -315,39 +586,208 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// set_*
|
// set_*
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPSurface::Kind's three values (P4a, ID-12 / esprytobj DV-4). MGPipeKind is REUSED
|
||||||
|
// rather than a second three-value enum minted beside it: it already spells Texture and
|
||||||
|
// Renderbuffer, its None is 0, and a zero-initialised MGPSurface is therefore ALREADY the
|
||||||
|
// empty attachment point this record describes - {Res = kMGPipeNullHandle, Kind = None}
|
||||||
|
// and every other field zero. The static_assert is what keeps that true if MGPipeKind is
|
||||||
|
// ever reordered.
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindNone = static_cast<Uint8>(MGPipeKind::None);
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindTexture = static_cast<Uint8>(MGPipeKind::Texture);
|
||||||
|
inline constexpr Uint8 kMGPipeSurfaceKindRenderbuffer =
|
||||||
|
static_cast<Uint8>(MGPipeKind::Renderbuffer);
|
||||||
|
static_assert(kMGPipeSurfaceKindNone == 0,
|
||||||
|
"a zero-initialised MGPSurface must already be the empty attachment point");
|
||||||
|
|
||||||
|
// MGPSurface::TextureTarget for a point that names no texture: the renderbuffer point and
|
||||||
|
// the empty point both carry it. It is MobileGL::TextureTarget::Unknown, which is -1 and
|
||||||
|
// therefore 0xFFFF in the field's Uint16 - a value no real target has, so a reader that
|
||||||
|
// forgets to gate on Kind gets a nonsense target rather than a plausible wrong one.
|
||||||
|
inline constexpr Uint16 kMGPipeSurfaceNoTextureTarget = 0xFFFF;
|
||||||
|
static_assert(kMGPipeSurfaceNoTextureTarget ==
|
||||||
|
static_cast<Uint16>(MobileGL::TextureTarget::Unknown),
|
||||||
|
"kMGPipeSurfaceNoTextureTarget is TextureTarget::Unknown widened to the "
|
||||||
|
"field, and MG_State moved Unknown off -1");
|
||||||
|
|
||||||
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
|
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
|
||||||
// push time with no lookup (section 4.5.6).
|
// push time with no lookup (section 4.5.6).
|
||||||
struct MGPSurface {
|
struct MGPSurface {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res;
|
||||||
Uint32 InternalFormat;
|
Uint32 InternalFormat;
|
||||||
Uint8 Kind; // Texture | Renderbuffer | None
|
Uint8 Kind; // kMGPipeSurfaceKind{None,Texture,Renderbuffer}, above
|
||||||
Uint8 Layered;
|
Uint8 Layered;
|
||||||
Uint16 Level;
|
Uint16 Level;
|
||||||
Uint32 Layer;
|
Uint32 Layer;
|
||||||
Uint16 UploadTarget;
|
Uint16 UploadTarget; // static_cast<Uint16>(MobileGL::TextureUploadTarget)
|
||||||
Uint16 Pad0;
|
// 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.
|
||||||
|
Uint16 TextureTarget;
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPSurface, 24);
|
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.
|
||||||
|
enum class MGPipeFramebufferTarget : Uint8 {
|
||||||
|
Draw = 0,
|
||||||
|
Read = 1,
|
||||||
|
Both = 2,
|
||||||
|
// Describes the framebuffer named by Fbo and changes no binding (ID-19). Emitted
|
||||||
|
// ahead of every DSA entry point that hands that framebuffer over by name.
|
||||||
|
Named = 3,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MGPFramebufferState::Color[] and DrawBuffers[] are ONE array width, and it is the wire's
|
||||||
|
// bound rather than the driver's: GetDynamicParameters().MaxColorAttachments is the raw ES
|
||||||
|
// cap and is not clamped to 8 on the GLES path, so a driver reporting more would silently
|
||||||
|
// truncate this record. The framebuffer subsystem bit is REFUSED at its first lookup in
|
||||||
|
// that case, with one ERROR naming the cap, and the legacy arm runs - the same shape the
|
||||||
|
// backend's existing bit-8-requires-bit-7 refusal already ships
|
||||||
|
// (ResolveFramebufferSubsystemArm, beside ResolveResourceSubsystemArm). Widening the
|
||||||
|
// payload is a wire change nobody has evidence for, and truncating silently is the bug
|
||||||
|
// class this phase is closing.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxColorAttachments = 8;
|
||||||
|
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.
|
||||||
struct MGPFramebufferState {
|
struct MGPFramebufferState {
|
||||||
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
||||||
MGPSurface Color[8];
|
MGPSurface Color[8];
|
||||||
MGPSurface Depth, Stencil;
|
MGPSurface Depth, Stencil;
|
||||||
// The RESOLVED read surface, not an index. This is what structurally closes the
|
// The RESOLVED read surface, not an index, and it is THIS framebuffer's own read
|
||||||
|
// buffer under every Target - Named included. This is what structurally closes the
|
||||||
// read-buffer-shared-FBO defect class.
|
// read-buffer-shared-FBO defect class.
|
||||||
MGPSurface ReadSurface;
|
MGPSurface ReadSurface;
|
||||||
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
|
// attachment index, -1 = NONE. The named object's array; applied to the bound draw
|
||||||
|
// framebuffer only when Target is Draw or Both.
|
||||||
|
Int8 DrawBuffers[8];
|
||||||
Uint16 Width, Height, Layers, Samples;
|
Uint16 Width, Height, Layers, Samples;
|
||||||
Uint8 FixedSampleLocations, IsDefault, Complete, Pad0;
|
// Complete is FramebufferObject::CheckCompleteness(), the frontend-only answer - NOT
|
||||||
|
// glCheckFramebufferStatus's. CheckFramebufferStatus_State additionally consults
|
||||||
|
// ActiveBackendRejectsDistinctDepthStencil() and HasNonRenderableColorAttachment,
|
||||||
|
// which read the backend's probed format-capability cache; a client emitting that
|
||||||
|
// answer would be reading the backend from the client side, which is the exact
|
||||||
|
// coupling this boundary exists to remove. A later phase must not assume the stronger
|
||||||
|
// answer, and glCheckFramebufferStatus keeps answering from the frontend as it does
|
||||||
|
// today.
|
||||||
|
Uint8 FixedSampleLocations, IsDefault, Complete;
|
||||||
|
// MGPipeFramebufferTarget, above (P4a, D-C2; was Pad0). Draw/Read/Both also set the
|
||||||
|
// named binding(s); Named sets none (ID-19). The ONLY binding-specific field.
|
||||||
|
Uint8 Target;
|
||||||
Uint32 Pad1;
|
Uint32 Pad1;
|
||||||
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
|
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
|
||||||
// emission suppressor - an unchanged hash means this record is not sent at all.
|
// emission suppressor - an unchanged hash means this record is not sent at all.
|
||||||
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
|
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
|
||||||
// redundant glBindSampler traffic reappears as a variable-length record per batch.
|
// redundant glBindSampler traffic reappears as a variable-length record per batch.
|
||||||
|
// Target is one of its inputs, and the suppressor is keyed per framebuffer.
|
||||||
Uint64 ContentHash;
|
Uint64 ContentHash;
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPFramebufferState, 304);
|
MGP_ASSERT_POD(MGPFramebufferState, 304);
|
||||||
|
|
||||||
|
// GL_MAX_VERTEX_ATTRIBS as MobileGL advertises it, on the MG_Pipe side of the boundary.
|
||||||
|
// It bounds the two declared counts of MGPVertexElements, the applier's two per-CSO
|
||||||
|
// arrays and the vertex-buffer set. It MUST equal VertexArrayObject::MAX_VERTEX_ATTRIBS;
|
||||||
|
// MG_Impl/Pipe/PipeFill.cpp is the one translation unit that sees both and carries the
|
||||||
|
// static_assert, because this header may not include a frontend one.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxVertexAttribs = 32;
|
||||||
|
|
||||||
|
// P4a, D-G2. MobileGL's texture-unit space is ONE MERGED array of
|
||||||
|
// TextureState::MAX_TEXTURE_IMAGE_UNITS = 192 - there is no stage dimension on
|
||||||
|
// set_sampler_views / bind_sampler_states / set_shader_images, because the same unit may
|
||||||
|
// be sampled from two stages and per-stage 32 is an advertised number rather than a
|
||||||
|
// storage shape. These two bound the three var-tail sets' Start + Count, and a record
|
||||||
|
// that names a window outside them is Fatal{ProtocolCorruption} - the var-tail window IS
|
||||||
|
// the bound and entries outside it are not cleared.
|
||||||
|
//
|
||||||
|
// Pinned against the frontend constant in MG_Impl/Pipe/PipeFill.cpp, the one translation
|
||||||
|
// unit that sees both, exactly as kMGPipeMaxVertexAttribs is: this header may not include
|
||||||
|
// a frontend one.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxTextureUnits = 192;
|
||||||
|
inline constexpr Uint32 kMGPipeMaxImageUnits = 192;
|
||||||
|
|
||||||
struct MGPVertexBuffer {
|
struct MGPVertexBuffer {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res;
|
||||||
Uint64 Offset;
|
Uint64 Offset;
|
||||||
@@ -361,9 +801,25 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// Var-tail header: MGPVertexBuffer[Count] follows.
|
// Var-tail header: MGPVertexBuffer[Count] follows.
|
||||||
struct MGPVertexBuffers {
|
struct MGPVertexBuffers {
|
||||||
Uint32 Start, Count;
|
Uint32 Start, Count;
|
||||||
|
// The vertex-FETCH base instance these offsets are valid for (P3a, D-H1). It is DRAW
|
||||||
|
// state, not VAO state, and it is NOT the same thing as MGPDrawInfo::StartInstance:
|
||||||
|
// that one is the GL draw's baseInstance and feeds gl_BaseInstance, this one is the
|
||||||
|
// shift the fetch address of an instanced array needs when the device has no native
|
||||||
|
// base-instance support. The server decides whether to emulate it or let
|
||||||
|
// GL_EXT_base_instance do the work - emulation is server-owned - so the client sends
|
||||||
|
// the draw's raw value and never a pre-shifted offset.
|
||||||
|
//
|
||||||
|
// IT IS A ContentHash INPUT, and that is a requirement rather than a nicety:
|
||||||
|
// set_vertex_buffers is suppressed on an unchanged hash (MG_Impl/Pipe/
|
||||||
|
// SetHashSuppressor.h's SetVertexBuffers slot), so a baseInstance that moved while
|
||||||
|
// the buffer set did not would be suppressed and the server would keep the previous
|
||||||
|
// shift. It rides ONE PER EMITTED SET rather than per entry: per entry the shift is
|
||||||
|
// redundant and lets a malformed record disagree with itself.
|
||||||
|
Uint32 BaseInstance;
|
||||||
|
Uint32 Pad0;
|
||||||
Uint64 ContentHash;
|
Uint64 ContentHash;
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPVertexBuffers, 16);
|
MGP_ASSERT_POD(MGPVertexBuffers, 24);
|
||||||
|
|
||||||
// An independent call, NOT a subset of the VAO configuration version (D5).
|
// An independent call, NOT a subset of the VAO configuration version (D5).
|
||||||
struct MGPIndexBuffer {
|
struct MGPIndexBuffer {
|
||||||
@@ -514,14 +970,17 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// to it because both sides are the same translation unit. G3 emits the offsetof
|
// to it because both sides are the same translation unit. G3 emits the offsetof
|
||||||
// assertions; under split the block is serialized field-wise rather than memcpy'd.
|
// assertions; under split the block is serialized field-wise rather than memcpy'd.
|
||||||
struct ResidualValueBlock {
|
struct ResidualValueBlock {
|
||||||
RenderStateParameters RenderState; // until create/bind_render_state + set_dynamic_state land
|
// The 35 CapabilityInput bits, packed in enum order. P2 retired everything else:
|
||||||
PixelStoreParameters Pack; // until set_pixel_pack_state lands
|
// RenderStateParameters to create/bind_render_state + set_dynamic_state, Pack to
|
||||||
|
// set_pixel_pack_state, and the patch quintet to set_patch_state.
|
||||||
|
//
|
||||||
|
// What is left is deliberately REDUNDANT. Every one of the 35 capabilities is
|
||||||
|
// answerable from the assembled working block now that P2 gave FramebufferSrgb,
|
||||||
|
// DepthClamp and TextureCubeMapSeamless real storage - which is the point: the
|
||||||
|
// applier compares the two answers bit by bit, so the day a later call takes a
|
||||||
|
// capability over and forgets to carry it, the block says so on the next draw
|
||||||
|
// (Fatal{PipeResidualDiverged, "<Cap>"}, MG_Pipe/PipeApply.cpp).
|
||||||
Uint64 CapabilityBits;
|
Uint64 CapabilityBits;
|
||||||
Uint32 PatchVertices;
|
|
||||||
Uint32 Pad0;
|
|
||||||
Float PatchOuter[4];
|
|
||||||
Float PatchInner[2];
|
|
||||||
Uint32 Pad1[2];
|
|
||||||
};
|
};
|
||||||
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
|
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
|
||||||
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
|
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
|
||||||
@@ -530,10 +989,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
|
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
|
||||||
// break - which is the point.
|
// break - which is the point.
|
||||||
//
|
//
|
||||||
// Stable across the ABIs MobileGL ships on: every member of RenderStateParameters and
|
// Stable across the ABIs MobileGL ships on: the one member is a fixed-width scalar.
|
||||||
// PixelStoreParameters is a fixed-width scalar or an array of one, with no pointer and no
|
//
|
||||||
// SizeT.
|
// P2: 1248 -> 8. RenderStateParameters (1168) retired to create/bind_render_state and
|
||||||
#define MGL_RESIDUAL_BLOCK_SIZE 1248
|
// set_dynamic_state, PixelStoreParameters (28) to set_pixel_pack_state, and the patch
|
||||||
|
// quintet (52 with its padding) to set_patch_state.
|
||||||
|
#define MGL_RESIDUAL_BLOCK_SIZE 8
|
||||||
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
|
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
|
||||||
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
|
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
|
||||||
"retired, and do not raise it");
|
"retired, and do not raise it");
|
||||||
@@ -566,16 +1027,50 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
|
// 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.
|
// 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
|
// 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
|
// 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, and
|
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0.
|
||||||
// Blob holds exactly Size source bytes. That caps ONE record at a 2^31-1 offset and a
|
//
|
||||||
|
// 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
|
// 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
|
// 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
|
// imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are
|
||||||
// the only spelling of this convention; nothing else reads the box for a buffer.
|
// the only spelling of this convention; nothing else reads the box for a buffer.
|
||||||
struct MGPSubData {
|
struct MGPSubData {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res;
|
||||||
|
// Target is PACKED - see the block above, and read it only through
|
||||||
|
// MGPipeSubDataResourceTargetOf / MGPipeSubDataUploadTargetOf below.
|
||||||
Uint16 Target, Level;
|
Uint16 Target, Level;
|
||||||
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
|
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
|
||||||
// bytes an untransformed level shadow?
|
// bytes an untransformed level shadow?
|
||||||
@@ -588,6 +1083,38 @@ namespace MobileGL::MG_Pipe {
|
|||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPSubData, 72);
|
MGP_ASSERT_POD(MGPSubData, 72);
|
||||||
|
|
||||||
|
// The one spelling of MGPSubData::Target's encoding, stated above the struct.
|
||||||
|
//
|
||||||
|
// Uint32 ARGUMENTS RATHER THAN THE TWO ENUM TYPES, and that is deliberate. This header is
|
||||||
|
// the contract: MGPipeResourceTarget is minted in it, but the upload half is MG_State's
|
||||||
|
// TextureUploadTarget, and nobody who reads the packed field ever needs that type - the
|
||||||
|
// applier and both backends read the halves BACK, as bytes, through the two accessors.
|
||||||
|
// Naming it in a signature would pin the contract's own API to a frontend enum for no
|
||||||
|
// reader's benefit, and would stop kMGPipeResourceTargetBuffer being passed as it stands.
|
||||||
|
// Callers pass static_cast<Uint32>(MobileGL::TextureUploadTarget) for `uploadTarget` and
|
||||||
|
// static_cast<Uint32>(MGPipeResourceTarget) - or kMGPipeResourceTargetBuffer - for
|
||||||
|
// `resourceTarget`.
|
||||||
|
constexpr inline Uint16 MGPipePackSubDataTarget(Uint32 resourceTarget, Uint32 uploadTarget) {
|
||||||
|
return static_cast<Uint16>((resourceTarget & 0xFFu) | ((uploadTarget & 0xFFu) << 8));
|
||||||
|
}
|
||||||
|
// Comparable against static_cast<Uint8>(MGPipeResourceTarget) / kMGPipeResourceTargetBuffer.
|
||||||
|
constexpr inline Uint8 MGPipeSubDataResourceTargetOf(Uint16 packed) {
|
||||||
|
return static_cast<Uint8>(packed & 0xFFu);
|
||||||
|
}
|
||||||
|
// Comparable against static_cast<Uint8>(MobileGL::TextureUploadTarget).
|
||||||
|
constexpr inline Uint8 MGPipeSubDataUploadTargetOf(Uint16 packed) {
|
||||||
|
return static_cast<Uint8>((packed >> 8) & 0xFFu);
|
||||||
|
}
|
||||||
|
// THE INVARIANT P3a's records and the applier's buffer test both rest on: a buffer
|
||||||
|
// record's Target is exactly kMGPipeResourceTargetBuffer, whole field, upload byte and
|
||||||
|
// all. TextureUploadTarget::Texture1D is 0, so the buffer case is the one place where the
|
||||||
|
// packed form and a bare enumerator agree - and it has to stay that place.
|
||||||
|
static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u) ==
|
||||||
|
kMGPipeResourceTargetBuffer,
|
||||||
|
"a buffer sub-data record's Target must stay exactly "
|
||||||
|
"kMGPipeResourceTargetBuffer: the applier's SubDataNamesABuffer tests the "
|
||||||
|
"whole field == 0");
|
||||||
|
|
||||||
// Encodes a buffer byte range into the record's box. False, with the record untouched,
|
// Encodes a buffer byte range into the record's box. False, with the record untouched,
|
||||||
// when the range does not fit one record: the emitter has to split it.
|
// when the range does not fit one record: the emitter has to split it.
|
||||||
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
|
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
|
||||||
@@ -606,6 +1133,76 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
|
// 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.
|
// zero regions - that is how a pull that needs nothing is answered.
|
||||||
struct MGPSubDataComplete {
|
struct MGPSubDataComplete {
|
||||||
|
|||||||
@@ -289,6 +289,21 @@ namespace MobileGL {
|
|||||||
// Every entry is initialized to all-true in RenderState's constructor.
|
// Every entry is initialized to all-true in RenderState's constructor.
|
||||||
Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
|
Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
|
||||||
|
|
||||||
|
// GL_FRAMEBUFFER_SRGB / GL_DEPTH_CLAMP / GL_TEXTURE_CUBE_MAP_SEAMLESS. Until P2 these
|
||||||
|
// three fell to SetCapability's "not supported currently" arm - glEnable was swallowed
|
||||||
|
// and IsCapabilityEnabled answered a compile-time false, so DirectGLES' sRGB block and
|
||||||
|
// the DirectVulkan read points consumed a constant while glIsEnabled lied about it.
|
||||||
|
// Placed HERE, in the three alignment bytes between ColorMasks (32 bytes, align 1) and
|
||||||
|
// ClearColor (align 4), so sizeof(RenderStateParameters) stays 1168 and no existing
|
||||||
|
// offset moves: the Espryt span constants and the P2 chunk table both depend on that.
|
||||||
|
// All three are PIPELINE state (their setters call BumpVersions): FramebufferSrgb is
|
||||||
|
// what ARCHITECTURE.md 5.3 asks for, DepthClamp is
|
||||||
|
// VkPipelineRasterizationStateCreateInfo::depthClampEnable, and TextureCubeMapSeamless
|
||||||
|
// changes sampler interpretation.
|
||||||
|
Bool FramebufferSrgbEnabled = false;
|
||||||
|
Bool DepthClampEnabled = false;
|
||||||
|
Bool TextureCubeMapSeamlessEnabled = false;
|
||||||
|
|
||||||
// Clear State
|
// Clear State
|
||||||
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
|
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
Float ClearDepth = 1.0f;
|
Float ClearDepth = 1.0f;
|
||||||
@@ -529,6 +544,61 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
} // namespace MG_State::GLState
|
} // namespace MG_State::GLState
|
||||||
|
|
||||||
|
// ---- P3a: the WIRE forms of the two views above (ARCHITECTURE.md section on vertex
|
||||||
|
// elements; brief D-G2). Neither VertexAttribute nor VertexBufferBindingPoint can travel
|
||||||
|
// as itself: both hold a SharedPtr<BufferObject>, and a payload never contains a pointer.
|
||||||
|
// They live here rather than in MGPipeTypes.h so the structs they mirror are one screen
|
||||||
|
// away and a member added above has its wire twin in view; MGPipeTypes.h includes this
|
||||||
|
// header, so MG_Pipe sees them unqualified like every other value type.
|
||||||
|
//
|
||||||
|
// Both ride the create_vertex_elements BLOB, in ascending index order, attributes first:
|
||||||
|
// MGPVertexAttribWire[AttributeCount] then MGPVertexBindingPointWire[BindingPointCount],
|
||||||
|
// each count <= VertexArrayObject::MAX_VERTEX_ATTRIBS (32). The applier refuses a record
|
||||||
|
// whose declared counts do not match the blob's declared size.
|
||||||
|
|
||||||
|
// The resolved flat attribute view. Buffer identity does NOT travel here - it travels in
|
||||||
|
// set_vertex_buffers, which is what keeps this record stable while buffers change under
|
||||||
|
// it. Stride is the RESOLVED distance and a surviving 0 can only have come from the
|
||||||
|
// binding model (see VertexAttribute::Stride above); collapsing it back into the element
|
||||||
|
// size is what made KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the
|
||||||
|
// buffer. Divisor is deliberately ABSENT: it is resolved per binding point and travels in
|
||||||
|
// MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads it.
|
||||||
|
// LegacyStride / LegacyPointer are likewise absent - they are the glGetVertexAttrib*
|
||||||
|
// query answers and stay client-side, because nothing but the query path reads them.
|
||||||
|
struct MGPVertexAttribWire {
|
||||||
|
Uint64 Offset; // 0
|
||||||
|
Int32 Stride; // 8
|
||||||
|
Uint32 Type; // 12 DataType
|
||||||
|
Uint8 Size; // 16 1..4; GL_BGRA keeps 4
|
||||||
|
Uint8 Enabled; // 17
|
||||||
|
Uint8 Normalized; // 18
|
||||||
|
Uint8 IsInteger; // 19
|
||||||
|
// CARRIED SEPARATELY from Type == Float64, and it has to be: VertexAttribFormat(
|
||||||
|
// GL_DOUBLE) also reads doubles from memory but asks for them converted to float,
|
||||||
|
// while VertexAttribLFormat keeps all 64 bits. The backend's fp64 narrowing and its
|
||||||
|
// Adreno disabled-attribute workaround both key on telling the two apart.
|
||||||
|
Uint8 IsLong; // 20
|
||||||
|
Uint8 IsBgra; // 21
|
||||||
|
Uint8 BindingIndex; // 22 which MGPVertexBuffer entry feeds it (< MAX_VERTEX_ATTRIBS)
|
||||||
|
Uint8 Pad0; // 23
|
||||||
|
};
|
||||||
|
|
||||||
|
// The ARB_vertex_attrib_binding view. Buffer identity is again in set_vertex_buffers.
|
||||||
|
//
|
||||||
|
// WHY IT TRAVELS AT ALL, since no backend has ever read a binding point (the frontend
|
||||||
|
// resolves them eagerly into the flat view above, and grep finds zero backend reads of
|
||||||
|
// VertexBufferBindingPoint / GetAttributeBindingIndex / GetAttributeRelativeOffset): the
|
||||||
|
// record DECLARES BindingPointCount, PipeFields.def names it, and a record whose declared
|
||||||
|
// counts do not describe its own blob is a shape the applier's bounds gate would have to
|
||||||
|
// police forever. Carrying both views keeps the record self-describing, and the cost is
|
||||||
|
// paid once per configuration change rather than per draw - the blob rides only on
|
||||||
|
// create_vertex_elements.
|
||||||
|
struct MGPVertexBindingPointWire {
|
||||||
|
Uint64 Offset; // 0
|
||||||
|
Int32 Stride; // 8 GL 4.6 core table 23.4: the INITIAL value is 16, not 0
|
||||||
|
Uint32 Divisor; // 12
|
||||||
|
};
|
||||||
|
|
||||||
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
|
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
|
||||||
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
|
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
|
||||||
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
|
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
|
||||||
@@ -545,5 +615,16 @@ namespace MobileGL {
|
|||||||
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
|
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
|
||||||
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
|
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
|
||||||
sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
|
sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
|
||||||
|
// The two P3a wire views. Unlike the structs they mirror these ARE flat PODs with
|
||||||
|
// explicit padding, so the trip wire is the same one every MGPipe payload carries: the
|
||||||
|
// blob they ride in is memcpy'd, and a field silently changing width is a protocol break
|
||||||
|
// no test would otherwise see. (MGP_ASSERT_POD is MGPipeTypes.h's and that header
|
||||||
|
// includes this one, so the assertions are spelled out here instead.)
|
||||||
|
static_assert(std::is_trivially_copyable_v<MGPVertexAttribWire> &&
|
||||||
|
sizeof(MGPVertexAttribWire) == 24);
|
||||||
|
static_assert(std::is_standard_layout_v<MGPVertexAttribWire>);
|
||||||
|
static_assert(std::is_trivially_copyable_v<MGPVertexBindingPointWire> &&
|
||||||
|
sizeof(MGPVertexBindingPointWire) == 16);
|
||||||
|
static_assert(std::is_standard_layout_v<MGPVertexBindingPointWire>);
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
#endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H
|
#endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,20 @@
|
|||||||
// kScreen lands in struct MGPipeScreen, every other class in struct
|
// kScreen lands in struct MGPipeScreen, every other class in struct
|
||||||
// MGPipeContext (plan section 4.3).
|
// MGPipeContext (plan section 4.3).
|
||||||
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
|
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
|
||||||
|
// 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
|
// 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
|
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
|
||||||
@@ -71,9 +85,21 @@
|
|||||||
// clang-format off
|
// clang-format off
|
||||||
#define MGP_CALL_LIST(X) \
|
#define MGP_CALL_LIST(X) \
|
||||||
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
|
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
|
||||||
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
|
/* kHasBlob because MGPCaps owns TWO MGPBlobRef members - FormatCapabilities and */ \
|
||||||
|
/* RendererInfo (MGPipeTypes.h). It carried none until P5 R-13.1; the flag is a */ \
|
||||||
|
/* property of the payload, so its absence was simply wrong. Adding a flag does not */ \
|
||||||
|
/* move an opcode: the opcode is this line's 1-based position. */ \
|
||||||
|
X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob) \
|
||||||
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
|
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
|
||||||
X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \
|
/* NO kHasBlob, BY RULING (P5 R-13.3; CONTRACT-P5.md table 1 row 19). MGPResourceDesc */ \
|
||||||
|
/* owns no MGPBlobRef and gains none: under split `initialBytes` is always nullptr and */ \
|
||||||
|
/* the initial content of a glBufferData(size, data) / glTexImage*(..., data) arrives */ \
|
||||||
|
/* 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 */ \
|
||||||
|
/* this ruling; table 1 row 19 gives it the two pad fields of MGPResourceDesc. */ \
|
||||||
|
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck) \
|
||||||
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
|
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
|
||||||
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
|
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
|
||||||
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
|
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
|
||||||
@@ -95,7 +121,11 @@
|
|||||||
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
|
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
|
||||||
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||||
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||||
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kNone) \
|
/* kHasBlob because MGPSamplerDesc owns an MGPBlobRef member, `Parameters` - the */ \
|
||||||
|
/* SamplerParameters POD byte for byte, borderColorForm included (P5 R-13.1). The */ \
|
||||||
|
/* companion pointer beside the record today is a TYPED frontend pointer */ \
|
||||||
|
/* (const SamplerParameters*), which is exactly what split may not carry. */ \
|
||||||
|
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob) \
|
||||||
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
|
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
|
||||||
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
|
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
|
||||||
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
|
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
|
||||||
@@ -128,6 +158,13 @@
|
|||||||
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
|
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
|
||||||
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
|
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
|
||||||
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
|
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
|
||||||
|
/* NO kHasBlob AND NO MGPBlobRef, BY RULING (P5 R-13.2; CONTRACT-P5.md table 1 row 21). */ \
|
||||||
|
/* It takes a content pointer today, but under split it CARRIES NO BYTES AT ALL: it is */ \
|
||||||
|
/* a {range, AccessFlags} control record, and the bytes of [Offset, Offset+Size) arrive */ \
|
||||||
|
/* AHEAD of it as resource_subdata records covering exactly that range. The ladder this */ \
|
||||||
|
/* drives rewrites its range "from the authoritative shadow" (Managers.cpp:1047-1076), */ \
|
||||||
|
/* and under split the authoritative shadow is server-owned (R-11), so subdata is the */ \
|
||||||
|
/* only way bytes reach it - a blobref here would be a SECOND way to say the same thing. */ \
|
||||||
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
|
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
|
||||||
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
|
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
|
||||||
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
|
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
|
||||||
|
|||||||
@@ -41,10 +41,16 @@
|
|||||||
#define MGP_FIELDS_MGPCaps(F) \
|
#define MGP_FIELDS_MGPCaps(F) \
|
||||||
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
|
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
|
||||||
|
|
||||||
|
// The three respecify-scope fields are listed like any other member: they occupy what used to be
|
||||||
|
// Pad0's high byte and Pad1, and the generator's cover check only excludes members actually
|
||||||
|
// NAMED Pad*, so leaving them out would fail --check rather than pass it. They are also real
|
||||||
|
// G4 comparands - a verify build that let the scope differ silently would be blind to the one
|
||||||
|
// thing that decides whether a respecify drops one pending upload or all of them.
|
||||||
#define MGP_FIELDS_MGPResourceDesc(F) \
|
#define MGP_FIELDS_MGPResourceDesc(F) \
|
||||||
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
|
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
|
||||||
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
|
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
|
||||||
F(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
|
F(HasDefinedContent) F(ImageBindableHint) F(HasRespecifiedLevel) F(GlNameForDiag) \
|
||||||
|
F(RespecifiedUploadTarget) F(RespecifiedLevel) F(ViewOf) F(BufferForTexBuffer) \
|
||||||
F(BufOffset) F(BufSize)
|
F(BufOffset) F(BufSize)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPFenceWait(F) \
|
#define MGP_FIELDS_MGPFenceWait(F) \
|
||||||
@@ -78,26 +84,36 @@
|
|||||||
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
||||||
F(Samples) F(FixedSampleLocations)
|
F(Samples) F(FixedSampleLocations)
|
||||||
|
|
||||||
|
// P4a, D-E1: BuiltinSampler and SamplerResync. Pad0 stays unlisted - gen_pipe.py's
|
||||||
|
// PADDING_MEMBER_RE (^Pad\d*$) excludes it, and a member that stops being called Pad<n> MUST
|
||||||
|
// gain a row here or pipe-gates goes red.
|
||||||
#define MGP_FIELDS_MGPTextureParams(F) \
|
#define MGP_FIELDS_MGPTextureParams(F) \
|
||||||
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) F(MinLod) F(MaxLod) \
|
F(Res) F(BuiltinSampler) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) \
|
||||||
F(LodBias)
|
F(SamplerResync) F(MinLod) F(MaxLod) F(LodBias)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPProgramDesc(F) \
|
#define MGP_FIELDS_MGPProgramDesc(F) \
|
||||||
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
||||||
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
|
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
|
||||||
|
|
||||||
|
// P4a, ID-12 / esprytobj DV-5: Pad0 became Uint16 TextureTarget. Same trip wire as
|
||||||
|
// MGPFramebufferState's Target below - PADDING_MEMBER_RE only excludes a member still NAMED
|
||||||
|
// Pad<n>, so the rename without this row is a pipe-gates failure, and the row without the
|
||||||
|
// rename is one too. A meaning-carrying byte cannot enter this record silently.
|
||||||
#define MGP_FIELDS_MGPSurface(F) \
|
#define MGP_FIELDS_MGPSurface(F) \
|
||||||
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
|
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget) F(TextureTarget)
|
||||||
|
|
||||||
|
// P4a, D-C2: Pad0 became Uint8 Target, and gen_pipe.py's PADDING_MEMBER_RE only excludes a
|
||||||
|
// member still NAMED Pad<n> - so the rename without this row is a pipe-gates failure, which
|
||||||
|
// is exactly the trip wire that makes the byte impossible to add silently.
|
||||||
#define MGP_FIELDS_MGPFramebufferState(F) \
|
#define MGP_FIELDS_MGPFramebufferState(F) \
|
||||||
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
|
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
|
||||||
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(ContentHash)
|
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(Target) F(ContentHash)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
||||||
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPVertexBuffers(F) \
|
#define MGP_FIELDS_MGPVertexBuffers(F) \
|
||||||
F(Start) F(Count) F(ContentHash)
|
F(Start) F(Count) F(BaseInstance) F(ContentHash)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPIndexBuffer(F) \
|
#define MGP_FIELDS_MGPIndexBuffer(F) \
|
||||||
F(Res) F(Offset) F(IndexSize)
|
F(Res) F(Offset) F(IndexSize)
|
||||||
@@ -144,8 +160,11 @@
|
|||||||
#define MGP_FIELDS_MGPPatchState(F) \
|
#define MGP_FIELDS_MGPPatchState(F) \
|
||||||
F(Vertices) F(Outer) F(Inner)
|
F(Vertices) F(Outer) F(Inner)
|
||||||
|
|
||||||
|
// P2 ratcheted this block from six rows to one: RenderStateParameters retired to
|
||||||
|
// create/bind_render_state + set_dynamic_state, Pack to set_pixel_pack_state and the
|
||||||
|
// patch trio to set_patch_state. What is left is the redundant capability trip wire.
|
||||||
#define MGP_FIELDS_ResidualValueBlock(F) \
|
#define MGP_FIELDS_ResidualValueBlock(F) \
|
||||||
F(RenderState) F(Pack) F(CapabilityBits) F(PatchVertices) F(PatchOuter) F(PatchInner)
|
F(CapabilityBits)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPResidualValueState(F) \
|
#define MGP_FIELDS_MGPResidualValueState(F) \
|
||||||
F(Version) F(Blob)
|
F(Version) F(Blob)
|
||||||
@@ -231,7 +250,8 @@
|
|||||||
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
|
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
|
||||||
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
|
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
|
||||||
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
|
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
|
||||||
F(DepthMask) F(ColorMasks) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
|
F(DepthMask) F(ColorMasks) F(FramebufferSrgbEnabled) F(DepthClampEnabled) \
|
||||||
|
F(TextureCubeMapSeamlessEnabled) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
|
||||||
F(DepthRanges) F(SampleCoverageValue) F(SampleCoverageInvert) F(SampleMaskValue) \
|
F(DepthRanges) F(SampleCoverageValue) F(SampleCoverageInvert) F(SampleMaskValue) \
|
||||||
F(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
|
F(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
|
||||||
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
|
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
|
||||||
@@ -250,6 +270,18 @@
|
|||||||
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
||||||
F(Alignment)
|
F(Alignment)
|
||||||
|
|
||||||
|
// P4a, D-F1: THE PADDING TRAP. SamplerParameters is sizeof == 100 with THREE BYTES OF
|
||||||
|
// TRAILING PADDING (96 bytes of members plus the 1-byte borderColorForm) and had no field
|
||||||
|
// table and no MGP_VERIFY_PAYLOAD_LIST row at all, so MGPSamplerDesc's blob was compared as
|
||||||
|
// BYTES and MOBILEGL_PIPE_VERIFY could false-differ on uninitialised padding - a coin flip
|
||||||
|
// rather than a gate. With this list the comparator sees the sixteen members and the three
|
||||||
|
// bytes can never enter the answer. The client-side CSO cache hashes and memcmp-confirms over
|
||||||
|
// a ZERO-INITIALISED canonical copy for the same reason, which is the other half of D-F1.
|
||||||
|
#define MGP_FIELDS_SamplerParameters(F) \
|
||||||
|
F(wrapS) F(wrapT) F(wrapR) F(minFilter) F(magFilter) F(mipmapMode) F(minLod) F(maxLod) \
|
||||||
|
F(lodBias) F(maxAnisotropy) F(compareFunc) F(compareMode) F(borderColor) F(borderColorI) \
|
||||||
|
F(borderColorUI) F(borderColorForm)
|
||||||
|
|
||||||
#define MGP_FIELDS_PerBufferBlendState(F) \
|
#define MGP_FIELDS_PerBufferBlendState(F) \
|
||||||
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
||||||
F(AlphaEquation)
|
F(AlphaEquation)
|
||||||
@@ -291,6 +323,21 @@
|
|||||||
#define MGP_FIELDS_MGHostSpan(F) \
|
#define MGP_FIELDS_MGHostSpan(F) \
|
||||||
F(Ptr) F(Seg) F(Size) F(Offset)
|
F(Ptr) F(Seg) F(Size) F(Offset)
|
||||||
|
|
||||||
|
// P3a's two vertex wire views (MGPipeValueTypes.h). They are not call payloads either: they
|
||||||
|
// are the ELEMENTS of create_vertex_elements' blob, and the comparator has to see into them
|
||||||
|
// for the same reason it sees into the value structs - a blob compared with memcmp would
|
||||||
|
// false-differ on MGPVertexAttribWire::Pad0. Divisor is deliberately not in the attribute
|
||||||
|
// list (it travels in MGPVertexBuffer) and the two Legacy* query answers are deliberately not
|
||||||
|
// on the wire at all; both absences are argued in MGPipeValueTypes.h and both are enforced
|
||||||
|
// here by gen_pipe.py's "every list names exactly its struct's direct members" rule.
|
||||||
|
|
||||||
|
#define MGP_FIELDS_MGPVertexAttribWire(F) \
|
||||||
|
F(Offset) F(Stride) F(Type) F(Size) F(Enabled) F(Normalized) F(IsInteger) F(IsLong) F(IsBgra) \
|
||||||
|
F(BindingIndex)
|
||||||
|
|
||||||
|
#define MGP_FIELDS_MGPVertexBindingPointWire(F) \
|
||||||
|
F(Offset) F(Stride) F(Divisor)
|
||||||
|
|
||||||
// Every payload above, in the order the comparator is generated. Keep in sync with the
|
// Every payload above, in the order the comparator is generated. Keep in sync with the
|
||||||
// macros; gen_pipe.py reads THIS list to know what to emit.
|
// macros; gen_pipe.py reads THIS list to know what to emit.
|
||||||
#define MGP_VERIFY_PAYLOAD_LIST(P) \
|
#define MGP_VERIFY_PAYLOAD_LIST(P) \
|
||||||
@@ -307,7 +354,9 @@
|
|||||||
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
||||||
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
||||||
P(MGPSurfaceInfo) \
|
P(MGPSurfaceInfo) \
|
||||||
P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \
|
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||||
P(DynamicBackendParameters) P(MGHostSpan)
|
P(StencilFaceState) \
|
||||||
|
P(DynamicBackendParameters) P(MGHostSpan) \
|
||||||
|
P(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -33,10 +33,395 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// A no-op unless a context is live, a verb has been filled, and `field` is in that verb
|
// A no-op unless a context is live, a verb has been filled, and `field` is in that verb
|
||||||
// class's may-read mask; a forwarded (sticky) field has no storage and is never copied.
|
// class's may-read mask; a forwarded (sticky) field has no storage and is never copied.
|
||||||
void MGPipeNoteFrontendMutation(MGPipeInputField field);
|
void MGPipeNoteFrontendMutation(MGPipeInputField field);
|
||||||
|
|
||||||
|
// ---- the aggregate generations (P2 brief D4, 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.
|
||||||
|
enum class MGPipeAggregate : Uint32 {
|
||||||
|
// VertexArrayState: any VAO attribute format / buffer / enable moved.
|
||||||
|
VaoAttribute = 0,
|
||||||
|
// FramebufferState: any FBO attachment or default-geometry write, or a bind - and, since
|
||||||
|
// P4a (fable seam F-3), any STORAGE DEFINITION of a texture or a renderbuffer, because
|
||||||
|
// set_framebuffer_state inlines an attachment's format, extent and samples and those
|
||||||
|
// setters are the only writers of what it inlines (TextureObject.cpp /
|
||||||
|
// RenderbufferObject.cpp, PipePublishDescriptor).
|
||||||
|
FramebufferAttachment,
|
||||||
|
// TextureState: any texture object CONTENT moved (an upload, a dirty region).
|
||||||
|
TextureContent,
|
||||||
|
// TextureState: any texture object or sampler object PARAMETER moved.
|
||||||
|
TextureParams,
|
||||||
|
// BufferState: any buffer object contents moved.
|
||||||
|
BufferChange,
|
||||||
|
// GLContext: a glVertexAttrib* default value moved. Not one of D4 five: the bit it
|
||||||
|
// shutters (NEW_VERTEX_ATTRIB_DEFAULTS) is specified there as a ContentHash over
|
||||||
|
// all 32 CurrentVertexAttributeValues, and hashing 768 bytes on EVERY draw does not
|
||||||
|
// fit inside the T1 ceiling. The hash still decides whether to EMIT (D11 set-hash
|
||||||
|
// suppressor); this decides whether to hash at all.
|
||||||
|
VertexAttribDefault,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MG_Impl/Pipe/PipeFill.cpp. A no-op unless a context is live.
|
||||||
|
void MGPipeNoteAggregate(MGPipeAggregate aggregate);
|
||||||
|
|
||||||
|
// ---- P3a: the resource family's emission points (brief D-A1) ----
|
||||||
|
//
|
||||||
|
// The seven BufferBackendOps hooks already dispatch at the GL call that causes them
|
||||||
|
// (ARCHITECTURE.md 5.1 names them as the ONE exception to push-at-validate), so their
|
||||||
|
// pipe calls are emitted from the same BufferObject dispatchers rather than from the
|
||||||
|
// validate point. That puts the emission inside MG_State, which is why these are
|
||||||
|
// DECLARED here beside the two notices and DEFINED in MG_Impl/Pipe/PipeFill.cpp: this
|
||||||
|
// header is the one MG_State already includes for exactly this, and the closure gate
|
||||||
|
// (check_include_closure.py's mutation-header probe) keeps it a declaration - reaching
|
||||||
|
// MG_Impl/Pipe/ResourceTracker.h from BufferObject.cpp would pull the client's tracker
|
||||||
|
// into the state machine that calls it.
|
||||||
|
//
|
||||||
|
// The forward declaration is the whole coupling: none of these needs the definition of
|
||||||
|
// BufferObject, and this header must not gain it.
|
||||||
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class BufferObject;
|
||||||
|
// P4a's five, for the BIRTH half at the tail of this header. Declarations only, exactly as
|
||||||
|
// BufferObject is: none of the hooks below needs a definition, and this header must not
|
||||||
|
// gain one - reaching a frontend class header from here would put the state machine's own
|
||||||
|
// types in front of every mutator that spells MGP_NOTE_MUTATION.
|
||||||
|
class ITextureObject;
|
||||||
|
class RenderbufferObject;
|
||||||
|
class FramebufferObject;
|
||||||
|
class SamplerObject;
|
||||||
|
class ProgramObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Pipe {
|
||||||
|
// (Features.PipePush & kMGPipeSubsystemResources) != 0 && MGPipeGetResourceOps() != nullptr.
|
||||||
|
//
|
||||||
|
// BOTH HALVES MATTER. The bit is the operator's per-subsystem A/B; the table is "has a
|
||||||
|
// backend taken this family over at all". Until one has, every dispatch below falls
|
||||||
|
// through to the BufferBackendOps table it replaces and the tree behaves exactly as it
|
||||||
|
// did - which is what lets the client half land on its own.
|
||||||
|
Bool MGPipeResourceSubsystemEnabled();
|
||||||
|
// The nullable member, asked the way the frontend asks g_bufferBackendOps->ResidentSubData
|
||||||
|
// today: one backend deliberately does not implement it and the caller has a different
|
||||||
|
// path when it is absent (BufferObject::FillSubData).
|
||||||
|
Bool MGPipeResourceOpsHaveSubDataResident();
|
||||||
|
|
||||||
|
// Minted from the constructor and released from the destructor, both unconditionally in
|
||||||
|
// a push build: a handle is CLIENT state and set_vertex_buffers names it whether or not
|
||||||
|
// the resource family is switched on. The CALLS are what the predicate above gates.
|
||||||
|
void MGPipeMintResourceHandle(MG_State::GLState::BufferObject& buffer);
|
||||||
|
// In this order, and it is not negotiable (D-L): the destroy resolves the handle, and
|
||||||
|
// MGPipeSlotAllocator::Free erases the lifetimeId -> slot mapping it resolves through.
|
||||||
|
//
|
||||||
|
// RETURNS whether resource_destroy was emitted, which is the LATCH taken at this buffer's
|
||||||
|
// create and not a second reading of MGPipeResourceSubsystemEnabled(). The destructor
|
||||||
|
// needs that answer to decide whether the legacy OnDestroy still owes a call: asking the
|
||||||
|
// predicate twice pairs a create emitted under one registration with a destroy gated on
|
||||||
|
// another, and either direction leaks - a live applier record on a slot about to be
|
||||||
|
// re-handed-out, or a backend object nobody releases.
|
||||||
|
Bool MGPipeEmitResourceDestroyAndFree(MG_State::GLState::BufferObject& buffer);
|
||||||
|
|
||||||
|
// THE VERTEX-ELEMENTS CSO's DEATH, and it is BACKEND-NEUTRAL - which is the whole point.
|
||||||
|
// Before this, the only thing that ever returned a VertexElementsCso slot was DirectGLES'
|
||||||
|
// StateObjectDeathOps table; under any 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 ~1.3 KB applier record for the life of the process, on the shipped
|
||||||
|
// 0x1ff mask, and past 65536 slots every create_vertex_elements became a permanent
|
||||||
|
// Fatal{ProtocolCorruption}. The client mints the slot, so the client is where the death
|
||||||
|
// has to be spoken from.
|
||||||
|
//
|
||||||
|
// Takes the lifetime id and not the object for StateObjectDeathNotice.h's reason: the last
|
||||||
|
// SharedPtr has already dropped by the time this runs, and the lifetime id is what the
|
||||||
|
// slot allocator resolves the handle from. Returns whether delete_vertex_elements went
|
||||||
|
// out, i.e. whether the applier actually held a record - see the definition for why that
|
||||||
|
// is asked rather than assumed.
|
||||||
|
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
|
||||||
|
// ---- P4a: ONE CLIENT-SIDE DEATH HELPER PER KIND P4a MINTS (brief D-I1) ----
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// ResourceDestroy, and then the SamplerViewCso minted off this same lifetime id (P4a
|
||||||
|
// D-F2: one sampler view per ITextureObject). Called from TextureObjectBase's VIRTUAL
|
||||||
|
// destructor, so 2D / 3D / cube / buffer / view all announce exactly once.
|
||||||
|
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// ResourceDestroy.
|
||||||
|
Bool MGPipeEmitRenderbufferDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// NO WIRE CALL AT ALL (D-I2). PipeCalls.def has no framebuffer delete, because a
|
||||||
|
// framebuffer is not a resource and is not a CSO - it is STATE, and set_framebuffer_state
|
||||||
|
// is the only call that names one - and the catalogue is closed, so P4a does not invent a
|
||||||
|
// row. The handle is minted and freed entirely client-side and this helper does steps 2
|
||||||
|
// and 3 only. A recycled framebuffer handle is distinguished by Gen, which is inside the
|
||||||
|
// record's ContentHash, so it can never be suppressed against its predecessor's record.
|
||||||
|
Bool MGPipeEmitFramebufferDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteSamplerState. Also the path the content-addressed CSO cache's LRU eviction takes,
|
||||||
|
// which is why it is addressed by lifetime id and not by "the object that owns it".
|
||||||
|
Bool MGPipeEmitSamplerCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteSamplerView. Called by the texture helper above; a sampler view has no frontend
|
||||||
|
// object of its own, so this is the only path there is.
|
||||||
|
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteShaderState, for an ordinary program AND for a program-pipeline COMPOSITE, whose
|
||||||
|
// slot has two independent release paths - the pipeline cache's LRU eviction and the
|
||||||
|
// composite ProgramObject's own destructor. One helper for both, and the second call is a
|
||||||
|
// proven no-op.
|
||||||
|
Bool MGPipeEmitShaderCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
|
||||||
|
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
||||||
|
void MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
||||||
|
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
||||||
|
void MGPipeEmitBufferSubDataResident(MG_State::GLState::BufferObject& buffer, SizeT offset,
|
||||||
|
const void* bytes, SizeT size);
|
||||||
|
void MGPipeEmitResourceFlushRange(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size,
|
||||||
|
Uint32 accessFlags);
|
||||||
|
void MGPipeEmitResourceReadback(MG_State::GLState::BufferObject& buffer);
|
||||||
|
// Returns the coherent host pointer the resource owner donated, or null for a DECLINE -
|
||||||
|
// which is a real answer. Every call, mint or decline, is one map-persistent roundtrip.
|
||||||
|
void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer);
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// P4a: THE BIRTH HALF, one hook per client path MG_State owns (D-C .. D-I)
|
||||||
|
// ================================================================================
|
||||||
|
//
|
||||||
|
// 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).
|
||||||
|
|
||||||
|
// ---- the publication latch (D-I1), and it is the ONE answer both halves read ----
|
||||||
|
//
|
||||||
|
// The create is gated at its call site and the destroy inside the death helper, so the
|
||||||
|
// two ask the same question at two different moments. An object born while its 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 the allocator is about to hand out again. A
|
||||||
|
// slot is NOT evidence of a record either: a backend twin table mints one through
|
||||||
|
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client to emit a
|
||||||
|
// create, and a delete_* on such a handle is a refused call the applier asserts on.
|
||||||
|
//
|
||||||
|
// So the emitter latches the answer when its create actually goes out, the death helper
|
||||||
|
// reads the latch, and the latch is keyed by {kind, slot, gen} so a recycled slot cannot
|
||||||
|
// inherit its predecessor's answer. Defined in PipeFill.cpp beside the six death helpers,
|
||||||
|
// declared here because both the helpers and the five emit headers read it.
|
||||||
|
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle);
|
||||||
|
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle);
|
||||||
|
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle);
|
||||||
|
|
||||||
|
// ---- the four mints (pure allocator work, no family knowledge) ----
|
||||||
|
//
|
||||||
|
// UNCONDITIONAL in a push build, for MGPipeMintResourceHandle's reason: a handle is CLIENT
|
||||||
|
// state and other subsystems name these objects by handle whether or not their own family
|
||||||
|
// is switched on - MGPSurface::Res names a Texture or a Renderbuffer out of the framebuffer
|
||||||
|
// subsystem, MGPBoundView::Texture and MGPImageView::Res name a Texture out of the sampler
|
||||||
|
// one. Gating the mint on the family bit would make those emit null handles in exactly the
|
||||||
|
// A/B arm that exists to isolate the families. Each costs one free-list pop and one map
|
||||||
|
// insert per object and emits nothing.
|
||||||
|
void MGPipeMintTextureHandle(MG_State::GLState::ITextureObject& texture);
|
||||||
|
void MGPipeMintRenderbufferHandle(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||||
|
// A framebuffer has a handle and NO wire lifetime (D-I2): set_framebuffer_state is the only
|
||||||
|
// call that names one, and there is no create or destroy for the kind. The mint is still
|
||||||
|
// the object's, so the identity exists before the first validate point that pushes it.
|
||||||
|
void MGPipeMintFramebufferHandle(MG_State::GLState::FramebufferObject& framebuffer);
|
||||||
|
// Ordinary programs only. A program-pipeline COMPOSITE is minted by the composite resolver
|
||||||
|
// out of the reserved band through MGPipeSlotAllocator::AllocateComposite, which is the one
|
||||||
|
// 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 ----
|
||||||
|
//
|
||||||
|
// 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&)
|
||||||
|
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
|
||||||
|
|
||||||
|
// WHICH STORAGE A TEXTURE RESPECIFY REPLACES (P4a final review C-1). The applier scopes
|
||||||
|
// its pending-upload clear on this answer and not on the descriptor, because the
|
||||||
|
// descriptor cannot give it: AllocateStorage is per (uploadTarget, level) and
|
||||||
|
// TruncateMipmapLevels removes every level at or above a cut, while MGPResourceDesc
|
||||||
|
// carries only the base extent and the level count. A level the applier had ACCEPTED at
|
||||||
|
// one verb (the client's dirty flag already clear, D-D5 step 1) and that a later per-level
|
||||||
|
// definition redefined AROUND was dropped by the whole-resource arm with nobody owing its
|
||||||
|
// texels - so every respecify states its scope, and "whole resource" is said, never
|
||||||
|
// defaulted. The emitter builds wire's MGPRespecifiedLevel from the pair, packed exactly
|
||||||
|
// as the drain packs a sub-data record's Target (MGPipePackSubDataTarget), so the key it
|
||||||
|
// drops is the key that level's emission made.
|
||||||
|
enum class MGPipeTextureRespecifyScope : Uint32 {
|
||||||
|
// The whole store is redefined or restated: a format, sample-count or
|
||||||
|
// fixed-sample-locations change, an immutable allocation completing
|
||||||
|
// (SetImmutableLevels), a texture view's creation. Every pending upload goes.
|
||||||
|
WholeResource = 0,
|
||||||
|
// ONE (uploadTarget, level) was (re)allocated: glTexImage*D, glCompressedTexImage*D,
|
||||||
|
// glCopyTexImage*D, one level of a glTexStorage* loop, one level of a generated-mipmap
|
||||||
|
// grow. That level's pending upload goes; every other level's stays. `uploadTarget` and
|
||||||
|
// `level` name it.
|
||||||
|
OneLevel = 1,
|
||||||
|
// The chain was cut: every level of `uploadTarget` at or above `level` is gone and the
|
||||||
|
// levels below it are untouched (glGenerateMipmap fitting the chain, a base-level
|
||||||
|
// redefinition discarding its tail, glTexStorage* fitting the chain to its level
|
||||||
|
// count). `level` is the first level removed; a cut at 0 is the whole resource.
|
||||||
|
LevelsFrom = 2,
|
||||||
|
};
|
||||||
|
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture,
|
||||||
|
MGPipeTextureRespecifyScope scope, Uint32 uploadTarget,
|
||||||
|
Uint32 level);
|
||||||
|
void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture);
|
||||||
|
// The DRAIN LIST's append, on a level's FIRST dirty mark, keyed on the STORAGE OWNER from
|
||||||
|
// day one (D-D4: a view and its owner already share one dirty state, so an upload through
|
||||||
|
// either lands on the same key). The record itself is emitted at the validate point by
|
||||||
|
// MGPipeTextureEmitter::DrainTextureSubData; this is only what puts the level on the list,
|
||||||
|
// and walking every live texture per verb is the cost it exists to avoid.
|
||||||
|
void MGPipeNoteTextureLevelDirty(MG_State::GLState::ITextureObject& storageOwner, Uint32 uploadTarget,
|
||||||
|
Uint32 level);
|
||||||
|
void MGPipeEmitRenderbufferResourceCreate(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||||
|
void MGPipeEmitRenderbufferResourceRespecify(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||||
|
|
||||||
|
// ---- D-A4's two sticky bind-mask producers (P4a final review M-A) ----
|
||||||
|
//
|
||||||
|
// kMGPipeBindSampler is "any texture the sampler-view resolution names in an emitted
|
||||||
|
// MGPBoundView" and kMGPipeBindShaderImage "any texture named in an emitted MGPImageView"
|
||||||
|
// - both the SAMPLER package's emitters (SamplerEmit.h, ImageEmit.h), which the texture
|
||||||
|
// emitter's header includes and which therefore cannot include it back - and, earliest of
|
||||||
|
// all, glBindImageTexture's state setter (TextureState.h, MG_State), which may include no
|
||||||
|
// emit header at all. So the note goes through this door, exactly as the birth hooks do.
|
||||||
|
// Nothing produced either bit before the fix round: ImageBindableHint was always 0, the
|
||||||
|
// metadata respecify (ID-18 M4) had no live trigger, and the remint pull the hint exists to
|
||||||
|
// prevent was neither prevented nor counted.
|
||||||
|
//
|
||||||
|
// UNCONDITIONAL IN A PUSH BUILD, like the mints: the mask is CLIENT state the framebuffer
|
||||||
|
// emitter ORs into whether or not the texture family is on, and the emission a mask move
|
||||||
|
// causes (the metadata respecify) is gated inside the emitter on the family's own pair.
|
||||||
|
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit);
|
||||||
|
// glBindImageTexture. The hint is the PREVENTION half of the texture-remint stall class -
|
||||||
|
// a texture the server knows may be image-bound is allocated image-bindable up front - so it
|
||||||
|
// has to reach the applier before the texture's first sync, i.e. at the bind itself, not at
|
||||||
|
// the validate point's image walk (which notes it as well, D-A4's letter).
|
||||||
|
void MGPipeNoteTextureImageBound(MG_State::GLState::ITextureObject& texture);
|
||||||
|
|
||||||
|
|
||||||
|
// ---- sampler CSOs and sampler views: MG_Impl/Pipe/SamplerEmit.h, package C ----
|
||||||
|
//
|
||||||
|
// Entry points MGPipeSamplerEmitter must provide, returning void:
|
||||||
|
// EmitSamplerCso(SamplerObject&) - D-F1's content-addressed mint-or-share at
|
||||||
|
// capacity 256, hashed field-wise over a canonical
|
||||||
|
// zero-initialised copy, behind the version-first
|
||||||
|
// skip. The HANDLE RULE FOR THIS KIND IS THE
|
||||||
|
// EMITTER'S, not this file's: two identical
|
||||||
|
// samplers share one CSO, so there is deliberately
|
||||||
|
// no per-object mint above, and it is the emitter
|
||||||
|
// that decides which lifetime id (if any) owns the
|
||||||
|
// slot the death helper will resolve.
|
||||||
|
// EmitSamplerView(ITextureObject&) - D-F2's ONE view per texture object, minted off
|
||||||
|
// the texture's own lifetime id and re-issued on
|
||||||
|
// the SAME handle when the restrictions move.
|
||||||
|
void MGPipeEmitSamplerCsoCreate(MG_State::GLState::SamplerObject& sampler);
|
||||||
|
void MGPipeEmitSamplerViewCreate(MG_State::GLState::ITextureObject& texture);
|
||||||
|
|
||||||
|
// ---- programs: MG_Impl/Pipe/ProgramEmit.h, package C ----
|
||||||
|
//
|
||||||
|
// Entry point MGPipeProgramEmitter must provide, returning void:
|
||||||
|
// EmitShaderCso(ProgramObject&)
|
||||||
|
//
|
||||||
|
// Re-issued on the SAME handle whenever the link version moves, exactly as
|
||||||
|
// create_vertex_elements is (Gen moves only on slot reuse). D-H4 keeps the TRACKER out of
|
||||||
|
// it - bit 6's shutter reads GetCurrentProgram() and deliberately not GetProgramForDraw(),
|
||||||
|
// because the tracker must not force a compile to answer "did the shader move" - so the
|
||||||
|
// ordinary emission is the validate point's, from the join the verb was going to make
|
||||||
|
// anyway. This hook exists for the paths that are NOT a draw: a link that completes off
|
||||||
|
// the draw path still owns its own publication.
|
||||||
|
void MGPipeEmitShaderCsoCreate(MG_State::GLState::ProgramObject& program);
|
||||||
} // namespace MobileGL::MG_Pipe
|
} // namespace MobileGL::MG_Pipe
|
||||||
#define MGP_NOTE_MUTATION(Field) \
|
#define MGP_NOTE_MUTATION(Field) \
|
||||||
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
||||||
|
#define MGP_NOTE_AGGREGATE(Aggregate) \
|
||||||
|
::MobileGL::MG_Pipe::MGPipeNoteAggregate(::MobileGL::MG_Pipe::MGPipeAggregate::Aggregate)
|
||||||
#else
|
#else
|
||||||
#define MGP_NOTE_MUTATION(Field) ((void)0)
|
#define MGP_NOTE_MUTATION(Field) ((void)0)
|
||||||
|
#define MGP_NOTE_AGGREGATE(Aggregate) ((void)0)
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -299,6 +299,107 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
|
|||||||
"SetStreamOutputTargets",
|
"SetStreamOutputTargets",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// P2 brief D5: the call that now SUPPLIES a field, so the residual fill loop no
|
||||||
|
// longer pulls it out of GLContext. kNone means the field is still pulled - which
|
||||||
|
// is what makes MOBILEGL_PIPE_PUSH a true per-subsystem A/B instead of a single
|
||||||
|
// switch. Rows come from Coverage.def's MGP_COVERAGE_EMITTED_LIST.
|
||||||
|
enum class MGPipeFieldEmitter : Uint8 {
|
||||||
|
kNone = 0,
|
||||||
|
BindRenderState,
|
||||||
|
BindVertexElements,
|
||||||
|
CreateRenderState,
|
||||||
|
SetDispatchProgram,
|
||||||
|
SetDrawProgram,
|
||||||
|
SetDynamicState,
|
||||||
|
SetFramebufferState,
|
||||||
|
SetPatchState,
|
||||||
|
SetSamplerViews,
|
||||||
|
SetShaderImages,
|
||||||
|
SetVertexAttribDefaults,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr const char* kMGPipeFieldEmitterNames[] = {
|
||||||
|
"kNone",
|
||||||
|
"BindRenderState",
|
||||||
|
"BindVertexElements",
|
||||||
|
"CreateRenderState",
|
||||||
|
"SetDispatchProgram",
|
||||||
|
"SetDrawProgram",
|
||||||
|
"SetDynamicState",
|
||||||
|
"SetFramebufferState",
|
||||||
|
"SetPatchState",
|
||||||
|
"SetSamplerViews",
|
||||||
|
"SetShaderImages",
|
||||||
|
"SetVertexAttribDefaults",
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount] = {
|
||||||
|
MGPipeFieldEmitter::kNone, // GetActiveTextureUnit
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetBlendColor
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetBlendEquationIndexed
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetBlendFuncIndexed
|
||||||
|
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackName
|
||||||
|
MGPipeFieldEmitter::BindVertexElements, // GetBoundVertexArray
|
||||||
|
MGPipeFieldEmitter::kNone, // GetBufferBindingSlot
|
||||||
|
MGPipeFieldEmitter::kNone, // GetBufferBindingPoint
|
||||||
|
MGPipeFieldEmitter::kNone, // GetBufferBindingPointCount
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTouchedBufferBindingPointCount
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetClampReadColor
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetClearColor
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetClearDepth
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetClearStencil
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetColorMaskIndexed
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetCullFaceMode
|
||||||
|
MGPipeFieldEmitter::SetVertexAttribDefaults, // GetCurrentVertexAttribute
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetDepthFunc
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetDepthMask
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetDepthRangeIndexed
|
||||||
|
MGPipeFieldEmitter::SetFramebufferState, // GetFramebufferBindingSlot
|
||||||
|
MGPipeFieldEmitter::SetShaderImages, // GetImageTextureBinding
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetLineWidth
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetLogicOp
|
||||||
|
MGPipeFieldEmitter::SetSamplerViews, // GetMaxTouchedTextureUnit
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetMinSampleShadingValue
|
||||||
|
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultInnerLevel
|
||||||
|
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultOuterLevel
|
||||||
|
MGPipeFieldEmitter::SetPatchState, // GetPatchVertices
|
||||||
|
MGPipeFieldEmitter::BindRenderState, // GetPipelineStateVersion
|
||||||
|
MGPipeFieldEmitter::kNone, // GetPixelStoreParameters
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetPolygonModeFront
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetFactor
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetUnits
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetPrimitiveRestartIndex
|
||||||
|
MGPipeFieldEmitter::SetDispatchProgram, // GetProgramForDispatch
|
||||||
|
MGPipeFieldEmitter::SetDrawProgram, // GetProgramForDraw
|
||||||
|
MGPipeFieldEmitter::kNone, // GetProgramObject
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetProvokingVertexMode
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetRenderStateParameters
|
||||||
|
MGPipeFieldEmitter::BindRenderState, // GetRenderStateParametersVersion
|
||||||
|
MGPipeFieldEmitter::kNone, // GetSamplingResolutionGeneration
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetScissorBox
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // GetStencilState
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTextureBindGeneration
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTextureContextId
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTextureObject
|
||||||
|
MGPipeFieldEmitter::SetSamplerViews, // GetTextureUnitObject
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackCapturedVertices
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackGeneration
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackPausedPrimitiveCounter
|
||||||
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackProgram
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetViewport
|
||||||
|
MGPipeFieldEmitter::SetDynamicState, // GetViewportIndexed
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // IsCapabilityEnabled
|
||||||
|
MGPipeFieldEmitter::CreateRenderState, // IsCapabilityEnabledIndexed
|
||||||
|
MGPipeFieldEmitter::kNone, // IsTransformFeedbackActive
|
||||||
|
MGPipeFieldEmitter::kNone, // IsTransformFeedbackPaused
|
||||||
|
MGPipeFieldEmitter::kNone, // InvalidateCompileEnv
|
||||||
|
MGPipeFieldEmitter::kNone, // ValidateProgramName
|
||||||
|
MGPipeFieldEmitter::kNone, // RecordError
|
||||||
|
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
|
||||||
|
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
|
||||||
|
};
|
||||||
|
inline constexpr SizeT kMGPipeEmittedFieldCount = 40;
|
||||||
|
|
||||||
struct MGPipeFilledState {
|
struct MGPipeFilledState {
|
||||||
Uint64 CurrentVerbSerial;
|
Uint64 CurrentVerbSerial;
|
||||||
Uint64 FilledGen[kMGPipeInputFieldCount];
|
Uint64 FilledGen[kMGPipeInputFieldCount];
|
||||||
|
|||||||
@@ -14,54 +14,80 @@
|
|||||||
|
|
||||||
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
|
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
|
||||||
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
|
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
|
||||||
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
|
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves
|
||||||
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
|
// (MG_Test/Pipe/RenderStateSpansTest.cpp).
|
||||||
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
|
|
||||||
// derived from a list that was reviewed rather than invented.
|
|
||||||
//
|
//
|
||||||
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
|
// P2 replaced P0's provenance with a RULE, and the rule is the only thing that decides
|
||||||
// table freezes:
|
// membership: a member is pipeline state IF AND ONLY IF some public RenderState setter that
|
||||||
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
|
// calls BumpVersions() writes it. That is what makes the G7 invariant true by construction
|
||||||
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
|
// rather than by inspection, and it turns the subset into a strict SUPERSET of the 24
|
||||||
// backend read points are constant false today. Pipeline state or dead capability?
|
// members VulkanRenderer::ComputePipelineStateHash used to hash.
|
||||||
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
|
//
|
||||||
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
|
// The three questions P0 left open are ANSWERED here, and the answers are in this list:
|
||||||
// are handled elsewhere in the payload path rather than in the memo word.
|
// - FramebufferSrgb, DepthClamp and TextureCubeMapSeamless had NO STORAGE at all -
|
||||||
|
// SetCapability fell to "not supported currently" and IsCapabilityEnabled answered a
|
||||||
|
// compile-time false. P2 gave all three real storage in the three padding bytes between
|
||||||
|
// ColorMasks and ClearColor, and their setters call BumpVersions(), so: pipeline state.
|
||||||
|
// - ProvokingVertexModeSetting: SetProvokingVertexMode calls BumpVersions(), so pipeline.
|
||||||
|
// - FrontFaceModeSetting likewise. ClipOrigin and ClipDepthMode do NOT (SetClipControl is
|
||||||
|
// ++m_version only), so they are dynamic, in chunk D1.
|
||||||
//
|
//
|
||||||
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
|
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
|
||||||
// glViewport must not mint a new CSO.
|
// glViewport must not mint a new CSO.
|
||||||
|
|
||||||
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
|
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
|
||||||
"CullFaceEnabled",
|
|
||||||
"DepthTestEnabled",
|
|
||||||
"PolygonOffsetFillEnabled",
|
|
||||||
"RasterizerDiscardEnabled",
|
|
||||||
"ColorLogicOpEnabled",
|
|
||||||
"StencilTestEnabled",
|
|
||||||
"PrimitiveRestartEnabled",
|
|
||||||
"PrimitiveRestartFixedIndexEnabled",
|
|
||||||
"DepthMask",
|
|
||||||
"SampleShadingEnabled",
|
|
||||||
"MultisampleEnabled",
|
|
||||||
"SampleMaskEnabled",
|
|
||||||
"SampleMaskValue",
|
|
||||||
"MinSampleShadingValue",
|
|
||||||
"PatchVertices",
|
"PatchVertices",
|
||||||
"PatchDefaultOuterLevel",
|
"PatchDefaultOuterLevel",
|
||||||
"PatchDefaultInnerLevel",
|
"PatchDefaultInnerLevel",
|
||||||
"PolygonModeFront",
|
|
||||||
"CullFaceModeSetting",
|
|
||||||
"DepthFunc",
|
|
||||||
"LogicOp",
|
|
||||||
"StencilStates",
|
|
||||||
"BlendStates",
|
"BlendStates",
|
||||||
|
"LogicOp",
|
||||||
|
"DepthTestEnabled",
|
||||||
|
"DepthFunc",
|
||||||
|
"DepthMask",
|
||||||
"ColorMasks",
|
"ColorMasks",
|
||||||
|
"FramebufferSrgbEnabled",
|
||||||
|
"DepthClampEnabled",
|
||||||
|
"TextureCubeMapSeamlessEnabled",
|
||||||
|
"SampleCoverageValue",
|
||||||
|
"SampleCoverageInvert",
|
||||||
|
"SampleMaskValue",
|
||||||
|
"MinSampleShadingValue",
|
||||||
|
"StencilStates",
|
||||||
|
"CullFaceEnabled",
|
||||||
|
"CullFaceModeSetting",
|
||||||
|
"FrontFaceModeSetting",
|
||||||
|
"ProvokingVertexModeSetting",
|
||||||
|
"PolygonModeFront",
|
||||||
|
"PolygonModeBack",
|
||||||
|
"ColorLogicOpEnabled",
|
||||||
|
"DebugOutputEnabled",
|
||||||
|
"DebugOutputSynchronousEnabled",
|
||||||
|
"DitherEnabled",
|
||||||
|
"LineSmoothEnabled",
|
||||||
|
"MultisampleEnabled",
|
||||||
|
"PolygonOffsetFillEnabled",
|
||||||
|
"PolygonOffsetLineEnabled",
|
||||||
|
"PolygonOffsetPointEnabled",
|
||||||
|
"PolygonSmoothEnabled",
|
||||||
|
"PrimitiveRestartEnabled",
|
||||||
|
"PrimitiveRestartFixedIndexEnabled",
|
||||||
|
"RasterizerDiscardEnabled",
|
||||||
|
"SampleAlphaToCoverageEnabled",
|
||||||
|
"SampleAlphaToOneEnabled",
|
||||||
|
"SampleCoverageEnabled",
|
||||||
|
"SampleMaskEnabled",
|
||||||
|
"SampleShadingEnabled",
|
||||||
|
"StencilTestEnabled",
|
||||||
|
"ProgramPointSizeEnabled",
|
||||||
|
"ScissorTestEnabledMask",
|
||||||
};
|
};
|
||||||
inline constexpr SizeT kMGPipePipelineStateMemberCount = 24;
|
inline constexpr SizeT kMGPipePipelineStateMemberCount = 44;
|
||||||
static_assert(kMGPipePipelineStateMemberCount ==
|
static_assert(kMGPipePipelineStateMemberCount ==
|
||||||
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
|
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
|
||||||
|
|
||||||
// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets
|
// Defined by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes every
|
||||||
// in C++ with offsetof rather than guessing them in python.
|
// boundary in C++ with offsetof rather than guessing it in python. 7 pipeline
|
||||||
|
// chunks / 396 bytes and 8 dynamic chunks / 772 bytes, and the two halves
|
||||||
|
// partition [0, sizeof(RenderStateParameters)) exactly - asserted there.
|
||||||
extern const MGPStateChunk kMGPipePipelineChunks[];
|
extern const MGPStateChunk kMGPipePipelineChunks[];
|
||||||
extern const MGPStateChunk kMGPipeDynamicChunks[];
|
extern const MGPStateChunk kMGPipeDynamicChunks[];
|
||||||
|
|||||||
@@ -109,10 +109,13 @@ inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, con
|
|||||||
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
|
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
|
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField);
|
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField);
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
|
||||||
@@ -245,6 +248,8 @@ struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
|||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<SamplerParameters> : std::true_type {};
|
||||||
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
||||||
@@ -252,6 +257,10 @@ template <>
|
|||||||
struct MGPipeHasFieldVerifier<DynamicBackendParameters> : std::true_type {};
|
struct MGPipeHasFieldVerifier<DynamicBackendParameters> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGHostSpan> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGHostSpan> : std::true_type {};
|
||||||
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<MGPVertexAttribWire> : std::true_type {};
|
||||||
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<MGPVertexBindingPointWire> : std::true_type {};
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
|
||||||
@@ -623,6 +632,11 @@ inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParamete
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField) {
|
||||||
|
MGP_FIELDS_SamplerParameters(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
|
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
|
||||||
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
||||||
return true;
|
return true;
|
||||||
@@ -643,6 +657,16 @@ inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char**
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField) {
|
||||||
|
MGP_FIELDS_MGPVertexAttribWire(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField) {
|
||||||
|
MGP_FIELDS_MGPVertexBindingPointWire(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#undef MGP_VERIFY_FIELD
|
#undef MGP_VERIFY_FIELD
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
|
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 72;
|
||||||
|
|||||||
@@ -118,6 +118,127 @@ enum class MGPWireOp : Uint16 {
|
|||||||
kOpCount = 72,
|
kOpCount = 72,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
|
||||||
|
// documented as "MGPipeCallFlags of the call", and until this table existed nothing
|
||||||
|
// generated said what those were: every consumer that needed to know whether a record owns
|
||||||
|
// an MGPBlobRef, a variable tail or a reply slot had to hard-code its own copy of
|
||||||
|
// PipeCalls.def's fourth column, and six of them were about to. A hard-coded copy is how
|
||||||
|
// GetCaps and CreateSamplerState came to carry an MGPBlobRef member with no kHasBlob on
|
||||||
|
// their line at all - nothing compared the two, because nothing had both in one place.
|
||||||
|
//
|
||||||
|
// Index 0 is MGPWireOp::kInvalid and is kNone: the catalogue is 1-based, and an encoder
|
||||||
|
// that reads flags for an opcode it never got from the catalogue must see the empty set
|
||||||
|
// rather than another call's flags.
|
||||||
|
//
|
||||||
|
// kHasBlob here means EXACTLY "the payload owns an MGPBlobRef member". Three calls carry
|
||||||
|
// bytes without one - resource_respecify, resource_flush_range and map_persistent, whose
|
||||||
|
// companion pointers have no carrier - and they are deliberately NOT flagged; MG_Remote's
|
||||||
|
// CONTRACT-P5.md table 1 is where those live, because a decoder that trusts kHasBlob has
|
||||||
|
// to find a member to read.
|
||||||
|
inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
|
||||||
|
/* 0 kInvalid */ static_cast<Uint32>(kNone),
|
||||||
|
/* 1 GetCaps */ static_cast<Uint32>(kReplySlot | kHasBlob),
|
||||||
|
/* 2 ResourceCreate */ static_cast<Uint32>(kNone),
|
||||||
|
/* 3 ResourceRespecify */ static_cast<Uint32>(kNeedsAck),
|
||||||
|
/* 4 ResourceDestroy */ static_cast<Uint32>(kNone),
|
||||||
|
/* 5 MapPersistent */ static_cast<Uint32>(kReplySlot | kOptional),
|
||||||
|
/* 6 UnmapPersistent */ static_cast<Uint32>(kOptional),
|
||||||
|
/* 7 FenceCreate */ static_cast<Uint32>(kNone),
|
||||||
|
/* 8 FenceStatus */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 9 FenceWait */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 10 FenceDestroy */ static_cast<Uint32>(kNone),
|
||||||
|
/* 11 QueryCreate */ static_cast<Uint32>(kNone),
|
||||||
|
/* 12 QueryBegin */ static_cast<Uint32>(kNone),
|
||||||
|
/* 13 QueryEnd */ static_cast<Uint32>(kNone),
|
||||||
|
/* 14 QueryAvailable */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 15 QueryResult */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 16 QueryDestroy */ static_cast<Uint32>(kNone),
|
||||||
|
/* 17 CreateRenderState */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 18 BindRenderState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 19 DeleteRenderState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 20 CreateVertexElements */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 21 BindVertexElements */ static_cast<Uint32>(kNone),
|
||||||
|
/* 22 DeleteVertexElements */ static_cast<Uint32>(kNone),
|
||||||
|
/* 23 CreateSamplerState */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 24 DeleteSamplerState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 25 CreateSamplerView */ static_cast<Uint32>(kNone),
|
||||||
|
/* 26 DeleteSamplerView */ static_cast<Uint32>(kNone),
|
||||||
|
/* 27 CreateShaderState */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 28 BindShaderState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 29 DeleteShaderState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 30 SetDynamicState */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 31 SetFramebufferState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 32 SetVertexBuffers */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 33 SetIndexBuffer */ static_cast<Uint32>(kNone),
|
||||||
|
/* 34 SetIndirectBuffers */ static_cast<Uint32>(kNone),
|
||||||
|
/* 35 SetSamplerViews */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 36 BindSamplerStates */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 37 SetShaderImages */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 38 SetShaderBuffers */ static_cast<Uint32>(kVarTail | kHostSpan),
|
||||||
|
/* 39 SetStreamOutputTargets */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 40 SetGlobalConstants */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 41 SetVertexAttribDefaults */ static_cast<Uint32>(kVarTail),
|
||||||
|
/* 42 SetPixelPackState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 43 SetPatchState */ static_cast<Uint32>(kNone),
|
||||||
|
/* 44 SetDrawProgram */ static_cast<Uint32>(kNone),
|
||||||
|
/* 45 SetDispatchProgram */ static_cast<Uint32>(kNone),
|
||||||
|
/* 46 SetResidualValueState */ static_cast<Uint32>(kHasBlob),
|
||||||
|
/* 47 SetTextureParams */ static_cast<Uint32>(kNone),
|
||||||
|
/* 48 ResourceSubData */ static_cast<Uint32>(kHasBlob | kVarTail),
|
||||||
|
/* 49 BufferSubDataResident */ static_cast<Uint32>(kHasBlob | kOptional),
|
||||||
|
/* 50 ResourceSubDataComplete */ static_cast<Uint32>(kNone),
|
||||||
|
/* 51 ResourceFlushRange */ static_cast<Uint32>(kNone),
|
||||||
|
/* 52 ResourceReadback */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 53 ResourceCopyRegion */ static_cast<Uint32>(kNone),
|
||||||
|
/* 54 GenerateMipmap */ static_cast<Uint32>(kNone),
|
||||||
|
/* 55 GetTextureImage */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 56 Blit */ static_cast<Uint32>(kNone),
|
||||||
|
/* 57 Clear */ static_cast<Uint32>(kNone),
|
||||||
|
/* 58 ReadPixels */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 59 DrawVbo */ static_cast<Uint32>(kHostSpan | kVarTail),
|
||||||
|
/* 60 LaunchGrid */ static_cast<Uint32>(kNone),
|
||||||
|
/* 61 MemoryBarrier */ static_cast<Uint32>(kNone),
|
||||||
|
/* 62 BeginStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
|
/* 63 EndStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
|
/* 64 PauseStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
|
/* 65 ResumeStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
|
/* 66 Flush */ static_cast<Uint32>(kNone),
|
||||||
|
/* 67 Present */ static_cast<Uint32>(kNone),
|
||||||
|
/* 68 SetSwapInterval */ static_cast<Uint32>(kOptional),
|
||||||
|
/* 69 QueryTimestamp */ static_cast<Uint32>(kReplySlot),
|
||||||
|
/* 70 QueryCounter */ static_cast<Uint32>(kNone),
|
||||||
|
/* 71 FenceWaitServer */ static_cast<Uint32>(kNone),
|
||||||
|
};
|
||||||
|
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
|
||||||
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
||||||
|
"the flags table and the opcode space disagree");
|
||||||
|
|
||||||
|
// The only supported read of the table. Out-of-range is kNone rather than undefined
|
||||||
|
// behaviour, because the one caller that can pass a bad opcode is a decoder holding bytes
|
||||||
|
// off a stream, and it must reach its own Fatal{ProtocolCorruption} rather than read past
|
||||||
|
// the array on the way there.
|
||||||
|
inline constexpr Uint32 MGPipeCallFlagsFor(MGPWireOp op) {
|
||||||
|
const SizeT index = static_cast<SizeT>(op);
|
||||||
|
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kMGPipeCallFlags[index]
|
||||||
|
: static_cast<Uint32>(kNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spot checks the generator states about its own output, so that a catalogue edit that
|
||||||
|
// silently drops a flag is a build break here and not a wrong decode six packages away.
|
||||||
|
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
|
||||||
|
"opcode 0 is not a call and carries no flags");
|
||||||
|
static_assert((MGPipeCallFlagsFor(MGPWireOp::GetCaps) & static_cast<Uint32>(kHasBlob)) != 0,
|
||||||
|
"MGPCaps owns two MGPBlobRef members; R-13.1 gave the call its flag");
|
||||||
|
static_assert((MGPipeCallFlagsFor(MGPWireOp::CreateSamplerState) & static_cast<Uint32>(kHasBlob)) != 0,
|
||||||
|
"MGPSamplerDesc owns an MGPBlobRef member; R-13.1 gave the call its flag");
|
||||||
|
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceFlushRange) & static_cast<Uint32>(kHasBlob)) == 0,
|
||||||
|
"R-13.2: resource_flush_range carries no bytes on the wire and owns no blobref");
|
||||||
|
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceRespecify) & static_cast<Uint32>(kHasBlob)) == 0,
|
||||||
|
"R-13.3: initial bytes follow as resource_subdata; MGPResourceDesc owns no blobref");
|
||||||
|
static_assert((MGPipeCallFlagsFor(MGPWireOp::DrawVbo) &
|
||||||
|
static_cast<Uint32>(kHostSpan | kVarTail)) == static_cast<Uint32>(kHostSpan | kVarTail),
|
||||||
|
"draw_vbo is the conditional-tail plus host-span shape the codec is measured on");
|
||||||
|
|
||||||
struct alignas(8) MGPWireRec_GetCaps {
|
struct alignas(8) MGPWireRec_GetCaps {
|
||||||
MGPWireRecHeader Header;
|
MGPWireRecHeader Header;
|
||||||
MGPCaps Payload;
|
MGPCaps Payload;
|
||||||
@@ -929,3 +1050,11 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
|
|||||||
}
|
}
|
||||||
|
|
||||||
#undef MGP_WIRE_CHECK_BOUNDS
|
#undef MGP_WIRE_CHECK_BOUNDS
|
||||||
|
|
||||||
|
// The ResidualValueBlock layout, from PipeFields.def's
|
||||||
|
// MGP_FIELDS_ResidualValueBlock. Retiring a field without lowering
|
||||||
|
// MGL_RESIDUAL_BLOCK_SIZE is a build break, which is the point.
|
||||||
|
static_assert(offsetof(ResidualValueBlock, CapabilityBits) == 0,
|
||||||
|
"the residual block's first member must sit at offset 0");
|
||||||
|
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
|
||||||
|
"the residual ratchet only ever goes down");
|
||||||
|
|||||||
@@ -0,0 +1,575 @@
|
|||||||
|
# CONTRACT-P5 — the four tables every P5 package is held to
|
||||||
|
|
||||||
|
Authority: this file. `~/w7/notes/p5/BRIEF-P5.md` states the rulings R-1…R-14; this file is
|
||||||
|
where they become implementable, and where the rulings the brief left to the contract package
|
||||||
|
are made. Where the two disagree, this file is newer and this file wins — and §6 lists every
|
||||||
|
place they disagree, so nobody has to diff them.
|
||||||
|
|
||||||
|
**How to change it.** `MG_Remote/CONTRACT-P5.md` is c0's file. A package that needs a row
|
||||||
|
changed goes through the integrator, who edits here first and only then lets the package land.
|
||||||
|
P4a's contract was corrected seven times and each correction cost a package a rework round;
|
||||||
|
the point of this file existing at all is that a correction is a one-line diff here rather than
|
||||||
|
a rediscovery in six places.
|
||||||
|
|
||||||
|
It lives beside the code, not in `~/w7/notes/`, because it describes bytes on a wire and has to
|
||||||
|
move with the branch that defines them.
|
||||||
|
|
||||||
|
Base: `feat/disaggregated @ a29807cc`. Every `file:line` below was read at that commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0 The three rules that sit above every table
|
||||||
|
|
||||||
|
These are R-2 and R-11 in their formal wording. They apply to every row of table 1 without
|
||||||
|
restatement, and every reviewer's first three questions (BRIEF §11.1, §11.4) are these three.
|
||||||
|
|
||||||
|
**Rule A — a content record must declare its bytes.** Under
|
||||||
|
`MG_Config::Transport != Monolith`, a record whose payload owns an `MGPBlobRef` and which
|
||||||
|
carries content must set `Seg` to a real segment, `Offset` to a byte offset **within that
|
||||||
|
segment**, and `Size` to a **non-zero** byte count. `Blob.Size == 0` on such a record is
|
||||||
|
`Fatal{ProtocolCorruption}`.
|
||||||
|
|
||||||
|
This inverts today's legal state, and deliberately. `MGPipeTypes.h:398-410` says in so many
|
||||||
|
words that a zero `Blob.Size` means *"this record does not declare its blob"* and *"is not a
|
||||||
|
fault"* — which is right for monolith, where the bytes travel beside the record as a companion
|
||||||
|
pointer. Under split there is no beside.
|
||||||
|
|
||||||
|
**Rule B — no host pointer crosses.** Under split, `MGHostSpan::Ptr` is written `nullptr` by
|
||||||
|
the encoder and any non-null `Ptr` seen by the apply side is `Fatal{ProtocolCorruption}`.
|
||||||
|
Today `Ptr` is the fast path (`MGPipeHostSpan.h:51`), which is exactly why this needs saying.
|
||||||
|
P5's reduced path should produce **zero** host spans at all — see table 0's cap-bit row.
|
||||||
|
|
||||||
|
**Rule C — an applier entry point may not hold a pointer past its return.** A `SEG_STAGE` run
|
||||||
|
is valid from publish until `retiredSeq` passes the record that named it. The tree has exactly
|
||||||
|
one violation and it is named rather than tolerated: Espryt's `GLESBufferResource::hostBytes`
|
||||||
|
(`Managers.h:839`), written by `Ops_H_SubData` (`Managers.cpp:1980-1983`) and `Ops_H_FlushRange`
|
||||||
|
(`:2035`), read by six later drains (`:2000`, `:2062`, `:2080`, `:2111`, `:2741`, `:2843`).
|
||||||
|
Under split those two writes must **copy into server-owned storage**. `MOBILEGL_IPC_AUDIT=1`'s
|
||||||
|
`0xDD` fill over retired staging bytes (R-2.5) is the mechanical control that says whether they
|
||||||
|
did; without it, an `inproc` implementation that kept the pointer is indistinguishable from one
|
||||||
|
that copied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 Table 0 — the encoding table
|
||||||
|
|
||||||
|
One row per wire field that is **not a handle**. Handles are `{slot, gen}` and are settled by
|
||||||
|
P2/P3a; everything else that is not a plain scalar is here.
|
||||||
|
|
||||||
|
| field | the ruling | zero means | who reads it | evidence / note |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **segment id space** | `SEG_CMD=1, SEG_STAGE=2, SEG_REPLY=3, SEG_EVENT=4, SEG_SHADOW=5, SEG_ADOPT=6`. Identical values to `Protocol::SegmentKind`. | **"no segment", always.** 0 is never a real segment id. | encoder, decoder, `gMGPipeSegmentResolver` | `protocol.fbs:36-44`; `kMGHostSpanSegNone = 0` at `MGPipeHostSpan.h:21`. The two are tied by `static_assert` in `Wire/PipeWireCodec.cpp`, which is the only place the flatbuffers header and the codec enum meet. `kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF` (`:26`) stays reserved for P8. |
|
||||||
|
| **`MGPBlobRef{Offset, Size, Seg, Pad0}`** (24 B, `MGPipeTypes.h:55-61`) | `Seg` from the row above. `Offset` is a **byte offset inside that segment**, never a host address. `Size != 0` ⇔ "this record declares its blob", and under split a content record **must** declare it (rule A). | `Size == 0` = "no blob declared" — legal in monolith, `Fatal` under split for a content record. | decoder, every bounds cross-check | Today every emitter writes `{Seg=None, Offset=<host address>, Size=0 or real}`. Five of the eleven blob members do declare a real size today (`CsoCache.h:156`, `VertexInputEmit.h:398`, `ResourceTracker.h:216`, `PipeFill.cpp:2259`); the rest write 0. See table 1's "length" column for which. |
|
||||||
|
| **`MGHostSpan{Ptr, Seg, Pad0, Size, Offset}`** (32 B, `MGPipeHostSpan.h:28-37`) | Split: `Ptr == nullptr` always (rule B). **The 32-byte layout must not be reordered** (`:29-31`). | `Seg == 0` with `Size != 0` is `Fatal`. | decoder | **P5's reduced path must produce none at all.** See the cap-bit row below. |
|
||||||
|
| **`kCapNeedsHostIndexBytes` (1<<7), `kCapNeedsHostUboBytes` (1<<8)** | **Both are 0 for the whole of P5**, by ruling. | — | client emitters | `MGPipeTypes.h:120-123`. This is the cheapest way to keep every `MGHostSpan` out of the first IPC frame: the two bits are the only things that ask for one. `TriangleScenario` must therefore use a **VBO-backed draw and not client-array indices**, or `kDrawHasUserIndices` (`MGPipeTypes.h:1212`) produces a span the split filling for which is P8's. |
|
||||||
|
| **`MGPCaps`** (4 members, `MGPipeTypes.h:126-139`) | **One carrier, not two.** `MGPCaps` is the model; `protocol.fbs`'s `CapsSnapshot` is its transport form. `Dynamic` and `CallMask` cross as POD bytes; `FormatCapabilities` and `RendererInfo` cross as the two blobs whose serializers are P5's new work (`MG_Remote/CapsCodec.h`). | — | `CapsMirror` (client), `ServerSession` (server) | `MGPCaps` has only a **compositional** size assertion (`MGPipeTypes.h:145-146`) because `DynamicBackendParameters` still carries `SizeT` and `GLenum`; P0.5's fixed-width rewrite never happened. **P5 does not rewrite it** — see the ABI row. |
|
||||||
|
| **`CapsSnapshot` redundancy** | `tableSlotMask` (`protocol.fbs:94`) is **DELETED**, not renamed. `maxComputeWorkGroupCount` / `maxComputeWorkGroupSize` (`:92-93`) and `prefersCpuXfbPrimitiveAccounting` (`:95`) are **deleted** too: the first two ride inside `Dynamic` already (`BackendObject.h:392-393`), and the third is answered by `kCapCpuXfbPrimitiveAccounting`. | — | s1 (the schema), c1 (the mirror) | R-8 offered rename-or-delete for `tableSlotMask`; **delete**, for two reasons and the second is decisive. `ARCHITECTURE.md:114` says `CallMask` *replaces* "is this table slot null" as the capability probe, so a field whose comment is "which `GLFunctionsTable` slots the peer registered" re-introduces precisely what it replaced. And `GLFunctionsTable` has **69** function-pointer slots (`BackendObject.h:117-292`), so a `ulong` mask cannot address it and never could — it is five bits short on day one. |
|
||||||
|
| **`MGPCaps::CallMask` layout** | bits **0..8** = `MGPCapBit`, unchanged. bits **9..31** reserved. bits **32..47** = the **consumer mask**: bit `32+n` means "the server has a consumer for MGPipe subsystem bit `n`". bits **48..63** reserved. | a clear consumer bit = "this server does not consume that family; emit nothing for it". | `CapsMirror::ServerConsumes` — the **only** legal client-side source | c0's ruling, and the thing that makes R-8 implementable at all: R-8 says the client's liveness gates must read the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no per-family bit. Constants and the two fold/test helpers are in `MG_Remote/CapsCodec.h`; `CapsCodec.cpp` asserts the block does not collide with `MGPCapBit` and that P4a's `0x1fff` fits sixteen bits. |
|
||||||
|
| **ABI agreement** | `Hello`/`Welcome` assert both peers agree on `sizeof(DynamicBackendParameters)`, `sizeof(MGPCaps)`, `sizeof(GLFunctionsTable)` and `buildFingerprint`. A mismatch is `Fatal{AbiMismatch}` and **never** a downgrade. | — | s1 | The compositional assertion above means the caps block's literal size is ABI-dependent. P6's spawn is same-machine, same-binary and inherits this unchanged. A fixed-width rewrite of `DynamicBackendParameters` is **P7's** account, not P5's. |
|
||||||
|
| **`MGPSubData::Target`** | Packed: **low byte = `MGPipeResourceTarget`, high byte = the cube-face upload target**. Read only through `MGPipeSubDataResourceTargetOf` / `MGPipeSubDataUploadTargetOf`. Whole field `== 0` is the **buffer** half; a low byte naming `Buffer`, `Renderbuffer` or `>= MGPipeResourceTarget::Count` is `Fatal{ProtocolCorruption}`. | whole field 0 = buffer upload | applier | P4a ID-12. Already settled; copied here because a decoder that open-codes it is the class-1 defect. |
|
||||||
|
| **`MGPImageView::Access`** | The three-value encoding at `ImageEmit.h:146-159`, verbatim. | — | decoder, applier | **This encoding has only ever lived in a package header** (P4a R-3). This table is its first wire reader, so it is now contract. |
|
||||||
|
| **`MGPSamplerView::Target`** | The encoding at `SamplerEmit.h:900`, verbatim. | — | decoder, applier | Same status as the row above. |
|
||||||
|
| **`MGPFramebufferState::DrawBuffers[8]`** | The `-1` / default-token narrowing at `FramebufferEmit.h:146-163`, verbatim. A draw-buffer entry naming a colour attachment outside the record's own array is `Fatal{ProtocolCorruption}` (the applier already says so). | `-1` = "no attachment" | decoder, applier | Same status. |
|
||||||
|
| **`MGPReplySlot::Id`** | **= the record's sequence number** (R-3). No new id space, no allocator. The server writes the answer into `SEG_REPLY[seq % slots]` and **stamps `seq` back into the slot header** so a wrong-slot read is detectable rather than plausible. | seq is 1-based; `0` = "no record / not encoded" | client barrier wait | `ARCHITECTURE.md:124`: the wire carries no per-record seq field, so seq *is* the ordinal. `MGPReplySlot` exists (`MGPipeTypes.h:78-81`) and **no payload of the ten `kReplySlot` calls contains one** — which is exactly why the id must be derived rather than carried. P9 generalises this to "seq is the id's initial value", which extends the rule rather than overturning it. |
|
||||||
|
| **reply slot header** | `{Uint64 Seq; Int32 Status; Uint32 Size;}` — 16 bytes, then the payload. `Status`: **0 = OK, 1 = DECLINED, 2 = ERROR**. | — | client | **`DECLINED` is a real answer, not a failure.** It is how `MapPersistent` says `nullptr` (R-6) and how the four `Bool` acceptance entry points say `false` (R-5). A client that treats DECLINED as an error re-creates ID-39's 66 lost uploads from the other side. |
|
||||||
|
| **`kRecPad` and seq** | A wrap filler **does not advance seq**, on either side. | — | both | R-9. `RingConsumer::Pop` already skips fillers; the rule is stated because the *counter* is the caller's, not `Pop`'s. A side that counts pads drifts by one per wrap, for ever — and since seq is the reply-slot id, a drifted seq reads another call's answer instead of failing. Pinned by `RingTest.AWrapFillerDoesNotAdvanceTheRecordSequence`. |
|
||||||
|
| **per-opcode flags** | `kMGPipeCallFlags[MGPWireOp::kOpCount]` in `generated/PipeWire.inc`, read only through `MGPipeCallFlagsFor(op)`. Index 0 (`kInvalid`) is `kNone`. | `kNone` = no flags | every package | R-13.4. Before this table existed nothing generated exported the flags, so six packages were each about to hard-code `PipeCalls.def`'s fourth column — which is how `GetCaps` and `CreateSamplerState` came to own an `MGPBlobRef` with no `kHasBlob` on their line. `gen_pipe.py` now also refuses a flag token that is not an `MGPipeCallFlags` enumerator, with two negative controls in `--self-test`. |
|
||||||
|
| **`kHasBlob`'s meaning** | **Exactly "the payload owns an `MGPBlobRef` member"** — nothing weaker. | — | decoder | `PipeApply.h:78-79` already says so. Three calls carry bytes with **no** `MGPBlobRef`; they are table 1 rows 19–21 and are deliberately unflagged, because a decoder that trusts `kHasBlob` has to find a member to read. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 Table 1 — the byte carriers
|
||||||
|
|
||||||
|
**23 rows, not 19.** BRIEF §3 lists 19 and `scout-premortem:§3` lists a different 19; the
|
||||||
|
union is 23 and the four that only the premortem lists — `MapPersistent`, `ResourceReadback`,
|
||||||
|
`ReadPixels`, `GetTextureImage` — are precisely the ones whose bytes travel **server → client**.
|
||||||
|
Leaving them out of the byte-carrier table is how a phase discovers in week three that it never
|
||||||
|
decided where readback pixels land. They are rows 20–23 and are marked with their owning phase.
|
||||||
|
|
||||||
|
Columns: **flags** · **blob member** · **companion pointer today** · **which segment the bytes
|
||||||
|
live in** · **who owns that memory** · **when the slot retires** · **who declares the length,
|
||||||
|
who cross-checks it** · **reply name** (for `kReplySlot` rows).
|
||||||
|
|
||||||
|
`apply` = retires when `DecodeAndApply` returns. `submit` = when the server has handed the bytes
|
||||||
|
to the driver. `gpu` = `completedFrameSerial`.
|
||||||
|
|
||||||
|
### Group A — `kHasBlob`, the payload owns an `MGPBlobRef`
|
||||||
|
|
||||||
|
| # | call (op) | flags | blob member | companion today | segment | owner | retires | length declared / cross-checked |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | `CreateRenderState` (17) | `kHasBlob` | `Blob` (`MGPipeTypes.h:364`) | `const void* chunkBytes` (`PipeApply.h:740`), passed `CsoCache.h:157` | `SEG_STAGE` | client stages, server copies on apply | **apply** | declared real (`CsoCache.h:156`, `= kMGPipePipelineChunkBytes`); **nothing reads it today** — `MGPipeApplyCreateRenderState` (`PipeApply.cpp:1280`) never touches `Blob.Size`. Decoder must cross-check against `ChunkMask`. |
|
||||||
|
| 2 | `CreateVertexElements` (20) | `kHasBlob` | `Blob` (`:419`) | `const void* blobBytes` (`PipeApply.h:932`), passed `VertexInputEmit.h:399` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`VertexInputEmit.h:398`); **cross-checked, and this is the model for every other row**: `PipeApply.cpp:1990-1999` recomputes `AttributeCount*sizeof(MGPVertexAttribWire) + BindingPointCount*sizeof(MGPVertexBindingPointWire)` and refuses a disagreement; both counts bounded by `kMGPipeMaxVertexAttribs`. |
|
||||||
|
| 3 | `CreateShaderState` (27) | `kHasBlob` | **seven**: `Spirv[6]` (`:507`) + `Reflection` (`:508`) | **two typed frontend pointers** — `const LinkArtifacts*` + `const SpirvArtifacts*` (`PipeApply.h:1039-1041`), passed `ProgramEmit.h:257` | `SEG_STAGE`, seven independent runs | client stages, server copies | **apply** | all seven declare `Size = 0` today and `Reflection.Offset` is literally `(Uint64)&link` (`ProgramEmit.h:254`). **The serializer already exists and no package may write a second one**: `EncodeProgramArtifacts`/`DecodeProgramArtifacts` (`ProgramArtifactsCodec.{h:53,60,cpp:252,264}`), its own suite, and the verify build already round-trips **every real program it links** (`PinProgramArchiveRoundTrip`, `PipeApply.cpp:1056-1087`, called `:2638`). |
|
||||||
|
| 4 | `SetDynamicState` (30) | `kHasBlob` | `Blob` (`:389`) | `const void* chunkBytes` (`PipeApply.h:748`), passed `PipeFill.cpp:2260` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`PipeFill.cpp:2259`); **nothing reads it** (`PipeApply.cpp:1363-1370` scatters by `ChunkMask`). Same fix as row 1. |
|
||||||
|
| 5 | `SetGlobalConstants` (40) | `kHasBlob` | `Blob` (`:848`) | `const void* bytes` = `MapUBO()`'s image (`PipeApply.h:1052`), passed `ProgramEmit.h:195` | `SEG_STAGE` | client stages, server copies | **apply** | declares **0** (`ProgramEmit.h:194`); cross-check exists at `PipeApply.cpp:2784` against `Desc.GlobalUboSize` but is **inert while Size is 0**. Under rule A it becomes live. Per program per frame, unbounded length — the row most worth watching against R-10's max-record counter. |
|
||||||
|
| 6 | `SetResidualValueState` (46) | `kHasBlob` | `Blob` (`:932`) | **none, and no record either**: the entry point is `MGPipeApplySetResidualValueState(const ResidualValueBlock&)` (`PipeApply.h:760`), passed `PipeFill.cpp:2184`. **`MGPResidualValueState` is never instantiated on the live path.** | `SEG_STAGE` | client | **apply** | nothing declares it. The encoder must invent **both** the record fill and the blob fill. `sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE == 8` is statically asserted (`MGPipeTypes.h:924-927`) and only ever ratchets **down**. **This is the hardest row in the table** and neither scout flagged it; see §6. |
|
||||||
|
| 7 | `ResourceSubData` (48) | `kHasBlob\|kVarTail` | `Blob` (`:1009`) | `const void* bytes` + `const MGPSubRegion* regions` (`PipeApply.h:897-899`); buffer half `PipeFill.cpp:705`, texture half `TextureEmit.h:1278-1280` | `SEG_STAGE` | client stages, **server must copy** (rule C names this call) | **apply** | **the two halves disagree today**: buffer declares real (`ResourceTracker.h:216`), cross-checked at `PipeApply.cpp:702`; texture declares **0** (`TextureEmit.h:1265-1267`) on the grounds that the byte count is *"the server's to compute once it has picked box-or-rects"* — which cannot be a bounds check. **Under rule A the texture half must declare too.** Tail: `MGPSubRegion[RegionCount]` (`:1007`). |
|
||||||
|
| 8 | `BufferSubDataResident` (49) | `kHasBlob\|kOptional` | `Blob` (`:1009`, same payload) | `const void* bytes` = application staging, *"valid for the duration of the call only"* (`PipeApply.h:90`, `:901`); one caller, `PipeFill.cpp:727` | `SEG_STAGE` | client stages, server copies | **apply** | declared real via `MGPipeBuildSubDataRecord`, cross-checked at `PipeApply.cpp:702`. `kOptional` is a **capability** question under split, not a null-pointer question: the client must gate on `kCapResidentSubData` through the caps mirror, never on a table slot (R-8). |
|
||||||
|
|
||||||
|
### Group B — `kVarTail`, a tail and no blob member
|
||||||
|
|
||||||
|
Every row here declares its tail by a **count**, and `MGP_WIRE_CHECK_BOUNDS` **cannot see the
|
||||||
|
tail at all** — it only proves `size >= sizeof(MGPWireRec_X)`, so a record declaring
|
||||||
|
`Count = 4000` while carrying 8 bytes passes today. The decoder must recompute the total from
|
||||||
|
the declared count(s) and require it to **equal** `MGPWireRecHeader::Size`.
|
||||||
|
|
||||||
|
| # | call (op) | flags | tail element × count | companion today | segment | owner | retires | length |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 9 | `SetVertexBuffers` (32) | `kVarTail` | `MGPVertexBuffer` × `Count` (`:730`) | `const MGPVertexBuffer*` (`PipeApply.h:941`), `VertexInputEmit.h:245` | `SEG_STAGE` (tail follows the payload in `SEG_CMD` only if it fits the record bound) | emitter-owned `Vector`, **reused next emission** | **apply** | `Count`; no tail cross-check today |
|
||||||
|
| 10 | `SetSamplerViews` (35) | `kVarTail` | `MGPBoundView` × `Count` (`:778`) | `const MGPBoundView*` (`PipeApply.h:1028`), `SamplerEmit.h:786` | as above | as above | **apply** | `Count`; `Start+Count` past the unit bound is already `Fatal` (`PipeApply.h:1024-1026`) — a *slot* bound, not a byte-length check |
|
||||||
|
| 11 | `BindSamplerStates` (36) | `kVarTail` | `MGPipeHandle` × `Count` (`:785`) | `const MGPipeHandle*` (`PipeApply.h:1029`), `SamplerEmit.h:860` | as above | as above | **apply** | as above |
|
||||||
|
| 12 | `SetShaderImages` (37) | `kVarTail` | `MGPImageView` × `Count` (`:802`) | `const MGPImageView*` (`PipeApply.h:1030`), `ImageEmit.h:129` | as above | as above | **apply** | as above |
|
||||||
|
| 13 | `SetShaderBuffers` (38) | `kVarTail\|kHostSpan` | **two tails**: `MGPBufferRange` × `Count` (`:826`), then `MGHostSpan` × `HostSpanCount` (`:828`) | **none — no applier entry point exists.** P5 writes the first producer *and* the first consumer. | `SEG_STAGE` | — | **apply** | `HostSpanCount` is 0 **or** `Count`, never anything else (`MGPipeTypes.h:820-823`), so the two arrays stay index-aligned. `kCapNeedsHostUboBytes` is 0 for all of P5, so the second tail is **always absent** in this phase. |
|
||||||
|
| 14 | `SetStreamOutputTargets` (39) | `kVarTail` | **two tails**: `MGPBufferRange` × `Count`, then `Uint32` × `Count` (`:836-838`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | one `Count` sizes both tails; off the reduced path in P5 |
|
||||||
|
| 15 | `SetVertexAttribDefaults` (41) | `kVarTail` | `MGPAttribValue` × `Count` (`:864`) | `const MGPAttribValue*` (`PipeApply.h:756`), `PipeFill.cpp:2115` | as above | as above | **apply** | **two declarants that must agree**: `Count` and `popcount(Mask)` (`:863`, contract at `PipeApply.h:754-755`). A disagreement is a wire fault nothing checks today; the decoder must. |
|
||||||
|
| 16 | `DrawVbo` (59) | `kHostSpan\|kVarTail` | `MGPDrawRange` × `NumDraws` (`:1239`), then a **conditional** `MGHostSpan` when `Flags & kDrawHasUserIndices` (`:1212`, `:1231`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | `NumDraws`; the span carries its own `Size`. **The only `kHostSpan` on the hot path, and P5 must produce none of them** — `TriangleScenario` uses a VBO-backed draw precisely so this tail never appears. `MGPipeTypes.h:1221-1224` defers the fixed-head-versus-tail question to this phase: **P5 keeps it in the tail, unchanged**; there are no per-draw byte histograms yet to justify moving it, and moving it would be a wire-format change with no measurement behind it. |
|
||||||
|
|
||||||
|
### Group C — carries content with **no** `MGPBlobRef` and **no** `kHasBlob`
|
||||||
|
|
||||||
|
These three are the reason `kHasBlob` had to be given an exact meaning (table 0).
|
||||||
|
|
||||||
|
| # | call (op) | flags | ruling | evidence |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 17 | `CreateSamplerState` (23) | **now `kHasBlob`** (R-13.1) | `MGPSamplerDesc` **does** own an `MGPBlobRef Parameters` (`:429`) and the flag was simply missing. The blob is `memcpy(sizeof(SamplerParameters))` — a POD, and **`borderColorForm` must survive byte for byte** (`MGPipeTypes.h:425-428`), because all three colour representations are always numerically populated and it is the only thing that says which one the backend must use. | companion today is a **typed frontend pointer**, `const SamplerParameters*` (`PipeApply.h:1000`), passed `SamplerEmit.h:458`. Declares `Size = 0` (`SamplerEmit.h:433-435`); cross-check at `PipeApply.cpp:2353` is inert until rule A arms it. **Padding trap:** `SamplerEmit.h:437-445` — assignment leaves three trailing padding bytes stale, and the bytes staged must be the bytes a later `memcmp` compares. |
|
||||||
|
| 18 | `GetCaps` (1) | **now `kReplySlot\|kHasBlob`** (R-13.1) | `MGPCaps` owns **two** `MGPBlobRef`s, `FormatCapabilities` and `RendererInfo` (`:137-138`), and carried no `kHasBlob` at all. | `PipeCalls.def:80` before the fix. Serializers are P5's new work (`MG_Remote/CapsCodec.h`); the header itself defers them to this phase (`MGPipeTypes.h:134-136`). |
|
||||||
|
| 19 | `ResourceRespecify` (3) | stays `kNeedsAck`, **no `kHasBlob`** (R-13.3) | **`initialBytes` is always `nullptr` under split. Initial content arrives as `ResourceSubData` records immediately after this one.** `MGPResourceDesc` owns no `MGPBlobRef` and gains none. | The alternative was costed and rejected: `MGPBlobRef` is 24 bytes, `MGPResourceDesc`'s two pads are `Uint16 Pad0` (`:303`) + `Uint32 Pad1` (`:315`) = **6 bytes**, so a blob member takes the struct 88 → 112 and moves `MGP_ASSERT_POD(MGPResourceDesc, 88)` (`:320`). The chosen route reuses a path that is already chunked (`MGPipeForEachSubDataRecordRange`, `PipeFill.cpp:694-713`) and already acceptance-gated; it costs one extra record. `HasDefinedContent` (`:301`) is the field the encoder branches on, and it already exists. **The texture path already does exactly this** — `TextureEmit.h:1137` passes `nullptr` and relies on a following upload — so this generalises today's texture behaviour to buffers rather than inventing anything. |
|
||||||
|
| 19b | `ResourceRespecify`'s **second** uncarried companion | — | **`const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`, 4 bytes: `Uint16 UploadTarget; Uint16 Level;`) has no wire carrier either, and it is not bytes — R-13.3 does not cover it.** Ruling: it rides in `MGPResourceDesc`'s existing pads — `Pad1` (4 B, `:315`) becomes `{Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;}` and one byte of `Pad0` (`:303`) becomes `Uint8 HasRespecifiedLevel`. **Zero size change, `MGP_ASSERT_POD(..., 88)` does not move**, and `PipeFields.def`'s `MGP_FIELDS_MGPResourceDesc` gains the two named members (pads are excluded from field lists, so this is required, not optional). | Null means "this respecify redefines the **whole** resource" and drops every pending upload; non-null names the single `(uploadTarget, level)` and drops **only** that key. Clearing the whole set for a per-level `glTexImage2D` loses exactly the texels the server-side set exists to protect (`PipeApply.h:805-820`). Without a carrier, every OpenRA per-level respecify would silently take the whole-resource arm. **LANDED** (integrator ruling A made `MGPipeTypes.h` c0's file): `Uint8 HasRespecifiedLevel` in Pad0's high byte, `Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;` in Pad1, `MGP_ASSERT_POD(MGPResourceDesc, 88)` unmoved, plus an `offsetof` assertion that the pair stays adjacent and in `MGPRespecifiedLevel`'s order. **Read it only through `MGPipeRespecifyIsWholeResource` / `MGPipeRespecifiedUploadTargetOf` / `MGPipeRespecifiedLevelOf`, and write it only through `MGPipeSetRespecifiedLevel` / `MGPipeClearRespecifiedLevel`**: three fields are one value, and an open-coded reader that forgets the presence byte reads level 0 of upload target 0 as a real scope. **The carrier has no producer** — P5 builds only whole-resource descriptors, and a verify build pins that (`PinWholeResourceRespecifyScope`, `PipeApply.cpp`, the `PinNoLiveHostWrites` shape) so the phase that wires it cannot arrive unannounced. |
|
||||||
|
| 20 | `ResourceFlushRange` (51) | stays `kNone` (R-13.2) | **It carries no bytes at all under split.** It is a `{range, AccessFlags}` control record; the bytes of `[Offset, Offset+Size)` arrive **ahead of it** as `ResourceSubData` records covering exactly that range. | R-13.2 offered "add a blobref" or "write the convention down". Neither, and for a reason: the ladder this record drives rewrites its range *"from the authoritative shadow"* (`Managers.cpp:1047-1076`), and under split the authoritative shadow is **server-owned** by rule C — so `resource_subdata` is already the only way bytes reach it, and a blobref here would be a second, forgeable way to say the same thing. `AccessFlags` must still cross **verbatim**, not normalised (`PipeApply.h:902-903`). **Overturn condition:** if the tier-1 `INVALIDATE_RANGE` arm turns out to need the bytes and the range in the *same* record — i.e. an intervening record could stale the subdata — this needs its own blobref. It cannot happen while the verb barrier holds, because nothing interleaves; **revisit when the barrier retires for the buffer family.** |
|
||||||
|
|
||||||
|
### Group D — the four server → client rows the brief's list omitted
|
||||||
|
|
||||||
|
| # | call (op) | flags | ruling | reply name |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 21 | `MapPersistent` (5) | `kReplySlot\|kOptional` | **Returns `nullptr` under split, always** (R-6/R-2.4). Its `const void* seedBytes` companion (`PipeApply.h:917`) therefore never crosses in P5 and needs no carrier. The three frontend sites already tolerate a decline (`BufferObject.cpp:238`, `:603-606`, `:657-660`). Answer travels as `Status = DECLINED` with a zero-length payload. | `map_persistent.decline` |
|
||||||
|
| 22 | `ResourceReadback` (52) | `kReplySlot` | Bytes go **server → client** in `SEG_EVENT` via `OnBufferWriteback` (#3), not in the reply slot: the destination is the client's shadow and the size is the resource's, not a fixed slot's. The reply slot carries only completion. **The ordering rule is load-bearing:** the writeback is applied **before** the mutation epoch bumps, never after (`ARCHITECTURE.md:292-294`, `Managers.cpp:2120-2136`). | `resource_readback.done` |
|
||||||
|
| 23 | `ReadPixels` (58) / `GetTextureImage` (55) | `kReplySlot` | **`ReadPixels` blocks in P5** and its pixels come back in the reply slot, which is why `ReplyPool::SlotBytes()` is sized from the scenario's largest read rather than guessed. `MGPReadbackInfo` has `DstOffset`/`DstSize` but **no `Seg`** (`MGPipeTypes.h:1197-1206`): ruling — the destination is **always `SEG_REPLY`** in P5, so no `Seg` field is added; the PBO destination (fire-and-forget plus a client-side `MarkGpuWritten`) is b1's and also needs none, because a PBO destination is a resource handle rather than a segment. `GetTextureImage` is **not on P5's reduced path** and its slot stays `Fatal{UnmigratedVerb}`. | `read_pixels.pixels` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 Table 2 — `PipeInputs` field ownership
|
||||||
|
|
||||||
|
**This section is the SPEC and the four class definitions. The authoritative instance is
|
||||||
|
generated**: package p1 writes the generator that emits `generated/PipeFieldOwnership.inc` plus
|
||||||
|
a `--check`, in the shape of `gen_pipe_dirty_surface.py`. **A field in none of the four classes
|
||||||
|
is a build failure** (R-7.1) — that is the whole mechanism, and a hand-maintained table would
|
||||||
|
be wrong within a week.
|
||||||
|
|
||||||
|
The domain is **63 fields** (`kMGPipeInputFieldCount`, asserted `generated/PipeFilled.inc:96`)
|
||||||
|
**plus the 7 sticky forwards**, which are among those 63 but are exempted from the poison and
|
||||||
|
so need their own row. 70 rows, each in exactly one class.
|
||||||
|
|
||||||
|
### The four classes
|
||||||
|
|
||||||
|
**RECORD-SUPPLIED** — a pushed record supplies the **whole** field, so the server never needs
|
||||||
|
the client for it.
|
||||||
|
Membership is `kMGPipeFieldEmittedBy` (`generated/PipeFilled.inc:336-402`,
|
||||||
|
`kMGPipeEmittedFieldCount = 40`) **minus** the nine for which
|
||||||
|
`EmittedCallSuppliesTheWholeField` returns false (`MG_Impl/Pipe/PipeFill.cpp:1924-1939`;
|
||||||
|
reasons `:1875-1923`). **31 fields today.**
|
||||||
|
The nine excluded, with the generator's own reason: `GetPixelStoreParameters` ("only the PACK
|
||||||
|
half has a carrier"); `GetCurrentVertexAttribute` ("the applier cannot reproduce GLContext's
|
||||||
|
cross-view conversion"); `GetMaxTouchedTextureUnit` ("the set is hash-suppressed while the
|
||||||
|
high-water mark still moves"); and six sharing one reason — "the storage is a frontend heap
|
||||||
|
reference and the record carries an 8-byte `{slot, gen}`" — `GetBoundVertexArray`,
|
||||||
|
`GetFramebufferBindingSlot`, `GetImageTextureBinding`, `GetTextureUnitObject`,
|
||||||
|
`GetProgramForDraw`, `GetProgramForDispatch`.
|
||||||
|
|
||||||
|
**APPLIER-DERIVED** — the applier writes it from the records it already applies; no client
|
||||||
|
participation at all. Today: the render-state mirrors, `m_pixelStore[0]` (the **pack** half),
|
||||||
|
capability bits, the current vertex attribute, and the patch fields — written at
|
||||||
|
`PipeApply.cpp:177-184`, `:1338-1437`, plus everything `MGPipeDeriveRenderStateFields`
|
||||||
|
(`PipeApply.h:1084`, `PipeApply.cpp:2157`) derives.
|
||||||
|
|
||||||
|
**BARRIER-PULLED** — **P5's debt, and every row names the phase that retires it.** The server
|
||||||
|
answers by reading a value the client's residual fill put into the single shared `gPipeInputs`
|
||||||
|
while the verb barrier holds both threads apart. It is correct only because of that barrier,
|
||||||
|
which is why the barrier is load-bearing rather than cautious.
|
||||||
|
Each read increments `PipeStats::CallClass::ResidualPulls` (short name `rsp`, inside the
|
||||||
|
`#if MOBILEGL_PIPE_PUSH` block so G1 holds), published per frame. **`rsp`'s value at the end of
|
||||||
|
P5 is the size of the P6/P7/P8 debt** and goes into MEASUREMENTS.
|
||||||
|
`MOBILEGL_IPC_STRICT_ERRORS=1` promotes every read in this class to `Fatal`, and a named test
|
||||||
|
asserts the abort actually happens — an instrumentation that cannot go red is decoration.
|
||||||
|
|
||||||
|
**FATAL** — no carrier, and the reduced path never reads it, so a read is a real defect.
|
||||||
|
`Fatal{UnmigratedPipeInput, "<Field>@<verb>"}` (`generated/PipeFilled.inc:407-413`), live at
|
||||||
|
every log level on purpose (`PipeInputs.h:29-31`: *"this is not `MOBILEGL_ASSERT`, which is
|
||||||
|
inert in INFO builds"*).
|
||||||
|
|
||||||
|
### The known BARRIER-PULLED rows — the 21 the reduced path actually reads
|
||||||
|
|
||||||
|
Union of `kClear` (7 of its 18 own fields), `kDraw` (19 of 47) and `kReadback` (12 of 17).
|
||||||
|
OpenRA adds no field to this set — it widens the **site** set, not the field set, and is the
|
||||||
|
first thing to reach the read-attachment sites (`Managers.cpp:8603`, `:8966`) and the
|
||||||
|
`maxTouchedUnit >= 0` texture-unit walks.
|
||||||
|
|
||||||
|
| field | class | retires in | note |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `GetBoundVertexArray` | O | **P8** | `DirectGLES.cpp:4486`, `PrepareForDraw`, **unconditional on every draw**. `PipeFill.cpp:1902-1905` says the pull retires at P8, not here. |
|
||||||
|
| `GetProgramForDraw` | O | **P8** (Espryt), P7 (Magma) | `DirectGLES.cpp:4497`, same site, also unconditional. |
|
||||||
|
| `GetBufferBindingSlot` | O | P8 (indirect half), P9 (readback), P13 (transfer) | 18 Espryt sites; the 7 of 15 `BufferTarget`s no call covers (`Coverage.def:37-70`). |
|
||||||
|
| `GetBufferBindingPoint` | O | P3b/P4b + P7 | |
|
||||||
|
| `GetTouchedBufferBindingPointCount` | V | P3b/P4b | |
|
||||||
|
| `GetFramebufferBindingSlot` | O | P3b/P4b (Espryt), **P7** (Magma) | 8 Espryt sites through `GetFramebufferBindingSlotChecked`; `SyncCurrentFBO` (`:2995`) is self-declared monolith glue (`DirectGLES.cpp:2961-2965`) while `BindCurrentFBO` (`:4303-4353`) is already split-clean. |
|
||||||
|
| `GetTextureUnitObject` | O | P3b/P4b, P7 | 13 Espryt + 8 Magma sites. |
|
||||||
|
| `GetImageTextureBinding` | O | P3b/P4b, P7 | |
|
||||||
|
| `GetActiveTextureUnit` | V | P3b/P4b | server answers from its own state (`Coverage.def:215-219`). |
|
||||||
|
| `GetMaxTouchedTextureUnit` | V | P3b/P4b | hash-suppressed set, high-water mark still moves. |
|
||||||
|
| `GetTextureContextId` | V | P3b/P4b | **not a value to migrate**: the server answers from its own `Serial`. `Coverage.def:220-224` is explicit that no call carries these three and none should. |
|
||||||
|
| `GetTextureBindGeneration` | V | P3b/P4b | as above |
|
||||||
|
| `GetSamplingResolutionGeneration` | V | P3b/P4b | as above |
|
||||||
|
| `GetCurrentVertexAttribute` | V | P3b/P4b | the applier cannot reproduce GLContext's cross-view conversion. |
|
||||||
|
| `GetPixelStoreParameters` | V | **split in P5 — see below** | |
|
||||||
|
| `IsTransformFeedbackActive`, `IsTransformFeedbackPaused`, `GetTransformFeedbackProgram`, `GetTransformFeedbackGeneration`, `GetBoundTransformFeedbackLifetimeId`, `GetTransformFeedbackCapturedVertices` | V/O | P3b/P4b (Espryt XFB scatter), P7 (Magma) | read on `kDraw`; XFB itself is off the reduced path but these are read on the draw walk regardless. |
|
||||||
|
|
||||||
|
**`GetPixelStoreParameters` is split into pack and unpack in this phase** (R-7's one small
|
||||||
|
migration, and it is c0's ruling to keep rather than soften). The field is `m_pixelStore[2]` and
|
||||||
|
only `[0]` (pack) has a carrier — which the applier does write (`PipeApply.cpp:1373`) — so the
|
||||||
|
whole field reads as unmigrated while the half a readback needs is fine. Splitting it is what
|
||||||
|
stops the readback path from taking a whole-field `Fatal` for a half it never touches. The five
|
||||||
|
Espryt read sites whose `isUnpack` argument decides which half they want are
|
||||||
|
`DirectGLES.cpp:7924`, `:9399`, `:10893`, `:11272` and `Utils.cpp:2302`; **the scout named them
|
||||||
|
and did not open them**, so p1 reads the argument at each before it writes the two rows.
|
||||||
|
|
||||||
|
### The FATAL rows
|
||||||
|
|
||||||
|
Three non-sticky fields, each off the reduced path for a different, checkable reason:
|
||||||
|
|
||||||
|
| field | why it is FATAL rather than pulled |
|
||||||
|
|---|---|
|
||||||
|
| `GetBoundTransformFeedbackName` | **dead** — read by no backend since D21 (`PipeInputs.h:232-234`). |
|
||||||
|
| `GetTransformFeedbackPausedPrimitiveCounter` | reachable only from class `kQuery`, which the reduced path never enters. |
|
||||||
|
| `GetProgramForDispatch` | reachable only from `kDispatch`; there is no compute on the reduced path. Sites: `DirectGLES.cpp:5779`, `VulkanRenderer.cpp:7327`, `:7379`. |
|
||||||
|
|
||||||
|
Plus **`MGPipeUnmigratedEmulation`'s five call sites**, which in a split build stop being
|
||||||
|
`(void)name;` and become `Fatal`: `Managers.cpp:5334` ("texture-remint-pull"),
|
||||||
|
`DirectGLES.cpp:8051` ("generate-mipmap-storage"), `:8702` ("generate-mipmap-cpu-fallback"),
|
||||||
|
`:8997` ("copy-image-shadow-mirror"), `:10623` ("get-tex-image-shadow"). **One function grows
|
||||||
|
teeth and five sites get them** — `PipeApply.cpp:2820`, today a no-op, which
|
||||||
|
`PipeApply.h:1055-1066` and `PipeApply.cpp:2812-2817` both already say is waiting for this
|
||||||
|
phase. None of the five is on the reduced path. (ROADMAP's P4a row says six sites; there are
|
||||||
|
**five calls and one comment** — `Managers.cpp:5287` is the comment.)
|
||||||
|
|
||||||
|
### The seven sticky forwards
|
||||||
|
|
||||||
|
`GetBufferBindingPointCount`, `GetProgramObject`, `GetTextureObject`,
|
||||||
|
`HasOpenTransformFeedbackSpan`, `ValidateProgramName`, `InvalidateCompileEnv`, `RecordError`
|
||||||
|
(`PipeInputs.h:569-577`; identity asserted `:135-137`; argued `Coverage.def:138-150`).
|
||||||
|
|
||||||
|
P5's assignment:
|
||||||
|
|
||||||
|
- the first five → **BARRIER-PULLED** (counted in `rsp`, `Fatal` under strict);
|
||||||
|
- `InvalidateCompileEnv` → **`OnCapsInvalidated`**, i.e. the re-arriving caps snapshot (R-12);
|
||||||
|
- `RecordError` → **BARRIER-PULLED**, counted in `rsp`; its *ordering* is P9's (`OnGlError`).
|
||||||
|
|
||||||
|
**And their exemption is cancelled in a split build.** Today F-class accessors carry no
|
||||||
|
`MGP_INPUT_CHECK` at all (`PipeInputs.h:563-568`) and `MGPipeInputFieldIsFresh`
|
||||||
|
(`generated/PipeFilled.inc:418-426`) answers "fresh" for a sticky field regardless of
|
||||||
|
generation. That is exactly backwards for this phase: these seven are the ones that hand a
|
||||||
|
**frontend object or a frontend write** straight to the backend, so the exit gate "an
|
||||||
|
unmigrated field read is Fatal" is structurally blind on the seven most dangerous fields.
|
||||||
|
Under `MOBILEGL_BUILD_DISAGGREGATED` the exemption is lifted so they enter `rsp`, and under
|
||||||
|
`MOBILEGL_IPC_STRICT_ERRORS=1` they become `Fatal` like any other BARRIER-PULLED row.
|
||||||
|
|
||||||
|
### The prerequisite nobody else owns: someone must stamp
|
||||||
|
|
||||||
|
`MGPipeApplyAccess` **deliberately does not stamp** the poison generations
|
||||||
|
(`PipeInputs.h:612-618`): *"a stamp says the filler published this for THIS verb, which is the
|
||||||
|
walk's statement, not the applier's"*. Under split the filler is in the other role, so nothing
|
||||||
|
stamps, every `FilledGen[]` stays 0, `MGPipeInputFieldIsFresh` returns false for **everything**,
|
||||||
|
and a purely-server-side read aborts on the **first** field inside `SyncRenderState` —
|
||||||
|
`Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"}` — long before any interesting
|
||||||
|
case. **The server stamps at the verb boundary**: p1 defines what is stamped and for which
|
||||||
|
verb, v1 places the call (`Server/PipeApplier::StampVerbBoundary`). Neither half works alone,
|
||||||
|
and this is not in the ROADMAP row.
|
||||||
|
|
||||||
|
### Two sets with no field ids
|
||||||
|
|
||||||
|
**The conservative client GPU-write set.** One row per backend `MarkGpuWritten` site, mapped to
|
||||||
|
the client predicate that must fire, one unit case each:
|
||||||
|
|
||||||
|
| backend site | what it marks | when |
|
||||||
|
|---|---|---|
|
||||||
|
| `DirectGLES.cpp:570` | every SSBO binding point | draw/dispatch about to go out |
|
||||||
|
| `DirectGLES.cpp:618` | every bound atomic counter | every conformance case reads the increment back |
|
||||||
|
| `DirectGLES.cpp:2603` | buffer textures on image units, **only when `Access != GL_READ_ONLY`** | from draw preparation, deliberately not from `glBindImageTexture` |
|
||||||
|
| `UniformManager.cpp:1075` | storage texel buffer, `Access != GL_READ_ONLY` | after `EnsureGpuResidentStorage()` |
|
||||||
|
| `UniformManager.cpp:1231` | SSBO block, unconditional | after `EnsureGpuResidentStorage()` |
|
||||||
|
| `VulkanRenderer.cpp:11618` | the XFB capture targets | "the capture is a GPU write like any shader's" |
|
||||||
|
|
||||||
|
Plus **two new producers P5 adds**, both client-side with no server participation
|
||||||
|
(`ARCHITECTURE.md:508`): `glReadPixels` into a pack PBO becomes fire-and-forget plus a client
|
||||||
|
`MarkGpuWritten` (strictly better than monolith's unconditional stall), and
|
||||||
|
`glEndTransformFeedback` drops its unbounded fence wait and marks the capture targets instead.
|
||||||
|
|
||||||
|
`SyncGpuWrites` gains a **third state** it cannot express today — *emitted, answer not back* —
|
||||||
|
and under split it must **block until `OnBufferWriteback` lands** rather than clear the flag
|
||||||
|
optimistically (`BufferObject.cpp:372-374` clears unconditionally; `ARCHITECTURE.md:509` lists
|
||||||
|
this among the unavoidable blocking points, because monolith already `glFinish()`es here).
|
||||||
|
**No narrowing in P5**: `ResourceTracker.h:587-592`'s `rangeCount == 1` assertion **stays**.
|
||||||
|
|
||||||
|
**The persistent-map reachable set: the census is 21 sites, not 20.** `MEASUREMENTS.md:111`
|
||||||
|
records 20 and `Managers.cpp:5047-5048` speaks of "the eleven Espryt" sites; the actual count
|
||||||
|
is **9 Espryt + 12 Magma = 21**, and Espryt's own count is 12 (9 + 3 `SyncGpuWrites`), so both
|
||||||
|
published numbers are exactly one low and the missing one is an Espryt line. **Ruling: 21 is
|
||||||
|
the number, and `DirectGLES.cpp:361` (`ResolveIndirectCommandBytes`) is in the set.** It is a
|
||||||
|
shared helper rather than a draw-path site, which is the most likely reason it was excluded,
|
||||||
|
but a helper that reaches a persistently-mapped range is exactly as able to read stale bytes as
|
||||||
|
a draw site is; excluding it would be a shutter that cannot see its own subject. The
|
||||||
|
per-site attribution table `ARCHITECTURE.md:290` refers to as "§5.7" **does not exist in the
|
||||||
|
tree** — b1 should not go looking for it. The nine Espryt sites: `DirectGLES.cpp:361`, `:6185`,
|
||||||
|
`:6439`, `:6440`, `:6541`, `:6542`, `Managers.cpp:2817`, `:2994`, `MultiDraw.cpp:511`. The
|
||||||
|
twelve Magma: `DirectVulkan.cpp:281`, `:472`, `:796`, `UniformManager.cpp:2024`,
|
||||||
|
`VulkanRenderer.cpp:3542`, `:3621`, `:4013`, `:7428`, `:12423`, `:12424`,
|
||||||
|
`VkBufferManager.cpp:628`, `:679`. **Correcting `MEASUREMENTS.md:111` from 20 to 21 is b1's
|
||||||
|
line to write**, in the same commit that lands the tracker.
|
||||||
|
|
||||||
|
`m_livePersistentMaps` is defined by `SyncPersistentMappedRange`'s own early-out chain read as
|
||||||
|
a membership test (`BufferObject.cpp:346-349`): persistent, write, **not** flush-explicit,
|
||||||
|
**not** GPU-resident.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 Table 3 — role and thread ownership of every process global
|
||||||
|
|
||||||
|
`ARCHITECTURE.md:578-581` claims MGPipe brings the globals a role split must duplicate down
|
||||||
|
from four to two. **That census is at least seven short.** Every row below also answers what
|
||||||
|
make-current and teardown do to it — the column `ARCHITECTURE.md` does not have.
|
||||||
|
|
||||||
|
| global | where | who writes | P5 ruling | make-current / teardown |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `gPipeInputs` (~20 KB) | `PipeInputs.h:706` | client residual fill (`PipeFill.cpp:530`, `:2131`, `:2604`) + applier (`PipeApply.cpp:1336`, `:1364`, `:1373`, `:1377`, `:1436`, `:1512`) | **One instance is legal, but only under the verb barrier.** The barrier makes at most one of {GL thread, apply thread} runnable at a time, so there is exactly one writer at any instant. **No second writer may be introduced before the barrier retires.** The invariant is a runtime assertion in debug/verify builds, not only a sentence here: the apply thread raises a flag on entering the applier and the client checks it when it touches `gPipeInputs` outside a barrier (`ClientSession::InBarrierWait` / `ApplyThreadIsInsideApplier`). | make-current: unchanged. teardown: nothing — it is POD in the image. |
|
||||||
|
| `g_applier` | `PipeApply.cpp:396` | applier | **Server-exclusive.** Its own header already says "under split there is one per served context" (`PipeApply.h:684`). | It is `*new MGPipeApplierState{}` and never destroyed, deliberately (`PipeApply.cpp:392-395`): `resource_destroy` is raised from `~BufferObject`, which runs from exit handlers after this TU's globals are gone. |
|
||||||
|
| `g_resourceOps` | `PipeApply.cpp:402` | the backend, at register time | **Server-exclusive, and the client must NEVER read it** (R-8). Under `inproc` a client reading it is right *by accident*; under spawn it is null and the four P4a families plus P3a's buffers emit **nothing at all**, silently. The client asks `CapsMirror::ServerConsumes` instead. | registered around `DirectGLES.cpp:11933`, nulled from `OnBackendContextDestroyed` (`Managers.cpp:2584`) — so it moves on every context loss, which is another reason the client cannot key on it. |
|
||||||
|
| `gMGPipeSegmentResolver` | `MGPipeHostSpan.h:47` — a **plain non-atomic inline variable** | `MG_Remote` installs | **One process-wide slot, so it cannot be per-role.** Ruling: **the server role installs it and the client never resolves a span at all** — the client only ever *writes* `Ptr = nullptr`. `SegmentTable::InstallProcessResolver()` asserts if one is already installed, so two roles racing on it is loud rather than silent. Install **before** the apply thread starts. | teardown: uninstall after the join, never before — a record still in flight can still resolve. |
|
||||||
|
| the ten `MG_Impl/Pipe` `*Instance()` singletons | `fable-seam-audit.md:120-135` | client | **Client-exclusive.** One correction carried forward: the texture **drain list `m_drain` is process-wide**, not per-context as D-D4 claims; the audit already booked "one drain per client context" as a P5 item. | `FreshlyPrimed` (`PipeFill.cpp:2414-2440`) resets them on make-current; leak at exit (ID-8). |
|
||||||
|
| `ScopedDefaultUnpackState::s_synced` + **six** value shadows | `Managers.cpp:5490-5496` | backend | **Server-exclusive — the sixth global the four→two census missed.** Latent rather than live in P5 only because the client role never touches GL on the reduced path. (`Managers.cpp:5491-5496` is six `GLint`s, not five: `s_skipImages` at `:5496` is the one usually dropped.) | **Never reset on context death** — `OnBackendContextDestroyed` resets the rings and the binding caches and not this. Benign while a lost context returns the driver to GL defaults; not benign the day a server re-attaches to a context something else moved. Register it now. |
|
||||||
|
| `pActiveBackendObject` | `GlobalObjects.cpp:23` | `MG_Backend::Init()` | **Client installs `BackendObject_Remote`; the server's `BackendObject_DirectGLES` is held privately by `ServerLoop`.** No thread-keyed shim, and therefore `MOBILEGL_BUILD_DISAGGREGATED_INPROC` needs none — but the cost is **seven** backend-internal reads across **six** functions, not the one the scout reported: `BackendObject_DirectGLES.cpp:815`, `:819` (`ClampSamplesToBackendSupport`) and `Utils.cpp:74`, `:82`, `:126`, `:220`, `:260`. All seven are format-capability lookups, so "pass the format cache down" still works. **`DirectGLES.cpp:12446` is NOT `ClampSamplesToBackendSupport`** — it is `Present()`'s fence poll, and `DirectGLES.cpp` contains no `pActiveBackendObject` reference at all. | `GetFormatCapabilities()` is **non-virtual** (`BackendObject.h:594`), so the remote object must **fill** `m_formatCapabilities` rather than override the accessor. Teardown: `pActiveBackendObject.reset()` (`MobileGL/Init.cpp:68`) runs `~BackendObject_DirectGLES` → `DestroyEGLContext()`, so under split it must be a **blocking** request onto the apply thread. |
|
||||||
|
| `gBackendFunctionsTable` | `GlobalObjects.cpp:24`, assigned `Init.cpp:44` | `MG_Backend::Init()` | **Client = the emit table (R-4); the server holds its real table directly and never goes through this global.** | cleared at `MobileGL/Init.cpp:91`. |
|
||||||
|
|
||||||
|
**Teardown order**, `ARCHITECTURE.md:537` plus the sentence it omits:
|
||||||
|
|
||||||
|
1. client publishes and waits for the server to drain and acknowledge;
|
||||||
|
2. **`Doorbell::Kill()`** — *the only thing that can wake an apply thread parked on
|
||||||
|
`kWaitForever`* (`Doorbell.h:211-221`; the shape is already pinned by
|
||||||
|
`InProcessTransportTest.cpp:344`);
|
||||||
|
3. **join**, bounded (that test uses 5 s) so a regression is a red test and not a hung CI job;
|
||||||
|
4. only then may the client free anything an emitter owns — a tail still referenced by an
|
||||||
|
unapplied record is a use-after-free the join is what prevents;
|
||||||
|
5. then the existing order (`MobileGL/Init.cpp:38-98`).
|
||||||
|
|
||||||
|
**ID-8 applies once per role-local singleton, not once overall**: every new
|
||||||
|
`MG_Remote/Client/*` and `MG_Remote/Server/*` singleton leaks at exit. The proof recipe is
|
||||||
|
inherited: both lanes run `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`.
|
||||||
|
|
||||||
|
**Known open item, flagged not resolved.** `ARCHITECTURE.md:537`'s required order puts the
|
||||||
|
client's sync/query handle release **after** the transport closes, while today
|
||||||
|
`DestroyAllSyncObjects` / `DestroyAllQueryObjects` (`MobileGL/Init.cpp:62`, `:67`) deliberately
|
||||||
|
run **before** `pActiveBackendObject.reset()` (`:68`). The two are only reconcilable if a split
|
||||||
|
sync handle is client-minted and needs no backend call — which is P10's, not P5's. **P5 keeps
|
||||||
|
today's order** and v1 records which way it went.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 The knobs
|
||||||
|
|
||||||
|
Parsed in `ConfigLoader.cpp`, declared in `Config.h`. All of them live behind
|
||||||
|
`#if MOBILEGL_BUILD_DISAGGREGATED` — including the parser — because `MG_ConfigLoader::Init()`
|
||||||
|
is a pull-build symbol and G1 admits **no resize**, which is the same reason the
|
||||||
|
`MOBILEGL_PIPE_VERIFY` knobs sit behind their own `#if`.
|
||||||
|
|
||||||
|
| knob | default | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `MOBILEGL_TRANSPORT` | `monolith` | `monolith\|inproc\|spawn\|unix:<path>\|pipe:<name>`. The three P6 forms **parse and are then refused by name**, staying on monolith: a P6 lane that set `spawn`, fell back silently and went green on the wrong arm is the failure this wording avoids. |
|
||||||
|
| `MOBILEGL_IPC_SERVER_PATH` | `""` | P6 consumes it; P5 parses it because t1's ctest `ENVIRONMENT` blocks and `add_trace_replay_test`'s SPLIT variant already carry it, and an unparsed variable is indistinguishable from a parsed-and-ignored one. |
|
||||||
|
| `MOBILEGL_IPC_RING_MB` | 8 | SEG_CMD. **One record may be at most half of this** (`RingProducer::MaxRecordBytes`), so 8 MiB caps a record at 4 MiB. R-10 makes the codec publish a max-record-bytes counter rather than assume that is enough. |
|
||||||
|
| `MOBILEGL_IPC_STAGE_MB` | 32 | SEG_STAGE. Every blob and every var-tail's bytes. |
|
||||||
|
| `MOBILEGL_IPC_SPIN_US` | 50 | spin before parking, either direction. |
|
||||||
|
| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | 64 | **0 is the E3(a) negative control, not "unlimited"**: it turns the push off and `PersistentCoherentMapScenario` must go red. |
|
||||||
|
| `MOBILEGL_IPC_ADOPT_TIER` | 2 | 2 = emulate, the only tier P5 implements. 0 and 1 parse and are `Fatal` at use, naming P11. |
|
||||||
|
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 0 is R-1's negative control and is **expected** to be red. |
|
||||||
|
| `MOBILEGL_IPC_STRICT_ERRORS` | 0 | promotes BARRIER-PULLED reads — and, in a split build, the seven sticky forwards — to `Fatal`. |
|
||||||
|
| `MOBILEGL_IPC_AUDIT` | 0 | `0xDD` over retired staging bytes (rule C's mechanical control). |
|
||||||
|
| `MOBILEGL_IPC_SERVER_AFFINITY` | `auto` | kept as the raw string; whoever starts the apply thread logs the **resolved mask**, because an affinity that silently did nothing looks exactly like one that worked. |
|
||||||
|
|
||||||
|
**One consequence, stated so it is not rediscovered.** In a build *without*
|
||||||
|
`MOBILEGL_BUILD_DISAGGREGATED`, `MOBILEGL_TRANSPORT=inproc` is accepted by the environment and
|
||||||
|
**silently ignored** — the parser does not exist there, and putting a complaint in the
|
||||||
|
unconditional part of `ConfigLoader` would move a pull-build symbol and break G1. That is
|
||||||
|
precisely the shape of "the split lane ran monolith and went green", so the guard against it is
|
||||||
|
a **build-level** check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||||
|
MG_Remote` must be non-empty in `build-split`, and it is t1's CI job to assert that.
|
||||||
|
|
||||||
|
CMake gained `MOBILEGL_BUILD_DISAGGREGATED_INPROC` (implies `DISAGGREGATED`) and, new here,
|
||||||
|
**`MOBILEGL_BUILD_DISAGGREGATED` now implies `MOBILEGL_PIPE_PUSH`**: the split path decodes
|
||||||
|
into the MGPipe applier and `MOBILEGL_PIPE_PUSH` is what compiles the applier, so
|
||||||
|
`-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone used to configure cleanly and then fail to link — a
|
||||||
|
shape indistinguishable at the CMake level from a legitimate transport-only build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6 Rulings this file makes that the brief did not, and where the brief is wrong
|
||||||
|
|
||||||
|
Each entry says what would overturn it.
|
||||||
|
|
||||||
|
1. **`CallMask` bits 32..47 are the consumer mask.** R-8 says the client's liveness gates read
|
||||||
|
the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no
|
||||||
|
per-family bit, so R-8 was not implementable as written. Overturned by: a decision to carry
|
||||||
|
a second mask field in `CapsSnapshot` instead — which costs a schema field and gains
|
||||||
|
nothing, since 16 bits is enough through P8.
|
||||||
|
|
||||||
|
2. **`tableSlotMask` is deleted, not renamed** (R-8 allowed either). Decisive evidence:
|
||||||
|
`GLFunctionsTable` has **69** slots and `ulong` is 64 bits, so the field cannot address the
|
||||||
|
table its own comment names. Overturned by: widening the schema field *and* a reason to
|
||||||
|
keep an explicit slot probe after `ARCHITECTURE.md:114` retired the concept.
|
||||||
|
|
||||||
|
3. **`ResourceFlushRange` carries no bytes at all** (R-13.2 offered "add a blobref" or "write
|
||||||
|
the convention down"; this is a third answer, and a stronger one). The ladder it drives
|
||||||
|
rewrites from the authoritative shadow, which under rule C is server-owned, so
|
||||||
|
`resource_subdata` is already the only path bytes take. Overturned by: evidence that the
|
||||||
|
tier-1 `INVALIDATE_RANGE` arm needs bytes and range in one record. Cannot arise while the
|
||||||
|
verb barrier holds; **revisit when the barrier retires for the buffer family.**
|
||||||
|
|
||||||
|
4. **`ResourceRespecify` also has a SECOND uncarried companion, and the brief does not mention
|
||||||
|
it.** `const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`) is the *scope* of the
|
||||||
|
redefinition and `MGPResourceDesc` cannot express it. Without a carrier every per-level
|
||||||
|
`glTexImage2D` in OpenRA silently takes the whole-resource arm and drops every pending
|
||||||
|
upload — the exact texel loss the server-side set exists to prevent. Ruling: two named
|
||||||
|
fields in the existing pads, zero size change. **c0 rules and specifies; the integrator
|
||||||
|
lands the `MGPipeTypes.h` + `PipeFields.def` edit before w1 encodes this record.**
|
||||||
|
|
||||||
|
5. **Table 1 is 23 rows, not 19.** The brief's 19 and `scout-premortem:§3`'s 19 are different
|
||||||
|
lists; the four only the premortem carries are the server → client ones, and a phase that
|
||||||
|
omits them discovers in week three that it never decided where readback pixels land.
|
||||||
|
|
||||||
|
6. **`SetResidualValueState` is a fourth typed companion, and neither scout nor the brief names
|
||||||
|
it.** `MGPipeApplySetResidualValueState` takes `const ResidualValueBlock&` — not a payload,
|
||||||
|
not a `const void*` — and `MGPResidualValueState` is **never instantiated on the live path**.
|
||||||
|
The encoder has to invent both the record fill and the blob fill. Budget it as w1's hardest
|
||||||
|
row, not as one of the easy `kHasBlob` eight.
|
||||||
|
|
||||||
|
7. **Three of the brief's 19 have no applier entry point at all** — `SetShaderBuffers` (38),
|
||||||
|
`SetStreamOutputTargets` (39), `DrawVbo` (59). `scout-premortem:§3` cites
|
||||||
|
`PipeApply.h:756, 941, 1028-1030` for a six-call row; those five citations cover five *other*
|
||||||
|
calls. For these three, "what crosses today" is **nothing**, and P5 writes the first producer
|
||||||
|
*and* the first consumer.
|
||||||
|
|
||||||
|
8. **`CreateRenderState` and `SetDynamicState` declare a `Blob.Size` that nothing ever reads.**
|
||||||
|
`scout-wire-codec:§4.2`'s "`Size = 0`" column is stale for four rows (add
|
||||||
|
`CreateVertexElements` and `ResourceSubData`'s buffer half). The applier's only four
|
||||||
|
`Blob.Size` reads are `PipeApply.cpp:702`, `:1998`, `:2353`, `:2784`. A fifth
|
||||||
|
flags-vs-payload-vs-signature disagreement for the reviewer's list.
|
||||||
|
|
||||||
|
9. **The emit table is 71 function pointers, not 69.** R-4 says 69 slots; that is
|
||||||
|
`GLFunctionsTable`'s count. The table the client actually installs is
|
||||||
|
`GlobalBackendFunctionsTable` = 69 + `Present` + `SetSwapInterval`. R-4's rule (no null slot,
|
||||||
|
no pass-through) applies to all 71, and `Present` is on the reduced path. The Bool member is
|
||||||
|
not a verb and is answered from `kCapCpuXfbPrimitiveAccounting`.
|
||||||
|
|
||||||
|
10. **`prefersCpuXfbPrimitiveAccounting` is a member of `GLFunctionsTable`
|
||||||
|
(`BackendObject.h:274`), not of `DynamicBackendParameters`.** So it does **not** ride inside
|
||||||
|
`MGPCaps::Dynamic`, and R-8's "same redundancy as (6)" is the wrong frame — it has three
|
||||||
|
spellings and no carrier in `MGPCaps` except the cap bit. Its one non-test client reader is
|
||||||
|
`GL_Query.cpp:221`.
|
||||||
|
|
||||||
|
11. **The persistent-map census is 21 sites, and `MEASUREMENTS.md:111`'s 20 is wrong.** Ruling
|
||||||
|
and the missing site named above. **`ARCHITECTURE.md`'s cited "§5.7" attribution table does
|
||||||
|
not exist in the tree.**
|
||||||
|
|
||||||
|
12. **`ScopedDefaultUnpackState` has six value shadows, not five** (`Managers.cpp:5491-5496`);
|
||||||
|
both the brief and the scout say five.
|
||||||
|
|
||||||
|
13. **`CanTouchGLNow()` guards 16 call sites, not 19.** 19 is the raw grep: 1 definition
|
||||||
|
(`Managers.cpp:928`) + 2 comment mentions (`:1967`, `:1979`) + 16 calls. The brief's "19
|
||||||
|
sites" over-counts. `IsBackendContextCurrentOnThisThread`'s 16 is right.
|
||||||
|
|
||||||
|
14. **`MGPipeApply*` is 37 entry points and 41 call sites**, not "~45 entry points" — and
|
||||||
|
`DirectGLES.cpp:12446` is `Present()`'s fence poll, **not** `ClampSamplesToBackendSupport`
|
||||||
|
(which is `BackendObject_DirectGLES.cpp:807-828`). Table 3's `pActiveBackendObject` row is
|
||||||
|
still correct but the diff is six functions, not one line.
|
||||||
|
|
||||||
|
15. **The 18 `build-split` unit aborts are not a poison problem.** They are
|
||||||
|
`Fatal{ProtocolCorruption}` trip wires the tests *expect*; seven test TUs test
|
||||||
|
`MOBILEGL_PIPE_POISON` without including the only header that defines it, so the macro reads
|
||||||
|
as 0 and they compile the "logs and carries on" arm while `PipeApply.cpp` compiles the
|
||||||
|
aborting one. Invisible in a push build (where it really is 0) and in a verify build (where
|
||||||
|
`-DMOBILEGL_PIPE_VERIFY=1` is on the command line); `MOBILEGL_BUILD_DISAGGREGATED` is the one
|
||||||
|
arming condition behind the header. Fixed in c0's own commit, test-local, no p1 surface.
|
||||||
|
|
||||||
|
16. **`MGHostSpan`, not `MGPHostSpan`.** The header is `MGPipeHostSpan.h`; the struct is
|
||||||
|
`MGHostSpan` (`:28`). `MGPHostSpan` does not exist.
|
||||||
|
|
||||||
|
17. **`ARCHITECTURE.md`'s own corrections, carried here so they are not lost**: `:83` says 61
|
||||||
|
`PipeInputs` fields, it is 63; `:492` cites `PipeStats.h:126` for
|
||||||
|
`MapPersistentRoundtrips`, it is `:141`; ROADMAP's P4a row says six
|
||||||
|
`MGPipeUnmigratedEmulation` sites, it is five calls plus one comment; `ARCHITECTURE.md:19`
|
||||||
|
says eight EGL lifecycle virtuals, there are nine (`ResizeEGLWindowSurface` is the
|
||||||
|
uncounted one).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7 R-15 — getter-shaped slots are answered locally, and the emit table's three classes
|
||||||
|
|
||||||
|
**R-15 (integrator ruling, made after the verb census).** A `GLFunctionsTable` slot whose answer
|
||||||
|
is a **static property of the server's device** is answered on the client **from the caps
|
||||||
|
mirror**. It is never emitted and never `Fatal`. The gate already exists and already runs on
|
||||||
|
every lane: `AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer`
|
||||||
|
(`MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp:580-623`) pins that the caps copy and
|
||||||
|
`glGetIntegeri_v` give one number.
|
||||||
|
|
||||||
|
This settles the census's sharpest finding: `GetIntegeri_v` is reached by the **first
|
||||||
|
`glCompileShader` of every context** (`CompileEnv.cpp:134-138` ← `Core.cpp:39`), not by any verb,
|
||||||
|
so an all-`Fatal` table would abort every scenario before it drew anything — and an emitter for
|
||||||
|
it would be a round trip for six constants the snapshot already carries.
|
||||||
|
|
||||||
|
### The three classes of the 71 slots. c1 does not re-derive this.
|
||||||
|
|
||||||
|
**Class A — answered locally from the caps mirror (2 slots). No record, ever.**
|
||||||
|
|
||||||
|
| slot | answered from |
|
||||||
|
|---|---|
|
||||||
|
| `GetIntegeri_v` (`BackendObject.h:205`) | `MGPCaps::Dynamic.MaxComputeWorkGroupCount` / `MaxComputeWorkGroupSize` (`BackendObject.h:392-393`) — the only indexed pnames the device owns. Every other indexed pname is frontend state and is answered before any table is consulted. |
|
||||||
|
| `IsTimerQuerySupported` (`:245`) | `kCapTimerQuery` (`MGPipeTypes.h:114`). A capability predicate, not a call: today a null slot means `COUNTER_BITS = 0` (`GL_Query.cpp:792`). |
|
||||||
|
|
||||||
|
`GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting` (`:274`) is in the same class by the same
|
||||||
|
argument — `kCapCpuXfbPrimitiveAccounting` — and is not a slot.
|
||||||
|
|
||||||
|
**Class B — emitted in P5 (5 slots).** The verb census's answer, and nothing else:
|
||||||
|
`Clear`, `DrawArrays`, `ReadPixels`, `BlitFramebuffer`, `Present`.
|
||||||
|
`Present` is in this class despite having **zero `MG_Impl` call sites** — it is reached through
|
||||||
|
`EGLImpl.cpp:178` → `BackendObject.cpp:396`, so c1 cannot find it by mirroring GLImpl.
|
||||||
|
|
||||||
|
**Class C — `Fatal{UnmigratedVerb, "<slot>"}` (64 slots).** Everything else, including
|
||||||
|
`SetSwapInterval`, `GetGpuTimestampNs` (a live GPU timestamp, not a static property, so **not**
|
||||||
|
class A), and the whole sync / query / transform-feedback / compute / copy / mipmap surface.
|
||||||
|
|
||||||
|
### The cross-cutting rule R-4 would otherwise break
|
||||||
|
|
||||||
|
**Forty-one of the 69 slots are null-checked at their call site, and several of those null checks
|
||||||
|
are CAPABILITY PROBES rather than safety checks.** R-4 forbids a null slot — so in the emit table
|
||||||
|
every one of those probes answers "supported" and the fallback behind it silently disappears.
|
||||||
|
That is not a theoretical risk: it is how a split lane produces a plausible picture for the wrong
|
||||||
|
reason. Three named cases; the rule generalises to all 41.
|
||||||
|
|
||||||
|
| probe site | what it decides today | reads instead |
|
||||||
|
|---|---|---|
|
||||||
|
| `GL_Query.cpp:481`, `:785` — `BeginOcclusionQuery != nullptr` | whether the target is rejected outright | `kCapOcclusionQuery` |
|
||||||
|
| `GL_Query.cpp:534` — the `BeginXfbPrimitivesQuery` ternary | GPU query vs CPU primitive accounting | `kCapXfbPrimitivesQuery` |
|
||||||
|
| the `SubDataResident` op-table slot | whether the resident-upload path exists at all | `kCapResidentSubData` |
|
||||||
|
|
||||||
|
**A null check on a slot may never survive into the client under split.** It becomes a caps-mirror
|
||||||
|
read — class A's mechanism — whatever class the slot itself is in. That is exactly
|
||||||
|
`ARCHITECTURE.md:114`'s "`CallMask` replaces 'is this table slot null' as the implicit capability
|
||||||
|
probe", now with a concrete list of what has to move.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8 Ownership amendments
|
||||||
|
|
||||||
|
- **`MobileGL/MG_Pipe/MGPipeTypes.h` is c0's file** (integrator ruling A; the BRIEF §5 ownership
|
||||||
|
table is amended). It was unowned, which is how the respecify-scope gap in table 1 row 19b had
|
||||||
|
no one to close it. A package that needs a payload struct shape changed goes through the
|
||||||
|
integrator, as with the three `.def` files.
|
||||||
|
- Consequently the row-19b carrier is **landed, not merely specified** — see §2 table 1 row 19b
|
||||||
|
and `MGPipeTypes.h`'s `HasRespecifiedLevel` / `RespecifiedUploadTarget` / `RespecifiedLevel`
|
||||||
|
and the five `MGPipeRespecify*` helpers beside them.
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/CapsCodec.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "CapsCodec.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote {
|
||||||
|
|
||||||
|
// The consumer mask may not collide with the MGPCapBits below it. kCapNeedsHostUboBytes
|
||||||
|
// is 1<<8 today; this asserts the gap stays a gap rather than trusting the comment.
|
||||||
|
static_assert((static_cast<Uint64>(MG_Pipe::kCapNeedsHostUboBytes) & kMGCapsConsumerMask) == 0,
|
||||||
|
"an MGPCapBit has grown into CallMask's consumer block (bits 32..47)");
|
||||||
|
static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
|
||||||
|
MG_Pipe::kMGPipeSubsystemResources),
|
||||||
|
"the consumer encoding does not round-trip");
|
||||||
|
static_assert(!MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
|
||||||
|
MG_Pipe::kMGPipeSubsystemPrograms),
|
||||||
|
"the consumer encoding answers yes for a family it was not given");
|
||||||
|
// P4a's highest allocated subsystem bit must fit the sixteen-bit block. This is the
|
||||||
|
// assertion that turns "room to P8" from a comment into a build break.
|
||||||
|
static_assert(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a <= 0xFFFFull,
|
||||||
|
"the subsystem mask no longer fits CallMask's sixteen consumer bits");
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedCapsCodec, \"%s\"} - P5 package w1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache&, Vector<Uint8>&) {
|
||||||
|
MGP5_C0_STUB("EncodeFormatCapabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool DecodeFormatCapabilities(const void*, Uint64, MG_Backend::FormatCapabilityCache&) {
|
||||||
|
MGP5_C0_STUB("DecodeFormatCapabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool EncodeRendererInfo(const RendererInfo&, Vector<Uint8>&) { MGP5_C0_STUB("EncodeRendererInfo"); }
|
||||||
|
|
||||||
|
Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); }
|
||||||
|
|
||||||
|
Uint64 CapsAbiFingerprint() { MGP5_C0_STUB("CapsAbiFingerprint"); }
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/CapsCodec.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The MGPCaps snapshot's serializers, and the CallMask encoding. Owner: package w1 (the two
|
||||||
|
// blob codecs) and s1 (the handshake asserts). Signatures by c0.
|
||||||
|
//
|
||||||
|
// MGPCaps (MG_Pipe/MGPipeTypes.h:126-139) has four members. Two are flat and cross by memcpy
|
||||||
|
// (DynamicBackendParameters Dynamic, Uint64 CallMask); two are MGPBlobRefs over containers
|
||||||
|
// and HAVE NO SERIALIZER IN THE TREE - the header says so itself at MGPipeTypes.h:134-136,
|
||||||
|
// "Their serializers land with the transport (P5)". They are:
|
||||||
|
//
|
||||||
|
// FormatCapabilities -> FormatCapabilityCache (MG_Backend/BackendObject.h:93-98):
|
||||||
|
// FullCaps + CaveatCaps (bitfield tables) + SampleCounts, a Vector<Int> per
|
||||||
|
// (target, format) pair. The Vector is why this cannot be a memcpy.
|
||||||
|
// RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an
|
||||||
|
// Optional<String>, and a Vector<GLExtension> inside GLInfo.
|
||||||
|
//
|
||||||
|
// THE ABI RULING (table 0). MGPCaps has only a COMPOSITIONAL size assertion
|
||||||
|
// (MGPipeTypes.h:145-146) because DynamicBackendParameters still carries SizeT and GLenum
|
||||||
|
// members - P0.5's fixed-width rewrite did not happen. P5 does NOT rewrite it. Instead the
|
||||||
|
// handshake asserts that both peers agree on sizeof(DynamicBackendParameters),
|
||||||
|
// sizeof(MGPCaps) and the build fingerprint, and Fatal{AbiMismatch} otherwise. P6's spawn is
|
||||||
|
// same-machine and same-binary, so it inherits this unchanged; the fixed-width rewrite is on
|
||||||
|
// P7's account.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote {
|
||||||
|
|
||||||
|
// ---- CallMask's layout (c0's ruling, extending R-8) ---------------------------------
|
||||||
|
//
|
||||||
|
// R-8 requires the client's liveness gates - MGPipeResourceSubsystemEnabled() and
|
||||||
|
// P4aFamilyHasItsConsumer() - to answer from MGPCaps::CallMask instead of from
|
||||||
|
// MGPipeGetResourceOps(), because that op table is the SERVER's registration: under
|
||||||
|
// inproc a client reading it is right by accident, and under spawn it is null and five
|
||||||
|
// whole record families silently emit nothing.
|
||||||
|
//
|
||||||
|
// But CallMask as declared carries only the nine MGPCapBit FEATURE bits
|
||||||
|
// (MGPipeTypes.h:108-124) and has no per-family bit at all, so "read the CallMask" was
|
||||||
|
// not yet an implementable instruction. It is now:
|
||||||
|
//
|
||||||
|
// bits 0..8 MGPCapBit, unchanged. kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes
|
||||||
|
// are BOTH ZERO in P5 by ruling (table 0), which is what keeps every
|
||||||
|
// MGHostSpan out of the first IPC frame.
|
||||||
|
// bits 9..31 reserved for further MGPCapBits.
|
||||||
|
// bits 32..47 THE CONSUMER MASK: bit (32 + n) means "the server has a consumer for
|
||||||
|
// MGPipe subsystem bit n" - i.e. the server's own subsystem mask, shifted.
|
||||||
|
// Sixteen bits covers bits 0..12 allocated through P4a with room to P8.
|
||||||
|
// bits 48..63 reserved.
|
||||||
|
//
|
||||||
|
// protocol.fbs:94's `tableSlotMask: ulong` is DELETED rather than renamed (R-8 offered
|
||||||
|
// either). Two reasons, and the second is decisive: ARCHITECTURE.md:114 says CallMask
|
||||||
|
// REPLACES "is this table slot null" as the capability probe, so a field whose comment is
|
||||||
|
// "which GLFunctionsTable slots the peer registered" re-introduces exactly what it
|
||||||
|
// replaced; and GLFunctionsTable has SIXTY-NINE function-pointer slots
|
||||||
|
// (BackendObject.h:117-292), so a 64-bit mask cannot address it and never could.
|
||||||
|
inline constexpr Uint32 kMGCapsConsumerBitShift = 32;
|
||||||
|
inline constexpr Uint64 kMGCapsConsumerMask = 0xFFFFull << kMGCapsConsumerBitShift;
|
||||||
|
|
||||||
|
// Server side: fold the subsystems this server actually consumes into a CallMask.
|
||||||
|
inline constexpr Uint64 MGCapsConsumerBits(Uint64 subsystemMask) {
|
||||||
|
return (subsystemMask & 0xFFFFull) << kMGCapsConsumerBitShift;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client side: the ONE legal spelling of "does the server consume this family".
|
||||||
|
// `subsystemBit` is a kMGPipeSubsystem* constant (MG_Pipe/MGPipe.h), not an index.
|
||||||
|
inline constexpr Bool MGCapsServerConsumes(Uint64 callMask, Uint64 subsystemBit) {
|
||||||
|
return (callMask & MGCapsConsumerBits(subsystemBit)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the two blob codecs ------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Byte-stable within one build; the handshake's fingerprint is what makes that enough.
|
||||||
|
// Both decoders must tolerate a truncated or over-long buffer by returning false, never
|
||||||
|
// by reading past `size`: these bytes arrive over the wire.
|
||||||
|
Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache& cache, Vector<Uint8>& out);
|
||||||
|
Bool DecodeFormatCapabilities(const void* bytes, Uint64 size, MG_Backend::FormatCapabilityCache& out);
|
||||||
|
|
||||||
|
Bool EncodeRendererInfo(const RendererInfo& info, Vector<Uint8>& out);
|
||||||
|
Bool DecodeRendererInfo(const void* bytes, Uint64 size, RendererInfo& out);
|
||||||
|
|
||||||
|
// ---- the ABI assertion the handshake carries ----------------------------------------
|
||||||
|
//
|
||||||
|
// Mixes sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable) and
|
||||||
|
// the compile-time build fingerprint. Compared in Hello/Welcome; a mismatch is
|
||||||
|
// Fatal{AbiMismatch} and never a downgrade, because every alternative silently reads one
|
||||||
|
// struct as another.
|
||||||
|
Uint64 CapsAbiFingerprint();
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for package c1. Every body is MGLOG_F + std::abort and never a silent no-op: a
|
||||||
|
// caps accessor that answers a default is how a split lane runs on the wrong device's limits.
|
||||||
|
|
||||||
|
#include "CapsMirror.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedCapsMirror, \"%s\"} - P5 package c1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
void CapsMirror::Adopt(const MG_Pipe::MGPCaps&, const MG_Backend::FormatCapabilityCache&,
|
||||||
|
const RendererInfo&, const String&, BackendType) {
|
||||||
|
MGP5_C0_STUB("CapsMirror::Adopt");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not stubs: the two the placeholder contract above promises are readable before the first
|
||||||
|
// snapshot. Everything else aborts, so nothing can accidentally answer from a zeroed mirror.
|
||||||
|
Bool CapsMirror::Valid() const { return m_generation != 0; }
|
||||||
|
Uint64 CapsMirror::Generation() const { return m_generation; }
|
||||||
|
|
||||||
|
const RendererInfo& CapsMirror::Renderer() const { MGP5_C0_STUB("CapsMirror::Renderer"); }
|
||||||
|
const MG_Backend::DynamicBackendParameters& CapsMirror::Dynamic() const {
|
||||||
|
MGP5_C0_STUB("CapsMirror::Dynamic");
|
||||||
|
}
|
||||||
|
const MG_Backend::FormatCapabilityCache& CapsMirror::Formats() const {
|
||||||
|
MGP5_C0_STUB("CapsMirror::Formats");
|
||||||
|
}
|
||||||
|
const String& CapsMirror::ApiVersion() const { MGP5_C0_STUB("CapsMirror::ApiVersion"); }
|
||||||
|
BackendType CapsMirror::Backend() const { MGP5_C0_STUB("CapsMirror::Backend"); }
|
||||||
|
Uint64 CapsMirror::CallMask() const { MGP5_C0_STUB("CapsMirror::CallMask"); }
|
||||||
|
Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit) const { MGP5_C0_STUB("CapsMirror::HasCap"); }
|
||||||
|
Bool CapsMirror::ServerConsumes(Uint64) const { MGP5_C0_STUB("CapsMirror::ServerConsumes"); }
|
||||||
|
Bool CapsMirror::PrefersCpuXfbPrimitiveAccounting() const {
|
||||||
|
MGP5_C0_STUB("CapsMirror::PrefersCpuXfbPrimitiveAccounting");
|
||||||
|
}
|
||||||
|
|
||||||
|
CapsMirror& CapsMirrorInstance() {
|
||||||
|
// ID-8: leak at exit. No frontend destructor may reach pipe or backend state from an
|
||||||
|
// exit handler, and that rule applies once per role-local singleton, not once overall.
|
||||||
|
static CapsMirror& instance = *new CapsMirror{};
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The client's copy of the server's capabilities. Owner: package c1. Signatures by c0.
|
||||||
|
//
|
||||||
|
// WHY A MIRROR AND NOT A ROUND TRIP. There are 56 client-side caps read points
|
||||||
|
// (40 GetDynamicParameters + 7 GetRendererInfo + 4 GetFormatCapabilities + 3 GetBackendType +
|
||||||
|
// 2 GetBackendAPIVersionString), and several of them - GL_Getter.cpp:2400 and
|
||||||
|
// ShaderTranspiler/CompileEnv.cpp:120-124 - bind a reference and then read many members, so a
|
||||||
|
// partial snapshot is not an option. Every one of the 56 must be answerable locally, with no
|
||||||
|
// record on the wire.
|
||||||
|
//
|
||||||
|
// GetRendererInfo() RETURNS A REFERENCE (BackendObject.h:590), so the mirror must OWN a
|
||||||
|
// RendererInfo instance to hand back - including before the first snapshot arrives, because
|
||||||
|
// LogBackendInfo() reads it at MG_Backend/Init.cpp:21, during MG_Backend::Init(), long before
|
||||||
|
// any context exists. Ruling (scout-caps-reply §1.2 option (a)): the mirror answers with a
|
||||||
|
// placeholder until the first snapshot, P5 accepts one inaccurate startup log line, and
|
||||||
|
// MG_Backend::Init() is NOT restructured.
|
||||||
|
//
|
||||||
|
// GetFormatCapabilities() is NON-VIRTUAL (BackendObject.h:594), so a remote backend object
|
||||||
|
// cannot override the accessor: it must FILL BackendObject::m_formatCapabilities from this
|
||||||
|
// mirror instead.
|
||||||
|
//
|
||||||
|
// INVALIDATION IS RE-ARRIVAL (R-12). DirectGLES has no OnCapsInvalidated producer at all - it
|
||||||
|
// re-runs UpdateAdvertisedCapabilityExtensions + UpdateDynamicBackendParameters at
|
||||||
|
// BackendObject_DirectGLES.cpp:865-871 and tells the frontend nothing, which is correct in
|
||||||
|
// monolith and a silent bug under split. Rather than add a DirectGLES-side callback (a
|
||||||
|
// dev-shaped backend edit), the SERVER re-sends the whole snapshot on every InitCapabilities
|
||||||
|
// re-run and the CLIENT treats a second arrival as the invalidation. Generation() is what a
|
||||||
|
// client-side memo keys on, and it is also the re-open signal for the server-context-death
|
||||||
|
// case that MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
class CapsMirror {
|
||||||
|
public:
|
||||||
|
// Replaces the whole mirror and bumps Generation(). Called once per CapsSnapshot,
|
||||||
|
// including the re-sends that mean "invalidate" (R-12).
|
||||||
|
void Adopt(const MG_Pipe::MGPCaps& caps, const MG_Backend::FormatCapabilityCache& formats,
|
||||||
|
const RendererInfo& renderer, const String& apiVersion,
|
||||||
|
BackendType backend);
|
||||||
|
|
||||||
|
// False until the first snapshot. The placeholder answers below are still safe to
|
||||||
|
// read - that is the point - but a caller that can wait should.
|
||||||
|
Bool Valid() const;
|
||||||
|
|
||||||
|
// ++ on every Adopt. A client memo that survives a server context loss must key on
|
||||||
|
// this; nothing else on the client can see that the server's context died.
|
||||||
|
Uint64 Generation() const;
|
||||||
|
|
||||||
|
const RendererInfo& Renderer() const;
|
||||||
|
const MG_Backend::DynamicBackendParameters& Dynamic() const;
|
||||||
|
const MG_Backend::FormatCapabilityCache& Formats() const;
|
||||||
|
const String& ApiVersion() const;
|
||||||
|
// The SERVER's backend type, never a new "Remote" enumerator: frontend branches
|
||||||
|
// switch on this (GL_Framebuffer.cpp:47, GL_Texture.cpp:6536, CompileEnv.cpp:122) and
|
||||||
|
// a value they do not know silently takes the wrong arm.
|
||||||
|
BackendType Backend() const;
|
||||||
|
|
||||||
|
Uint64 CallMask() const;
|
||||||
|
Bool HasCap(MG_Pipe::MGPCapBit bit) const;
|
||||||
|
|
||||||
|
// R-8. `subsystemBit` is a kMGPipeSubsystem* constant. THIS IS THE ONLY LEGAL SOURCE
|
||||||
|
// of the answer on the client under split: MGPipeGetResourceOps() is the SERVER's
|
||||||
|
// registration and is null in the client process, which would silently disable the
|
||||||
|
// whole push path in the one mode that matters.
|
||||||
|
Bool ServerConsumes(Uint64 subsystemBit) const;
|
||||||
|
|
||||||
|
// GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting (BackendObject.h:274) does NOT
|
||||||
|
// ride in MGPCaps::Dynamic - it is a member of the function table, which is precisely
|
||||||
|
// the thing a split client never receives. Its only non-test client reader is
|
||||||
|
// GL_Query.cpp:221, and under split it must be answered from kCapCpuXfbPrimitiveAccounting.
|
||||||
|
Bool PrefersCpuXfbPrimitiveAccounting() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
MG_Pipe::MGPCaps m_caps{};
|
||||||
|
MG_Backend::FormatCapabilityCache m_formats{};
|
||||||
|
RendererInfo m_renderer{};
|
||||||
|
String m_apiVersion;
|
||||||
|
BackendType m_backend = BackendType::Unknown;
|
||||||
|
Uint64 m_generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per client context in principle; one per process in P5, because P5 serves one context.
|
||||||
|
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
|
||||||
|
// reach pipe or backend state from an exit handler.
|
||||||
|
CapsMirror& CapsMirrorInstance();
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for packages s1 (construction, handshake) and c1 (barrier, reply read).
|
||||||
|
|
||||||
|
#include "ClientSession.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedClientSession, \"%s\"} - P5 packages s1/c1 have not " \
|
||||||
|
"landed this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
// Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to
|
||||||
|
// install the remote backend object, and that question has a legitimate "no" - it is the
|
||||||
|
// monolith answer. Every call that PRESUMES a session aborts instead.
|
||||||
|
ClientSession* ClientSession::Active() { return nullptr; }
|
||||||
|
|
||||||
|
MobileGLResult ClientSession::Start(MG_Config::TransportMode, const String&) {
|
||||||
|
MGP5_C0_STUB("ClientSession::Start");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientSession::Stop() { MGP5_C0_STUB("ClientSession::Stop"); }
|
||||||
|
|
||||||
|
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
|
||||||
|
|
||||||
|
CapsMirror& ClientSession::Caps() { return CapsMirrorInstance(); }
|
||||||
|
|
||||||
|
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
|
||||||
|
void*, Uint64, Int32*) {
|
||||||
|
MGP5_C0_STUB("ClientSession::EmitAndWait");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
||||||
|
|
||||||
|
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
|
||||||
|
// and an assertion helper that aborts when asked is worse than useless.
|
||||||
|
Bool ClientSession::InBarrierWait() { return false; }
|
||||||
|
Bool ClientSession::ApplyThreadIsInsideApplier() { return false; }
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/ClientSession.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The client half of a session: the rings, the handshake, the verb barrier. Owner: package s1
|
||||||
|
// (construction and handshake) with c1 (the barrier and the reply read). Signatures by c0.
|
||||||
|
//
|
||||||
|
// INPROC USES ShmSegment AND THE RING, NOT new[] AND NOT InProcessTransport's deques. That is
|
||||||
|
// half of what "inproc runs the same G3 codec as spawn" means: InProcessTransport
|
||||||
|
// (InProcessTransport.cpp:38-97) is two deque<vector<uint8_t>> plus two condvar doorbells, it
|
||||||
|
// touches neither a ring nor a codec, and building the session on top of it instead of on top
|
||||||
|
// of the ring would make the whole phase unfalsifiable. The transport supplies the two
|
||||||
|
// DOORBELLS and the control plane; the records go through SEG_CMD.
|
||||||
|
//
|
||||||
|
// THE VERB BARRIER (R-1). After emitting a verb the client blocks until
|
||||||
|
// RingControl::appliedSeq >= the seq it just got back from the encoder. It is not caution: 31
|
||||||
|
// of the 63 PipeInputs fields are still filled by the client's residual pass out of a live
|
||||||
|
// GLContext, so an unbarriered queue lets the server read a FUTURE value of them. Two
|
||||||
|
// consequences that must be stated because both are load-bearing:
|
||||||
|
// - while the barrier holds, at most one of {GL thread, apply thread} is runnable, which is
|
||||||
|
// what makes a single process-wide gPipeInputs legal (table 3);
|
||||||
|
// - the barrier is a RETIRING object, not a design. It opens family by family as table 2's
|
||||||
|
// BARRIER-PULLED column empties, and each later package reports how many rows it left.
|
||||||
|
//
|
||||||
|
// THE BARRIER'S WAIT IS ALSO THE REPLY'S WAIT (R-3/R-5). The reply slot id IS the record seq,
|
||||||
|
// so "wait for appliedSeq >= mySeq" and "wait for my answer" are one wait and the four Bool
|
||||||
|
// acceptance returns, ReadPixels' pixels and MapPersistent's decline cost ZERO extra round
|
||||||
|
// trips. The client MUST NOT re-derive any of those four answers locally - that is the c0f/c0g
|
||||||
|
// defect P4a paid two contract corrections for, and "always accept" is ID-39's 66 lost uploads.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#include "../Wire/PipeWireCodec.h"
|
||||||
|
#include "CapsMirror.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
class ClientSession {
|
||||||
|
public:
|
||||||
|
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
|
||||||
|
static ClientSession* Active();
|
||||||
|
|
||||||
|
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
|
||||||
|
// - for TransportMode::InProcess - starts the server role's apply thread. Returns a
|
||||||
|
// named error rather than falling back to monolith: a fallback here is the "split lane
|
||||||
|
// ran monolith and went green" failure, and it must be loud.
|
||||||
|
MobileGLResult Start(MG_Config::TransportMode mode, const String& endpoint);
|
||||||
|
|
||||||
|
// Teardown order matters and is table 3's fourth column: publish and let the server
|
||||||
|
// drain, Doorbell::Kill() (the ONLY thing that wakes an apply thread parked on
|
||||||
|
// kWaitForever, Doorbell.h:211-221), then join, and only then release anything an
|
||||||
|
// emitter owns - a tail still referenced by an unapplied record is a use-after-free
|
||||||
|
// the join is what prevents.
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
Wire::PipeWireEncoder& Encoder();
|
||||||
|
CapsMirror& Caps();
|
||||||
|
|
||||||
|
// Emit one record and, if the barrier is armed, wait for it. `replyOut`/`replyBytes`
|
||||||
|
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
|
||||||
|
// Returns the record's seq, which is also its reply-slot id.
|
||||||
|
//
|
||||||
|
// Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with
|
||||||
|
// producerParked set before blocking - the shape Doorbell.h:121 already implements.
|
||||||
|
Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||||
|
const void* varTail, Uint64 varTailBytes, void* replyOut,
|
||||||
|
Uint64 replyBytes, Int32* statusOut);
|
||||||
|
|
||||||
|
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
|
||||||
|
// red; it must be run once and the way it goes red recorded.
|
||||||
|
Bool BarrierArmed() const;
|
||||||
|
|
||||||
|
// R-1's invariant made checkable rather than only written down: true while this
|
||||||
|
// thread is inside a barrier wait. The apply thread sets its own flag on entry to the
|
||||||
|
// applier; a debug/verify build asserts the two are never both true, and that the
|
||||||
|
// client never touches gPipeInputs while the server is inside the applier.
|
||||||
|
static Bool InBarrierWait();
|
||||||
|
static Bool ApplyThreadIsInsideApplier();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Wire::PipeWireEncoder m_encoder;
|
||||||
|
Wire::SegmentTable m_segments;
|
||||||
|
CapsMirror* m_caps = nullptr;
|
||||||
|
Bool m_barrierArmed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for package c1.
|
||||||
|
|
||||||
|
#include "EmitTables.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
// The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is
|
||||||
|
// GLFunctionsTable plus Present plus SetSwapInterval; GLFunctionsTable is 69 function
|
||||||
|
// pointers plus one Bool (PrefersCpuXfbPrimitiveAccounting, BackendObject.h:274). A slot
|
||||||
|
// added to either without a decision here is a build break, which is the point: R-4 forbids
|
||||||
|
// a null slot, so a new slot needs an owner on the day it appears.
|
||||||
|
static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) ==
|
||||||
|
sizeof(MG_Backend::GLFunctionsTable) + 2 * sizeof(void (*)()),
|
||||||
|
"GlobalBackendFunctionsTable is no longer GLFunctionsTable + Present + SetSwapInterval");
|
||||||
|
static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) ==
|
||||||
|
kRemoteEmitSlotCount * sizeof(void (*)()) + sizeof(void (*)()),
|
||||||
|
"the emit table's 71 slots plus the packed Bool no longer describe the table");
|
||||||
|
|
||||||
|
[[noreturn]] void UnmigratedVerbFatal(const char* slot) {
|
||||||
|
// The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the
|
||||||
|
// slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is
|
||||||
|
// inert in an INFO build - and INFO is what every device lane runs.
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot);
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() {
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedEmitTable, \"RemoteEmitTable\"} - P5 package c1 has "
|
||||||
|
"not landed this yet; c0 shipped the signature only");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 ImplementedVerbCount() { return 0; }
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/EmitTables.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The client's emitting function table. Owner: package c1. Signatures by c0.
|
||||||
|
//
|
||||||
|
// MG_Backend/Init.cpp:44 assigns gBackendFunctionsTable from the active backend object, and
|
||||||
|
// 91 MG_Impl/GLImpl sites call through it directly. So a BackendObject_Remote has to return a
|
||||||
|
// COMPLETE table, and "complete" is a bigger number than R-4's headline:
|
||||||
|
//
|
||||||
|
// GLFunctionsTable (BackendObject.h:117-292) = 69 function pointers
|
||||||
|
// + Bool PrefersCpuXfbPrimitiveAccounting
|
||||||
|
// GlobalBackendFunctionsTable (BackendObject.h:293-299) = the above, + Present, + SetSwapInterval
|
||||||
|
// = 71 function pointers in total
|
||||||
|
//
|
||||||
|
// R-4's rule, restated over all 71: NO SLOT MAY BE NULL, and no slot may fall through to a
|
||||||
|
// driver. A null slot is 91 potential null calls; a pass-through slot is a split lane quietly
|
||||||
|
// running monolith and going green, which is the one outcome every gate in this phase exists
|
||||||
|
// to prevent. A verb P5 does not implement gets a slot that raises
|
||||||
|
// Fatal{UnmigratedVerb, "<slot>"} - the same shape as MGPipeInputPoisonFatal, live at every
|
||||||
|
// log level, MGLOG_F + std::abort.
|
||||||
|
//
|
||||||
|
// WHICH SLOTS GET A REAL EMITTER IS DECIDED BY THE VERB CENSUS (R-4), not guessed here:
|
||||||
|
// ~/w7/notes/p5/verb-census.md. CONTRACT-P5.md §7 carries the resulting THREE-CLASS SPLIT and
|
||||||
|
// it is not to be re-derived:
|
||||||
|
//
|
||||||
|
// A. ANSWERED LOCALLY from the caps mirror, never emitted and never Fatal (R-15) - two
|
||||||
|
// slots, GetIntegeri_v and IsTimerQuerySupported, plus the Bool member
|
||||||
|
// PrefersCpuXfbPrimitiveAccounting, which is not a slot. GetIntegeri_v is the one that
|
||||||
|
// would otherwise sink the phase: it is reached by the FIRST glCompileShader of every
|
||||||
|
// context (CompileEnv.cpp:134-138 <- Core.cpp:39), not by any verb, so a Fatal there
|
||||||
|
// aborts every scenario before it draws anything.
|
||||||
|
// B. EMITTED in P5 - five slots: Clear, DrawArrays, ReadPixels, BlitFramebuffer, Present.
|
||||||
|
// Present has ZERO MG_Impl call sites: it is reached through EGLImpl.cpp:178 ->
|
||||||
|
// BackendObject.cpp:396, so mirroring GLImpl will not find it.
|
||||||
|
// C. Fatal{UnmigratedVerb} - the remaining 64, SetSwapInterval and GetGpuTimestampNs among
|
||||||
|
// them.
|
||||||
|
//
|
||||||
|
// AND THE RULE R-4 WOULD OTHERWISE BREAK. 41 of the 69 slots are null-checked at their call
|
||||||
|
// site, and several of those checks are CAPABILITY PROBES, not safety checks - BeginOcclusionQuery
|
||||||
|
// (GL_Query.cpp:481, :785), BeginXfbPrimitivesQuery (:534), SubDataResident. With no null slot
|
||||||
|
// in this table every one of them answers "supported" and the fallback behind it silently
|
||||||
|
// disappears. A null check on a slot may not survive into the client: it becomes a caps-mirror
|
||||||
|
// read, which is what ARCHITECTURE.md:114 means by "CallMask replaces 'is this table slot null'".
|
||||||
|
//
|
||||||
|
// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture,
|
||||||
|
// sampler and program families do NOT come through here. They are emitted from
|
||||||
|
// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under
|
||||||
|
// split each of those becomes an encode. This table covers only the verbs - the draws,
|
||||||
|
// clears, blits, readbacks, queries, fences and present.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
// The table MG_Backend::Init() installs into gBackendFunctionsTable for the remote role.
|
||||||
|
// A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8).
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable();
|
||||||
|
|
||||||
|
// Called by the Fatal slots. Named separately so a death test can filter on it and so
|
||||||
|
// that the message wording lives in exactly one place.
|
||||||
|
[[noreturn]] void UnmigratedVerbFatal(const char* slot);
|
||||||
|
|
||||||
|
// How many of the 71 slots have a real emitter. Reported at bring-up and asserted by the
|
||||||
|
// gate: a table that silently loses an emitter should not be able to look the same as one
|
||||||
|
// that never had it.
|
||||||
|
Uint32 ImplementedVerbCount();
|
||||||
|
|
||||||
|
// The total the count above is out of. Asserted against the struct in EmitTables.cpp, so
|
||||||
|
// a slot added to GLFunctionsTable without a decision here is a build break.
|
||||||
|
inline constexpr Uint32 kRemoteEmitSlotCount = 71;
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for package v1 (with p1 for the stamp rule).
|
||||||
|
|
||||||
|
#include "PipeApplier.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
||||||
|
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
|
||||||
|
|
||||||
|
void ReplyPool::PostReply(Uint64, Int32, const void*, Uint64) { MGP5_C0_STUB("ReplyPool::PostReply"); }
|
||||||
|
|
||||||
|
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
||||||
|
|
||||||
|
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
|
||||||
|
: m_segments(segments), m_replies(replies) {}
|
||||||
|
|
||||||
|
Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); }
|
||||||
|
|
||||||
|
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) {
|
||||||
|
MGP5_C0_STUB("PipeApplier::StampVerbBoundary");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; }
|
||||||
|
|
||||||
|
void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) {
|
||||||
|
MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The server's applier bridge. Owner: package v1, with p1 for the stamp rule. Signatures by c0.
|
||||||
|
//
|
||||||
|
// IT IS A BRIDGE, NOT AN APPLIER. The applier already exists and is not edited by this phase:
|
||||||
|
// MG_Pipe/PipeApply.{h,cpp}, 37 MGPipeApply* free functions. This class owns the three things
|
||||||
|
// that only exist once records arrive over a wire rather than by direct call:
|
||||||
|
//
|
||||||
|
// 1. THE VERB STAMP. This is the phase's prerequisite, and it is not in the ROADMAP row.
|
||||||
|
// MGPipeApplyAccess deliberately does not stamp the poison generations
|
||||||
|
// (PipeInputs.h:612-618): "a stamp says the filler published this for THIS verb, which is
|
||||||
|
// the walk's statement, not the applier's". Under split the filler is in another role, so
|
||||||
|
// NOTHING stamps, every FilledGen[] stays 0, MGPipeInputFieldIsFresh returns false for
|
||||||
|
// everything, and a pure server aborts on the FIRST read inside SyncRenderState with
|
||||||
|
// Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"} - before reaching any
|
||||||
|
// interesting case. So: the server stamps at the verb boundary. p1 defines what is
|
||||||
|
// stamped and for which verb; v1 places the call. Neither half works alone.
|
||||||
|
//
|
||||||
|
// 2. ACCEPTANCE. Four applier entry points return Bool - ResourceCreate, ResourceRespecify,
|
||||||
|
// ResourceSubData, SetTextureParams - and MapPersistent returns void*. Those returns are
|
||||||
|
// what the CLIENT gates destructive state changes on (clearing per-level dirty flags,
|
||||||
|
// latching parameters, adopting a pointer). They go back through the reply slot, id =
|
||||||
|
// record seq (R-3), and are collected in the barrier's existing wait (R-5). The client
|
||||||
|
// may not recompute any of them.
|
||||||
|
//
|
||||||
|
// 3. R-11, THE BORROWED-POINTER RULE. A SEG_STAGE run is valid from publish until retiredSeq
|
||||||
|
// passes the record naming it. NO APPLIER ENTRY POINT MAY HOLD A POINTER PAST ITS RETURN.
|
||||||
|
// The tree has exactly one violation and it is named: GLESBufferResource::hostBytes
|
||||||
|
// (Managers.h:839), written by Ops_H_SubData (Managers.cpp:1980-1983) and Ops_H_FlushRange
|
||||||
|
// (:2035), read by six later drains (:2000, :2062, :2080, :2111, :2741, :2843). Under split
|
||||||
|
// those two must copy into server-owned storage. MOBILEGL_IPC_AUDIT=1's 0xDD fill (R-2.5)
|
||||||
|
// is the mechanical control that says whether they did.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#include "../Transport/Ring.h"
|
||||||
|
#include "../Wire/PipeWireCodec.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
// Writes answers into SEG_REPLY at seq % slots, stamping the seq back into the slot header
|
||||||
|
// so a wrong-slot read is detectable rather than plausible (table 0's slot header row:
|
||||||
|
// {Uint64 Seq; Int32 Status; Uint32 Size;}).
|
||||||
|
class ReplyPool final : public Wire::ReplySink {
|
||||||
|
public:
|
||||||
|
ReplyPool() = default;
|
||||||
|
ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes);
|
||||||
|
|
||||||
|
void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) override;
|
||||||
|
|
||||||
|
// A reply larger than one slot is Fatal rather than chunked: P5's only large answer is
|
||||||
|
// ReadPixels, whose size the client already knows before it emits, so the slot size is
|
||||||
|
// chosen from that and an overflow means the two sides disagree about the frame.
|
||||||
|
Uint32 SlotBytes() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Uint8* m_base = nullptr;
|
||||||
|
Uint64 m_size = 0;
|
||||||
|
Uint32 m_slots = 0;
|
||||||
|
Uint32 m_slotBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PipeApplier {
|
||||||
|
public:
|
||||||
|
PipeApplier() = default;
|
||||||
|
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
|
||||||
|
|
||||||
|
// Decode one record, stamp the verb, apply, post the reply if the call has one, then
|
||||||
|
// advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): the barrier's
|
||||||
|
// waiter reads it, and a batched watermark promises work that has not run.
|
||||||
|
Bool ApplyOne(const Transport::RingRecordView& record);
|
||||||
|
|
||||||
|
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
|
||||||
|
// runs, with the verb the record belongs to.
|
||||||
|
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
|
||||||
|
|
||||||
|
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
|
||||||
|
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
|
||||||
|
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
|
||||||
|
Uint64 ResidualPullCount() const;
|
||||||
|
|
||||||
|
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
|
||||||
|
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
|
||||||
|
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Wire::SegmentTable* m_segments = nullptr;
|
||||||
|
ReplyPool* m_replies = nullptr;
|
||||||
|
Wire::PipeWireDecoder m_decoder;
|
||||||
|
Uint64 m_residualPulls = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for package v1 - the phase's highest-risk package.
|
||||||
|
|
||||||
|
#include "ServerLoop.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedServerLoop, \"%s\"} - P5 package v1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
MobileGLResult ServerLoop::Start(ServerSession&) { MGP5_C0_STUB("ServerLoop::Start"); }
|
||||||
|
|
||||||
|
void ServerLoop::Stop() { MGP5_C0_STUB("ServerLoop::Stop"); }
|
||||||
|
|
||||||
|
// Not a stub: teardown asks this to decide whether to Kill and join at all, and a teardown
|
||||||
|
// helper that aborts when the thread was never started is a hang in the shutdown path.
|
||||||
|
Bool ServerLoop::Running() const { return m_running; }
|
||||||
|
|
||||||
|
MG_Backend::BackendObject* ServerLoop::Backend() { MGP5_C0_STUB("ServerLoop::Backend"); }
|
||||||
|
|
||||||
|
MobileGLResult ServerLoop::RunOnApplyThread(ControlWork, void*) {
|
||||||
|
MGP5_C0_STUB("ServerLoop::RunOnApplyThread");
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerLoop& ServerLoopInstance() {
|
||||||
|
// ID-8: leak at exit, like every MG_Remote singleton.
|
||||||
|
static ServerLoop& instance = *new ServerLoop{};
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The apply thread and the server's private backend object. Owner: package v1 - the highest
|
||||||
|
// risk item in P5. Signatures by c0.
|
||||||
|
//
|
||||||
|
// WHY THE THREAD IS THE POINT. DirectGLES has 16 IsBackendContextCurrentOnThisThread() guards
|
||||||
|
// (DirectGLES.cpp:12034..12428) and Managers.cpp has 16 CanTouchGLNow() guards (:1494..:3428);
|
||||||
|
// every one of them DEGRADES when the answer is false - fences become always-signaled, queries
|
||||||
|
// return null handles, Present creates no frame fence so the buffer pool's recycle watermark
|
||||||
|
// never advances, and the two persistent-map acquisitions (Managers.cpp:1494, :2170) DECLINE,
|
||||||
|
// which would make PersistentCoherentMapScenario unreachable. Making the apply thread the
|
||||||
|
// context owner for life turns all 32 of those answers true on the server and removes the
|
||||||
|
// whole degradation class at once. It is also exactly the shape P6's spawned server inherits.
|
||||||
|
//
|
||||||
|
// P5 BUILDS ONE THREAD, NOT TWO. No mgl-srv-io: inproc's control plane is in the same process.
|
||||||
|
// P6 splits it.
|
||||||
|
//
|
||||||
|
// PARKING AND SHUTDOWN. The thread parks on Doorbell::Wait(consumerParked, ready, spinUs,
|
||||||
|
// kWaitForever) and shuts down when Wait returns false with Dead() set. Doorbell::Kill()
|
||||||
|
// (Doorbell.h:211-221) IS THE ONLY THING that wakes a thread parked on kWaitForever - a fact
|
||||||
|
// ARCHITECTURE.md's teardown order (:537) omits and InProcessTransportTest.cpp:344 already
|
||||||
|
// pins. Kill BEFORE join; join before the client frees any emitter-owned Vector; and the join
|
||||||
|
// must be bounded (that test uses 5 s) so a regression is a red test and not a hung CI job.
|
||||||
|
//
|
||||||
|
// THE EGL OWNERSHIP MOVE. eglMakeCurrent runs ONCE on this thread and is never released
|
||||||
|
// (DirectGLES.cpp:11925 plus the six cache invalidations at :11933-11953, which become a
|
||||||
|
// one-time startup cost instead of a per-make-current storm). The client's nine EGL virtuals
|
||||||
|
// become BLOCKING control requests executed here. ReleaseEGLResources and
|
||||||
|
// ~BackendObject_DirectGLES MUST be blocking: MobileGL::Destroy() (MobileGL/Init.cpp:68)
|
||||||
|
// otherwise walks on while the server still holds the context.
|
||||||
|
//
|
||||||
|
// THE FALLBACK IS PRE-DECLARED, NOT INVENTED UNDER PRESSURE (R-1). If the context migration is
|
||||||
|
// still not running ClearThenReadPixels at the end of v1's fourth working day, the integrator -
|
||||||
|
// not the package - declares `inproc-inline`: the client thread drains the ring itself, no
|
||||||
|
// thread is created, no context migrates, and a second package picks up the thread arm.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include "ServerSession.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
class ServerLoop {
|
||||||
|
public:
|
||||||
|
// Creates the apply thread, names it mgl-srv-apply, applies
|
||||||
|
// MOBILEGL_IPC_SERVER_AFFINITY (borrowing ShaderCompilePool's big-core detection) and
|
||||||
|
// LOGS THE RESOLVED MASK - an affinity that silently did nothing is indistinguishable
|
||||||
|
// from one that worked, and the split's whole performance claim rests on both halves
|
||||||
|
// landing on fast cores.
|
||||||
|
MobileGLResult Start(ServerSession& session);
|
||||||
|
|
||||||
|
// Kill the doorbell, join the thread (bounded), then destroy the private backend object
|
||||||
|
// ON THAT THREAD before it exits. Blocking by contract - see the header note.
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
Bool Running() const;
|
||||||
|
|
||||||
|
// The server role's own backend object. NOT pActiveBackendObject: that global holds the
|
||||||
|
// client's BackendObject_Remote. Table 3's ruling is that the server holds its
|
||||||
|
// BackendObject_DirectGLES privately here, and that the seven backend-internal reads of
|
||||||
|
// pActiveBackendObject - ClampSamplesToBackendSupport (BackendObject_DirectGLES.cpp:815,
|
||||||
|
// :819) and five in Utils.cpp (:74, :82, :126, :220, :260), all of them format-capability
|
||||||
|
// lookups - take the format cache as a parameter instead. That is six functions across
|
||||||
|
// two files, and it is why no thread-keyed shim is needed for MOBILEGL_BUILD_DISAGGREGATED_INPROC.
|
||||||
|
MG_Backend::BackendObject* Backend();
|
||||||
|
|
||||||
|
// Run one blocking control request on the apply thread and wait for it. This is how all
|
||||||
|
// nine EGL lifecycle virtuals cross; it is deliberately NOT a queue of async messages,
|
||||||
|
// because every one of them has a return value the caller acts on immediately.
|
||||||
|
//
|
||||||
|
// A raw function pointer plus a user pointer, not std::function: this runs on the
|
||||||
|
// teardown path too, and the teardown path may not allocate - ID-8's leak-at-exit rule
|
||||||
|
// exists because frontend destructors reach here from exit handlers.
|
||||||
|
using ControlWork = MobileGLResult (*)(void* user);
|
||||||
|
MobileGLResult RunOnApplyThread(ControlWork work, void* user);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ServerSession* m_session = nullptr;
|
||||||
|
Bool m_running = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ServerLoop& ServerLoopInstance();
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0 stubs for package s1.
|
||||||
|
|
||||||
|
#include "ServerSession.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedServerSession, \"%s\"} - P5 package s1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
ServerSession* ServerSession::Active() { return nullptr; }
|
||||||
|
|
||||||
|
MobileGLResult ServerSession::Accept(Transport::ITransport&) { MGP5_C0_STUB("ServerSession::Accept"); }
|
||||||
|
|
||||||
|
MobileGLResult ServerSession::PublishCapsSnapshot() {
|
||||||
|
MGP5_C0_STUB("ServerSession::PublishCapsSnapshot");
|
||||||
|
}
|
||||||
|
|
||||||
|
Transport::RingConsumer& ServerSession::CommandRing() { return m_commands; }
|
||||||
|
Transport::RingControl& ServerSession::Control() { MGP5_C0_STUB("ServerSession::Control"); }
|
||||||
|
Wire::SegmentTable& ServerSession::Segments() { return m_segments; }
|
||||||
|
PipeApplier& ServerSession::Applier() { return m_applier; }
|
||||||
|
ReplyPool& ServerSession::Replies() { return m_replies; }
|
||||||
|
|
||||||
|
Transport::Doorbell& ServerSession::ConsumerDoorbell() {
|
||||||
|
MGP5_C0_STUB("ServerSession::ConsumerDoorbell");
|
||||||
|
}
|
||||||
|
Transport::Doorbell& ServerSession::ProducerDoorbell() {
|
||||||
|
MGP5_C0_STUB("ServerSession::ProducerDoorbell");
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Server/ServerSession.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The server half of a session: the consumer side of the rings, the handshake reply, the caps
|
||||||
|
// snapshot. Owner: package s1. Signatures by c0.
|
||||||
|
//
|
||||||
|
// The four segment sizes are already pinned by ProtocolSmokeTest.cpp:72 and are not up for
|
||||||
|
// re-derivation here: SEG_CMD 8 MiB, SEG_STAGE 32 MiB, SEG_REPLY 8 MiB, SEG_EVENT 256 KiB.
|
||||||
|
// MOBILEGL_IPC_RING_MB and MOBILEGL_IPC_STAGE_MB move the first two; the ring caps ONE record
|
||||||
|
// at half its size, so the default 8 MiB caps a record at 4 MiB (R-10).
|
||||||
|
//
|
||||||
|
// THE TWO DOORBELL ACCESSORS ARE ON THE CONCRETE CLASS, NOT ON ITransport
|
||||||
|
// (InProcessTransport.h:64-68). P5 decides this now rather than letting P6 discover it: the
|
||||||
|
// SESSION owns the pair and hands out references, so ITransport stays the dumb control-plane
|
||||||
|
// interface its header says it is and SocketTransport does not have to grow two accessors it
|
||||||
|
// has no natural home for. Discovering this in P6 would mean re-laying one package's call sites.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#include "../Transport/Doorbell.h"
|
||||||
|
#include "../Transport/ITransport.h"
|
||||||
|
#include "../Transport/Ring.h"
|
||||||
|
#include "../Wire/PipeWireCodec.h"
|
||||||
|
#include "PipeApplier.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Server {
|
||||||
|
|
||||||
|
class ServerSession {
|
||||||
|
public:
|
||||||
|
static ServerSession* Active();
|
||||||
|
|
||||||
|
// Maps the four segments, answers Hello with Welcome, and publishes the first
|
||||||
|
// CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is
|
||||||
|
// decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable)
|
||||||
|
// and the build fingerprint must match, and a mismatch is Fatal{AbiMismatch}.
|
||||||
|
MobileGLResult Accept(Transport::ITransport& transport);
|
||||||
|
|
||||||
|
// Re-publishes the whole snapshot. R-12: a SECOND arrival IS the invalidation signal,
|
||||||
|
// which is how DirectGLES - which has no OnCapsInvalidated producer - tells the client
|
||||||
|
// its InitCapabilities re-ran, without any dev-shaped backend edit. It is also the
|
||||||
|
// re-open signal for "the server's ES context died and its rings were dropped", the
|
||||||
|
// event MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58).
|
||||||
|
MobileGLResult PublishCapsSnapshot();
|
||||||
|
|
||||||
|
Transport::RingConsumer& CommandRing();
|
||||||
|
Transport::RingControl& Control();
|
||||||
|
Wire::SegmentTable& Segments();
|
||||||
|
PipeApplier& Applier();
|
||||||
|
ReplyPool& Replies();
|
||||||
|
|
||||||
|
// The client rings this one; the apply thread parks on it.
|
||||||
|
Transport::Doorbell& ConsumerDoorbell();
|
||||||
|
// The server rings this one, but only when producerParked is set (a store to a shared
|
||||||
|
// cache line otherwise burns a big core for a whole frame on a phone).
|
||||||
|
Transport::Doorbell& ProducerDoorbell();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Transport::RingConsumer m_commands;
|
||||||
|
Wire::SegmentTable m_segments;
|
||||||
|
PipeApplier m_applier;
|
||||||
|
ReplyPool m_replies;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Server
|
||||||
@@ -41,6 +41,49 @@
|
|||||||
// (Records.def / PipeCalls.def) is a separate deliverable; the ring itself
|
// (Records.def / PipeCalls.def) is a separate deliverable; the ring itself
|
||||||
// only needs kind/flags/size, so it can carry the real records the day they
|
// only needs kind/flags/size, so it can carry the real records the day they
|
||||||
// land without changing shape.
|
// land without changing shape.
|
||||||
|
//
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// THE FIVE WATERMARKS (P5 R-9). One sentence each, and they are a contract:
|
||||||
|
// every one of the five was declared here at P0 and written by nobody but
|
||||||
|
// InitRingControl, so until P5 there was nothing to disagree with.
|
||||||
|
//
|
||||||
|
// submittedSeq Advanced by the PRODUCER after it publishes. NOBODY
|
||||||
|
// WAITS ON IT - it is diagnostic, the answer to "how far
|
||||||
|
// ahead of the server is the client right now".
|
||||||
|
// appliedSeq Advanced by the CONSUMER for EVERY SINGLE RECORD it
|
||||||
|
// applies. The client's verb barrier and every reply wait
|
||||||
|
// read it, so it is the one watermark P5 FORBIDS BATCHING:
|
||||||
|
// the sixty-four-record batching this ring was designed
|
||||||
|
// for makes a waiter block on work that already ran, or -
|
||||||
|
// far worse - resume on work that has not.
|
||||||
|
// retiredSeq Advanced by the CONSUMER once it has finished with the
|
||||||
|
// SEG_STAGE bytes a record referenced. The staging
|
||||||
|
// allocator reclaims behind it, and nothing else may.
|
||||||
|
// completedFrameSerial Advanced by the SERVER when a present completes. What
|
||||||
|
// recycling and ageing wait on; it trails appliedSeq by
|
||||||
|
// the GPU's own depth and must never be conflated with it.
|
||||||
|
// presentAckSerial Advanced by the SERVER when it returns a present credit.
|
||||||
|
// The client's present throttle waits on it; it is the
|
||||||
|
// only back-pressure that bounds latency rather than bytes.
|
||||||
|
//
|
||||||
|
// Every wait on all five is `>=`, never `==`: a waiter that tests equality
|
||||||
|
// misses the wakeup the moment a producer or consumer moves by more than one.
|
||||||
|
//
|
||||||
|
// BATCHING MAY ONLY MAKE A WATERMARK LATE. All five except appliedSeq may be
|
||||||
|
// published lazily, because a waiter that sees an old value waits longer than
|
||||||
|
// it had to and is still correct. NONE of them may ever be published EARLY: a
|
||||||
|
// watermark that reports more than was actually done turns every waiter into a
|
||||||
|
// silent use of work that has not happened, and there is no checksum anywhere
|
||||||
|
// on this ring that would catch it.
|
||||||
|
//
|
||||||
|
// kRecPad DOES NOT ADVANCE SEQ. A wrap filler is framing, not a record: it has
|
||||||
|
// no opcode, no payload meaning and no reply slot. Both sides must skip it
|
||||||
|
// BEFORE counting. If one side counts it and the other does not, the two seq
|
||||||
|
// spaces drift by one at every wrap - and because seq IS the reply-slot id
|
||||||
|
// (P5 R-3), a drifted seq silently reads another call's answer rather than
|
||||||
|
// failing. Nothing on this ring would detect that, which is why the rule is
|
||||||
|
// stated here rather than left to each side's loop.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 c0's stubs for package w1. Every body is MGLOG_F + std::abort and NOT a silent no-op:
|
||||||
|
// an unimplemented codec that returns quietly is exactly how a split lane runs monolith and
|
||||||
|
// goes green, which is the failure the whole phase is built to make impossible.
|
||||||
|
|
||||||
|
#include "PipeWireCodec.h"
|
||||||
|
|
||||||
|
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Wire {
|
||||||
|
|
||||||
|
// Table 0's first row, mechanised: this enum and the schema's SegmentKind are ONE id
|
||||||
|
// space, and the only place they are compared is here. A schema edit that renumbers a
|
||||||
|
// segment is a build break rather than a wrong pointer on a ring.
|
||||||
|
//
|
||||||
|
// Fully qualified from the global namespace on purpose: the generated header's namespace
|
||||||
|
// is `MobileGL::Wire` and we are inside `MobileGL::MG_Remote::Wire`, so a bare `Wire::`
|
||||||
|
// resolves to THIS namespace and the assertion would silently be about the wrong enum -
|
||||||
|
// or, as it first was, fail to compile for a reason that looks unrelated.
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::None) == kSegNone);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Cmd) == kSegCmd);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Stage) == kSegStage);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Reply) == kSegReply);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Event) == kSegEvent);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Shadow) == kSegShadow);
|
||||||
|
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Adopt) == kSegAdopt);
|
||||||
|
// And the other half of table 0's rule: MG_Pipe's "no segment" sentinel is the same 0.
|
||||||
|
static_assert(static_cast<Uint32>(MG_Pipe::kMGHostSpanSegNone) == kSegNone,
|
||||||
|
"kMGHostSpanSegNone and SegmentId::kSegNone must be the same value");
|
||||||
|
|
||||||
|
#define MGP5_C0_STUB(what) \
|
||||||
|
do { \
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnimplementedWireCodec, \"%s\"} - P5 package w1 has not landed " \
|
||||||
|
"this yet; c0 shipped the signature only", \
|
||||||
|
what); \
|
||||||
|
std::abort(); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
void SegmentTable::Install(SegmentId, SegmentView) { MGP5_C0_STUB("SegmentTable::Install"); }
|
||||||
|
|
||||||
|
SegmentView SegmentTable::Get(SegmentId) const { MGP5_C0_STUB("SegmentTable::Get"); }
|
||||||
|
|
||||||
|
const void* SegmentTable::Resolve(Uint32, Uint64, Uint64) const {
|
||||||
|
MGP5_C0_STUB("SegmentTable::Resolve");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SegmentTable::InstallProcessResolver() { MGP5_C0_STUB("SegmentTable::InstallProcessResolver"); }
|
||||||
|
|
||||||
|
void SegmentTable::UninstallProcessResolver() {
|
||||||
|
MGP5_C0_STUB("SegmentTable::UninstallProcessResolver");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOT a stub: the two Fatal helpers are the one thing every package needs on day one, and
|
||||||
|
// a Fatal that is itself unimplemented would report the wrong failure.
|
||||||
|
void WireProtocolFatal(const char* what, const char* detail) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : "");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what,
|
||||||
|
static_cast<unsigned long long>(got), static_cast<unsigned long long>(expected));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckBlobIsHonest(MG_Pipe::MGPWireOp, const MG_Pipe::MGPBlobRef&, const SegmentTable&) {
|
||||||
|
MGP5_C0_STUB("CheckBlobIsHonest");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan&) { MGP5_C0_STUB("CheckHostSpanIsHonest"); }
|
||||||
|
|
||||||
|
PipeWireEncoder::PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
|
||||||
|
Transport::RingProducer* stage, SegmentTable* segments)
|
||||||
|
: m_control(control), m_cmd(cmd), m_stage(stage), m_segments(segments) {}
|
||||||
|
|
||||||
|
Bool PipeWireEncoder::Valid() const { return m_control != nullptr && m_cmd != nullptr; }
|
||||||
|
|
||||||
|
MG_Pipe::MGPBlobRef PipeWireEncoder::StageBytes(const void*, Uint64) {
|
||||||
|
MGP5_C0_STUB("PipeWireEncoder::StageBytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 PipeWireEncoder::EncodeRecord(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64) {
|
||||||
|
MGP5_C0_STUB("PipeWireEncoder::EncodeRecord");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PipeWireEncoder::Publish() { MGP5_C0_STUB("PipeWireEncoder::Publish"); }
|
||||||
|
|
||||||
|
Uint64 PipeWireEncoder::EmitSeq() const { return m_emitSeq; }
|
||||||
|
|
||||||
|
Uint64 PipeWireEncoder::MaxRecordBytesSeen() const { return m_maxRecordBytes; }
|
||||||
|
|
||||||
|
PipeWireDecoder::PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
|
||||||
|
ReplySink* replies)
|
||||||
|
: m_control(control), m_segments(segments), m_replies(replies) {}
|
||||||
|
|
||||||
|
Bool PipeWireDecoder::Valid() const { return m_control != nullptr && m_segments != nullptr; }
|
||||||
|
|
||||||
|
Bool PipeWireDecoder::DecodeAndApply(const Transport::RingRecordView&) {
|
||||||
|
MGP5_C0_STUB("PipeWireDecoder::DecodeAndApply");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 PipeWireDecoder::AppliedSeq() const { return m_applySeq; }
|
||||||
|
|
||||||
|
#undef MGP5_C0_STUB
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Wire
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// G3: the MGPipe record codec. Owner: package w1.
|
||||||
|
//
|
||||||
|
// This header is the CONTRACT (MG_Remote/CONTRACT-P5.md) in C++ form; P5's c0 package wrote
|
||||||
|
// it so the other seven could compile on day one against signatures that cannot then move
|
||||||
|
// under them. Every body below is a named Fatal until w1 lands the real one.
|
||||||
|
//
|
||||||
|
// WHAT THIS LAYER IS, AND WHAT IT IS NOT
|
||||||
|
//
|
||||||
|
// It turns one MGPipe call into bytes in SEG_CMD (+ SEG_STAGE), and bytes back into ONE CALL
|
||||||
|
// OF AN EXISTING MGPipeApply* FREE FUNCTION. It owns NO semantics: MG_Pipe/PipeApply.cpp is
|
||||||
|
// not edited by this package, and a decoder arm that "handles" a record itself rather than
|
||||||
|
// delegating is a review failure (R-4's rule, one level down).
|
||||||
|
//
|
||||||
|
// THE FIVE HONESTY RULES (R-2), because they are what make `inproc` worth running at all.
|
||||||
|
// In the same address space every shortcut works: MGHostSpan::Ptr dereferences, a blobref
|
||||||
|
// whose Offset is a host address resolves, and MGPipeApplyMapPersistent's return value is a
|
||||||
|
// usable pointer. So the codec is held to the SPAWN rules even when it does not need to be:
|
||||||
|
// 1. encoder writes MGHostSpan::Ptr == nullptr and points Seg/Offset at SEG_STAGE;
|
||||||
|
// 2. encoder fills a real Seg, a real in-segment Offset and a NON-ZERO Size for every
|
||||||
|
// MGPBlobRef that carries content;
|
||||||
|
// 3. decoder Fatal{ProtocolCorruption} on: Ptr != nullptr; a content record with
|
||||||
|
// Blob.Size == 0; Size != 0 with Seg == kSegNone; Offset + Size past the segment;
|
||||||
|
// 4. MGPipeApplyMapPersistent returns nullptr under split (R-6; b1's half);
|
||||||
|
// 5. with MOBILEGL_IPC_AUDIT=1 the server fills a retired record's SEG_STAGE bytes with
|
||||||
|
// 0xDD, so an implementation that kept a pointer past apply reads 0xDD next frame.
|
||||||
|
//
|
||||||
|
// SEQ. The record ordinal IS the sequence number and IS the reply-slot id (R-3): there is no
|
||||||
|
// per-record seq field on the wire (ARCHITECTURE.md:124) and no second id space. Seq is
|
||||||
|
// 1-based so that 0 can mean "nothing encoded". A kRecPad wrap filler DOES NOT ADVANCE SEQ -
|
||||||
|
// both sides must skip it before counting, or every ring wrap offsets the two sides'
|
||||||
|
// numbering permanently and nothing checksums it (R-9, Ring.h's header).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#include "../Transport/Ring.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Wire {
|
||||||
|
|
||||||
|
// ---- table 0: the segment id space -------------------------------------------------
|
||||||
|
//
|
||||||
|
// The SAME VALUES as Protocol::SegmentKind (protocol.fbs:36-44); PipeWireCodec.cpp
|
||||||
|
// static_asserts the two agree, which is the only place the flatbuffers header and this
|
||||||
|
// enum meet. 0 is ALWAYS "no segment" and is never a real segment id, which is what lets
|
||||||
|
// MGPBlobRef{Seg == 0, Size != 0} be a detectable fault rather than a legal shape.
|
||||||
|
enum SegmentId : Uint32 {
|
||||||
|
kSegNone = 0,
|
||||||
|
kSegCmd = 1, // client-owned command ring (RingControl + records)
|
||||||
|
kSegStage = 2, // client-owned bulk staging: every blob and every var-tail's bytes
|
||||||
|
kSegReply = 3, // server-owned reply pool, addressed seq % slots (R-3)
|
||||||
|
kSegEvent = 4, // server-owned event ring (the reverse channel)
|
||||||
|
kSegShadow = 5, // client-owned per-object shadow (P8+)
|
||||||
|
kSegAdopt = 6, // server-owned adopted store, client RW (P11)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seq is 1-based. 0 is "no record", never a valid reply-slot id.
|
||||||
|
inline constexpr Uint64 kInvalidSeq = 0;
|
||||||
|
|
||||||
|
// One mapped segment as this ROLE sees it. Two roles in one process have two different
|
||||||
|
// SegmentTables over the same memory on purpose: a client that can resolve SEG_REPLY as
|
||||||
|
// if it owned it is the inproc cheat R-2 exists to kill.
|
||||||
|
struct SegmentView {
|
||||||
|
void* Base = nullptr;
|
||||||
|
Uint64 Size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the per-role segment table, and the process resolver hook ---------------------
|
||||||
|
//
|
||||||
|
// gMGPipeSegmentResolver (MG_Pipe/MGPipeHostSpan.h:47) is a plain non-atomic inline
|
||||||
|
// variable and there is exactly ONE of it per process, so under inproc the two roles
|
||||||
|
// cannot both install their own into it. TABLE 3's ruling: the resolver is installed by
|
||||||
|
// the SERVER role only, before the apply thread starts, and the client never resolves a
|
||||||
|
// span at all (it only ever writes Ptr = nullptr). Install() therefore takes the role.
|
||||||
|
class SegmentTable {
|
||||||
|
public:
|
||||||
|
void Install(SegmentId seg, SegmentView view);
|
||||||
|
SegmentView Get(SegmentId seg) const;
|
||||||
|
|
||||||
|
// Bounds-checked resolve. Returns nullptr when seg is unknown, size is 0, or
|
||||||
|
// offset + size runs past the segment; the CALLER escalates that to
|
||||||
|
// Fatal{ProtocolCorruption} (R-2.3) rather than this returning into a Fatal, so a
|
||||||
|
// unit test can exercise the arithmetic without dying.
|
||||||
|
const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const;
|
||||||
|
|
||||||
|
// Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if
|
||||||
|
// a resolver is already installed, because two roles racing on one inline variable is
|
||||||
|
// the failure this function exists to make loud.
|
||||||
|
void InstallProcessResolver();
|
||||||
|
static void UninstallProcessResolver();
|
||||||
|
|
||||||
|
private:
|
||||||
|
SegmentView m_views[kSegAdopt + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the four Fatal arms, worded once ----------------------------------------------
|
||||||
|
//
|
||||||
|
// One function so encoder, decoder and every package's own bounds check produce the SAME
|
||||||
|
// log line. `what` is the record or field; `detail` is the number that was wrong.
|
||||||
|
[[noreturn]] void WireProtocolFatal(const char* what, const char* detail);
|
||||||
|
[[noreturn]] void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected);
|
||||||
|
|
||||||
|
// R-2.3 arms 1-4 over one record's blobref. Split only; a monolith emission is exempt by
|
||||||
|
// construction because it never reaches this layer.
|
||||||
|
void CheckBlobIsHonest(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
|
||||||
|
const SegmentTable& segments);
|
||||||
|
// R-2.3 arm for MGHostSpan. P5's reduced path should produce ZERO host spans
|
||||||
|
// (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are both 0 in P5, table 0), so this
|
||||||
|
// firing at all is a finding, not just a corruption check.
|
||||||
|
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span);
|
||||||
|
|
||||||
|
// ---- encoder -----------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Not thread safe: one encoder per client context, driven by the GL thread, by
|
||||||
|
// construction (SPSC is the ring's contract too).
|
||||||
|
class PipeWireEncoder {
|
||||||
|
public:
|
||||||
|
PipeWireEncoder() = default;
|
||||||
|
PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
|
||||||
|
Transport::RingProducer* stage, SegmentTable* segments);
|
||||||
|
|
||||||
|
Bool Valid() const;
|
||||||
|
|
||||||
|
// Copies `size` bytes into SEG_STAGE and returns the blobref that names them:
|
||||||
|
// {Seg = kSegStage, Offset = in-segment byte offset, Size = size}. R-2.2 - Size is
|
||||||
|
// NEVER 0 for a content blob, and a 0-size call is a programming error that Fatals
|
||||||
|
// rather than returning an empty ref, because "the record declared no blob" and "the
|
||||||
|
// record declared an empty blob" must not be spelled the same way on a wire.
|
||||||
|
//
|
||||||
|
// The bytes are valid until retiredSeq passes the record that names them (R-11).
|
||||||
|
MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size);
|
||||||
|
|
||||||
|
// Writes one record: header (op, MGPipeCallFlagsFor(op), total size), then the fixed
|
||||||
|
// payload, then the variable tail. Returns the record's SEQ, which is also its
|
||||||
|
// reply-slot id (R-3), or kInvalidSeq if the ring refused it.
|
||||||
|
//
|
||||||
|
// A record larger than RingProducer::MaxRecordBytes() is Fatal{RingOverrun}, NOT a
|
||||||
|
// wait: R-10 says P5 does no chunking and must instead PROVE it never needs any, so
|
||||||
|
// this is where the proof fails loudly if it is wrong. MaxRecordBytesSeen() is the
|
||||||
|
// counter that feeds that proof into MEASUREMENTS.
|
||||||
|
Uint64 EncodeRecord(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||||
|
const void* varTail = nullptr, Uint64 varTailBytes = 0);
|
||||||
|
|
||||||
|
// Release-stores the head cursor, then rings the consumer doorbell IF PARKED. The
|
||||||
|
// order is pinned by RingTest.cpp:446 and must not be swapped: notify-then-publish
|
||||||
|
// loses the wakeup.
|
||||||
|
void Publish();
|
||||||
|
|
||||||
|
// The highest seq this encoder has produced. The verb barrier (R-1) waits for
|
||||||
|
// RingControl::appliedSeq to reach it.
|
||||||
|
Uint64 EmitSeq() const;
|
||||||
|
|
||||||
|
// R-10's proof obligation: the largest single record this encoder has written.
|
||||||
|
Uint64 MaxRecordBytesSeen() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Transport::RingControl* m_control = nullptr;
|
||||||
|
Transport::RingProducer* m_cmd = nullptr;
|
||||||
|
Transport::RingProducer* m_stage = nullptr;
|
||||||
|
SegmentTable* m_segments = nullptr;
|
||||||
|
Uint64 m_emitSeq = kInvalidSeq;
|
||||||
|
Uint64 m_maxRecordBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- decoder -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Where a kReplySlot answer goes. Declared HERE and not in Server/ so the codec does not
|
||||||
|
// depend on the server session: the decoder's job ends at "produce the answer bytes".
|
||||||
|
//
|
||||||
|
// The slot is addressed seq % slots and the server writes the seq back into the slot
|
||||||
|
// header for self-check (table 0's slot header row). Status: 0 = OK, 1 = DECLINED,
|
||||||
|
// 2 = ERROR. DECLINED IS A REAL ANSWER, not a failure - it is how MapPersistent says
|
||||||
|
// nullptr (R-6) and how the four Bool acceptance entry points say false (R-5).
|
||||||
|
class ReplySink {
|
||||||
|
public:
|
||||||
|
virtual ~ReplySink() = default;
|
||||||
|
static constexpr Int32 kStatusOk = 0;
|
||||||
|
static constexpr Int32 kStatusDeclined = 1;
|
||||||
|
static constexpr Int32 kStatusError = 2;
|
||||||
|
virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not thread safe: one decoder on the apply thread, by construction.
|
||||||
|
class PipeWireDecoder {
|
||||||
|
public:
|
||||||
|
PipeWireDecoder() = default;
|
||||||
|
PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
|
||||||
|
ReplySink* replies);
|
||||||
|
|
||||||
|
Bool Valid() const;
|
||||||
|
|
||||||
|
// Decodes ONE record and calls the matching MGPipeApply* free function.
|
||||||
|
//
|
||||||
|
// TWO BOUNDS CHECKS, NOT ONE. The generated MGP_WIRE_CHECK_BOUNDS only proves
|
||||||
|
// `size >= sizeof(MGPWireRec_X)` - IT CANNOT SEE THE TAIL, so a record declaring
|
||||||
|
// Count = 4000 while carrying 8 bytes passes it today. The decoder must recompute the
|
||||||
|
// total from the declared count(s) and require it to EQUAL MGPWireRecHeader::Size.
|
||||||
|
// The three double-tailed shapes are SetShaderBuffers (MGPBufferRange[Count] then
|
||||||
|
// MGHostSpan[HostSpanCount]), SetStreamOutputTargets (MGPBufferRange[Count] then
|
||||||
|
// Uint32[Count]) and DrawVbo (MGPDrawRange[NumDraws] then a conditional MGHostSpan).
|
||||||
|
//
|
||||||
|
// Returns whether the record was applied. False is reserved for a record this build
|
||||||
|
// deliberately does not implement; a MALFORMED record never returns, it Fatals.
|
||||||
|
//
|
||||||
|
// A kRecPad record must be skipped by the CALLER before this is reached; passing one
|
||||||
|
// here Fatals, because a pad that reached the decoder has already been counted.
|
||||||
|
Bool DecodeAndApply(const Transport::RingRecordView& record);
|
||||||
|
|
||||||
|
// Advanced by exactly one per applied non-pad record. P5 FORBIDS BATCHING IT (R-9):
|
||||||
|
// the verb barrier's waiter reads it, and a batched watermark makes the client wait
|
||||||
|
// for records the server has not run.
|
||||||
|
Uint64 AppliedSeq() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Transport::RingControl* m_control = nullptr;
|
||||||
|
SegmentTable* m_segments = nullptr;
|
||||||
|
ReplySink* m_replies = nullptr;
|
||||||
|
Uint64 m_applySeq = kInvalidSeq;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Wire
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -33,9 +34,31 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
BufferObject::BufferObject(Uint externalIndex)
|
BufferObject::BufferObject(Uint externalIndex)
|
||||||
: m_externalIndex(externalIndex), m_size(0), m_usage(BufferUsage::StaticDraw), m_isMapped(false),
|
: m_externalIndex(externalIndex), m_size(0), m_usage(BufferUsage::StaticDraw), m_isMapped(false),
|
||||||
m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} {}
|
m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P3a D-A2: a resource EXISTS before anything can name it, so resource_create is
|
||||||
|
// emitted from the constructor and carries no storage - the store is defined lazily
|
||||||
|
// by the first respecify and every backend already tolerates a resource with none.
|
||||||
|
// The handle itself is minted whatever the subsystem bitmask says, because
|
||||||
|
// set_vertex_buffers names this buffer by handle out of a different subsystem.
|
||||||
|
MG_Pipe::MGPipeMintResourceHandle(*this);
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) MG_Pipe::MGPipeEmitResourceCreate(*this);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
BufferObject::~BufferObject() {
|
BufferObject::~BufferObject() {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P3a D-L: the buffer's death crosses as resource_destroy, which is the catalogue
|
||||||
|
// call for it - no seventh NotifyStateObjectDestroyed raiser is added, because that
|
||||||
|
// header exists for kinds that have no such call. The emit-then-free ORDER is fixed
|
||||||
|
// inside the helper and is not negotiable.
|
||||||
|
// The answer is the helper's LATCH - "was resource_create emitted for this buffer" -
|
||||||
|
// not a second reading of MGPipeResourceSubsystemEnabled(): a buffer constructed
|
||||||
|
// while a backend's table was registered and destroyed after it was unregistered has
|
||||||
|
// a pipe record to drop and no legacy backend object, and one constructed the other
|
||||||
|
// way round has the opposite, so the create's answer is the only one that pairs.
|
||||||
|
if (MG_Pipe::MGPipeEmitResourceDestroyAndFree(*this)) return;
|
||||||
|
#endif
|
||||||
if (m_resource.Backend() && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) {
|
if (m_resource.Backend() && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) {
|
||||||
g_bufferBackendOps->OnDestroy(m_resource.ReleaseBackend());
|
g_bufferBackendOps->OnDestroy(m_resource.ReleaseBackend());
|
||||||
}
|
}
|
||||||
@@ -43,6 +66,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void BufferObject::NotifyRespecify() {
|
void BufferObject::NotifyRespecify() {
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
MG_Pipe::MGPipeEmitResourceRespecify(*this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (g_bufferBackendOps && g_bufferBackendOps->Respecify) {
|
if (g_bufferBackendOps && g_bufferBackendOps->Respecify) {
|
||||||
g_bufferBackendOps->Respecify(*this);
|
g_bufferBackendOps->Respecify(*this);
|
||||||
}
|
}
|
||||||
@@ -50,8 +80,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
if (size == 0) return;
|
if (size == 0) return;
|
||||||
m_hasDefinedContent = true;
|
m_hasDefinedContent = true;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
MG_Pipe::MGPipeEmitResourceSubData(*this, offset, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
||||||
g_bufferBackendOps->SubData(*this, offset, size);
|
g_bufferBackendOps->SubData(*this, offset, size);
|
||||||
}
|
}
|
||||||
@@ -59,8 +96,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
if (range.start >= range.end) return;
|
if (range.start >= range.end) return;
|
||||||
m_hasDefinedContent = true;
|
m_hasDefinedContent = true;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
static_assert(sizeof(appAccess.GetRaw()) <= sizeof(Uint32),
|
||||||
|
"MGPFlushRange::AccessFlags is a Uint32 and carries the application's "
|
||||||
|
"real Flags<BufferMappingAccessBit>, unnormalised");
|
||||||
|
MG_Pipe::MGPipeEmitResourceFlushRange(*this, range.start, range.end - range.start,
|
||||||
|
static_cast<Uint32>(appAccess.GetRaw()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
||||||
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
||||||
}
|
}
|
||||||
@@ -73,6 +121,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// undefined store to "has content" - that would cost the next orphaning
|
// undefined store to "has content" - that would cost the next orphaning
|
||||||
// respecification a full-size upload of bytes the application never wrote.
|
// respecification a full-size upload of bytes the application never wrote.
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_hasDefinedContent = true;
|
m_hasDefinedContent = true;
|
||||||
@@ -80,6 +129,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// The write already landed in coherent GPU memory; the backend has no separate
|
// The write already landed in coherent GPU memory; the backend has no separate
|
||||||
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NotifySubData(offset, size);
|
NotifySubData(offset, size);
|
||||||
@@ -183,6 +233,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (m_size < kLargeBufferAdoptBytes) return;
|
if (m_size < kLargeBufferAdoptBytes) return;
|
||||||
if (m_resource.IsGpuResident()) return;
|
if (m_resource.IsGpuResident()) return;
|
||||||
if (m_isMapped) return;
|
if (m_isMapped) return;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
if (void* base = MG_Pipe::MGPipeEmitMapPersistent(*this)) m_resource.AdoptPersistentMap(base);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return;
|
||||||
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) {
|
||||||
m_resource.AdoptPersistentMap(base);
|
m_resource.AdoptPersistentMap(base);
|
||||||
@@ -302,6 +358,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
data.size, m_size);
|
data.size, m_size);
|
||||||
Memcpy(m_resource.Bytes() + atOffset, data.data, data.size);
|
Memcpy(m_resource.Bytes() + atOffset, data.data, data.size);
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferObject::MarkGpuWritten() {
|
void BufferObject::MarkGpuWritten() {
|
||||||
@@ -314,6 +371,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Cleared unconditionally: without a readback op the shadow can never catch up,
|
// Cleared unconditionally: without a readback op the shadow can never catch up,
|
||||||
// and retrying on every subsequent read would only repeat the same no-op.
|
// and retrying on every subsequent read would only repeat the same no-op.
|
||||||
m_gpuWritePending = false;
|
m_gpuWritePending = false;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
// The answer comes back through the reverse channel's OnBufferWriteback, which
|
||||||
|
// resolves this handle to this object and writes the shadow before the server
|
||||||
|
// bumps its mutation epoch. In monolith the whole sequence is synchronous inside
|
||||||
|
// the applier, so the caller sees the reconciled shadow on return exactly as it
|
||||||
|
// does today.
|
||||||
|
MG_Pipe::MGPipeEmitResourceReadback(*this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
|
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -362,10 +430,35 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// NotifyContentWrite on a resident store only bumps the serial: the backend has no
|
// NotifyContentWrite on a resident store only bumps the serial: the backend has no
|
||||||
// separate copy to sync, so no transfer op runs.
|
// separate copy to sync, so no transfer op runs.
|
||||||
void BufferObject::LandBytesIntoResidentStore(SizeT offset, DataPtr bytes) {
|
void BufferObject::LandBytesIntoResidentStore(SizeT offset, DataPtr bytes) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// buffer_subdata_resident stays NULLABLE and stays asymmetric: one backend
|
||||||
|
// deliberately does not implement it, and the frontend checks the pipe table exactly
|
||||||
|
// as it checks the op table it replaces, so a backend without it keeps the legacy
|
||||||
|
// ordered in-place host write below.
|
||||||
|
if (bytes.size > 0 && MG_Pipe::MGPipeResourceSubsystemEnabled() &&
|
||||||
|
MG_Pipe::MGPipeResourceOpsHaveSubDataResident()) {
|
||||||
|
MG_Pipe::MGPipeEmitBufferSubDataResident(*this, offset, bytes.data, bytes.size);
|
||||||
|
m_hasDefinedContent = true;
|
||||||
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No resident op: the write lands in place below, after retiring the GPU writes this
|
||||||
|
// store is known to be waiting on - which is the same answer, and the same code, a
|
||||||
|
// backend with a null ResidentSubData gets today.
|
||||||
|
if (MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
SyncGpuWrites();
|
||||||
|
Memcpy(m_resource.Bytes() + offset, bytes.data, bytes.size);
|
||||||
|
NotifyContentWrite(offset, bytes.size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (bytes.size > 0 && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
if (bytes.size > 0 && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
||||||
g_bufferBackendOps->ResidentSubData(*this, offset, bytes);
|
g_bufferBackendOps->ResidentSubData(*this, offset, bytes);
|
||||||
m_hasDefinedContent = true;
|
m_hasDefinedContent = true;
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
|
MGP_NOTE_AGGREGATE(BufferChange);
|
||||||
m_gpuWritePending = true;
|
m_gpuWritePending = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -394,7 +487,16 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// op the landing would memcpy the expansion into the mapping the loop below
|
// op the landing would memcpy the expansion into the mapping the loop below
|
||||||
// fills in place anyway, so a whole-arena clear would allocate a whole arena
|
// fills in place anyway, so a whole-arena clear would allocate a whole arena
|
||||||
// for nothing.
|
// for nothing.
|
||||||
if (m_resource.IsGpuResident() && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) {
|
if (m_resource.IsGpuResident() &&
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The same question, asked of whichever table owns the family in this build.
|
||||||
|
(MG_Pipe::MGPipeResourceSubsystemEnabled()
|
||||||
|
? MG_Pipe::MGPipeResourceOpsHaveSubDataResident()
|
||||||
|
: (g_bufferBackendOps && g_bufferBackendOps->ResidentSubData))
|
||||||
|
#else
|
||||||
|
g_bufferBackendOps && g_bufferBackendOps->ResidentSubData
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
Vector<Uint8> expanded(size);
|
Vector<Uint8> expanded(size);
|
||||||
if (pattern.size == 1) {
|
if (pattern.size == 1) {
|
||||||
Memset(expanded.data(), *static_cast<const Uint8*>(pattern.data), size);
|
Memset(expanded.data(), *static_cast<const Uint8*>(pattern.data), size);
|
||||||
@@ -496,6 +598,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (m_isMapped) {
|
if (m_isMapped) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (m_size != 0 && MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this);
|
||||||
|
if (pushedBase == nullptr) return false;
|
||||||
|
m_resource.AdoptPersistentMap(pushedBase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -540,6 +650,16 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// returning; AdoptPersistentMap then releases the shadow. Falls back to the
|
// returning; AdoptPersistentMap then releases the shadow. Falls back to the
|
||||||
// shadow when the backend declines (returns null). Only attempted once - the
|
// shadow when the backend declines (returns null). Only attempted once - the
|
||||||
// storage is immutable and outlives unmap/remap.
|
// storage is immutable and outlives unmap/remap.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
|
||||||
|
!(access & BufferMappingAccessBit::FlushExplicit) &&
|
||||||
|
MG_Pipe::MGPipeResourceSubsystemEnabled()) {
|
||||||
|
if (void* pushedBase = MG_Pipe::MGPipeEmitMapPersistent(*this)) {
|
||||||
|
m_resource.AdoptPersistentMap(pushedBase);
|
||||||
|
}
|
||||||
|
return m_resource.Bytes() + range.start;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
|
if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) &&
|
||||||
!(access & BufferMappingAccessBit::FlushExplicit) && g_bufferBackendOps &&
|
!(access & BufferMappingAccessBit::FlushExplicit) && g_bufferBackendOps &&
|
||||||
g_bufferBackendOps->AcquirePersistentMap) {
|
g_bufferBackendOps->AcquirePersistentMap) {
|
||||||
|
|||||||
@@ -64,7 +64,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool ValidateName(Uint index) const;
|
Bool ValidateName(Uint index) const;
|
||||||
Bool ValidateBufferObject(Uint index) const;
|
Bool ValidateBufferObject(Uint index) const;
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 brief D4: "did the contents of ANY buffer object move". One counter for every
|
||||||
|
// BufferObject ++m_changeSerial site, which is what NEW_VERTEX_BUFFERS /
|
||||||
|
// NEW_INDEX_BUFFER / NEW_CONST_BUFFERS / NEW_SHADER_BUFFERS / NEW_SO_TARGETS all
|
||||||
|
// shutter on in P2 - five bits over one aggregate until P3b splits them.
|
||||||
|
void NoteBufferChanged() { ++m_anyBufferChangeGeneration; }
|
||||||
|
Uint64 GetAnyBufferChangeGeneration() const { return m_anyBufferChangeGeneration; }
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Uint64 m_anyBufferChangeGeneration = 0;
|
||||||
|
#endif
|
||||||
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
|
UnorderedMap<Uint, SharedPtr<BufferObject>> m_bufferObjects;
|
||||||
IndexGenerator<Uint> m_indexGenerator;
|
IndexGenerator<Uint> m_indexGenerator;
|
||||||
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
|
Array<BindingSlot<BufferObject>, GlobalBufferTargets.size()> m_bindingSlots;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State {
|
namespace MobileGL::MG_State {
|
||||||
void Init() {
|
void Init() {
|
||||||
@@ -213,6 +214,12 @@ namespace MobileGL::MG_State {
|
|||||||
current.intValue[component] = static_cast<Int32>(value[component]);
|
current.intValue[component] = static_cast<Int32>(value[component]);
|
||||||
current.uintValue[component] = static_cast<Uint32>(value[component]);
|
current.uintValue[component] = static_cast<Uint32>(value[component]);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// The two views above are CONVERSIONS, not bit copies, so which one was written
|
||||||
|
// is part of the value; set_vertex_attrib_defaults carries it.
|
||||||
|
m_currentVertexAttributeClasses[index] = kVertexAttribValueClassFloat;
|
||||||
|
#endif
|
||||||
|
MGP_NOTE_AGGREGATE(VertexAttribDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
|
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
|
||||||
@@ -227,6 +234,10 @@ namespace MobileGL::MG_State {
|
|||||||
current.floatValue[component] = static_cast<Float>(value[component]);
|
current.floatValue[component] = static_cast<Float>(value[component]);
|
||||||
current.uintValue[component] = static_cast<Uint32>(value[component]);
|
current.uintValue[component] = static_cast<Uint32>(value[component]);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_currentVertexAttributeClasses[index] = kVertexAttribValueClassInt;
|
||||||
|
#endif
|
||||||
|
MGP_NOTE_AGGREGATE(VertexAttribDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
|
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
|
||||||
@@ -241,6 +252,10 @@ namespace MobileGL::MG_State {
|
|||||||
current.floatValue[component] = static_cast<Float>(value[component]);
|
current.floatValue[component] = static_cast<Float>(value[component]);
|
||||||
current.intValue[component] = static_cast<Int32>(value[component]);
|
current.intValue[component] = static_cast<Int32>(value[component]);
|
||||||
}
|
}
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
m_currentVertexAttributeClasses[index] = kVertexAttribValueClassUint;
|
||||||
|
#endif
|
||||||
|
MGP_NOTE_AGGREGATE(VertexAttribDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
|
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
|
||||||
|
|||||||
@@ -30,10 +30,26 @@ namespace MobileGL {
|
|||||||
void Init();
|
void Init();
|
||||||
|
|
||||||
namespace GLState {
|
namespace GLState {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// MGPAttribValue::ValueClass' encoding (MG_Pipe/MGPipeTypes.h documents the order
|
||||||
|
// "Float | Int | Uint | Double"). It lives here rather than in MG_Pipe because the
|
||||||
|
// FRONTEND is the only thing that knows which of the three views below a value was
|
||||||
|
// written through - the other two are numeric conversions of it - and MG_Pipe has
|
||||||
|
// no enum for the field yet. If package A introduces one, this becomes its alias.
|
||||||
|
inline constexpr Uint32 kVertexAttribValueClassFloat = 0;
|
||||||
|
inline constexpr Uint32 kVertexAttribValueClassInt = 1;
|
||||||
|
inline constexpr Uint32 kVertexAttribValueClassUint = 2;
|
||||||
|
#endif
|
||||||
|
|
||||||
struct CurrentVertexAttributeValue {
|
struct CurrentVertexAttributeValue {
|
||||||
Array<Float, 4> floatValue{0.f, 0.f, 0.f, 1.f};
|
Array<Float, 4> floatValue{0.f, 0.f, 0.f, 1.f};
|
||||||
Array<Int32, 4> intValue{0, 0, 0, 1};
|
Array<Int32, 4> intValue{0, 0, 0, 1};
|
||||||
Array<Uint32, 4> uintValue{0u, 0u, 0u, 1u};
|
Array<Uint32, 4> uintValue{0u, 0u, 0u, 1u};
|
||||||
|
// Three scalar arrays and NOTHING ELSE. MG_Backend/MGPipe/PipeInputs.cpp
|
||||||
|
// compares this storage with one memcmp and asserts that size, so a fourth
|
||||||
|
// member here is a build break in a file P2 package B does not own. The
|
||||||
|
// written-class discriminator set_vertex_attrib_defaults needs therefore
|
||||||
|
// lives beside the array on GLContext, not inside the value.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which of the three views above a shader input of a given GLSL type consumes.
|
// Which of the three views above a shader input of a given GLSL type consumes.
|
||||||
@@ -198,6 +214,64 @@ namespace MobileGL {
|
|||||||
Uint GetBoundProgramPipelineName() const { return m_boundProgramPipeline; }
|
Uint GetBoundProgramPipelineName() const { return m_boundProgramPipeline; }
|
||||||
const SharedPtr<ProgramPipelineObject>& GetBoundProgramPipeline() const;
|
const SharedPtr<ProgramPipelineObject>& GetBoundProgramPipeline() const;
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// ---- the aggregate generations (P2 brief D4) ----
|
||||||
|
//
|
||||||
|
// The bump points sit on OBJECTS - a VertexArrayObject, a TextureObject, a
|
||||||
|
// BufferObject - which have no back-pointer to the state container that owns
|
||||||
|
// them, so MGP_NOTE_AGGREGATE goes through MGPipeNoteAggregate, which finds
|
||||||
|
// the live context and lands here. This facade is the whole reason the
|
||||||
|
// objects need no back-pointer, and it is push-only so the pull build's
|
||||||
|
// GLContext is byte-identical (G1).
|
||||||
|
void NoteVaoAttributeChanged() { m_vertexArrayState.NoteAttributeChanged(); }
|
||||||
|
Uint64 GetAnyVaoAttributeGeneration() const {
|
||||||
|
return m_vertexArrayState.GetAnyAttributeGeneration();
|
||||||
|
}
|
||||||
|
void NoteFramebufferAttachmentChanged() { m_framebufferState.NoteAttachmentChanged(); }
|
||||||
|
Uint64 GetAnyFramebufferAttachmentGeneration() const {
|
||||||
|
return m_framebufferState.GetAnyAttachmentGeneration();
|
||||||
|
}
|
||||||
|
void NoteTextureContentChanged() { m_textureState.NoteTextureContentChanged(); }
|
||||||
|
Uint64 GetAnyTextureContentGeneration() const {
|
||||||
|
return m_textureState.GetAnyTextureContentGeneration();
|
||||||
|
}
|
||||||
|
void NoteTextureParamsChanged() { m_textureState.NoteTextureParamsChanged(); }
|
||||||
|
Uint64 GetAnyTextureParamsGeneration() const {
|
||||||
|
return m_textureState.GetAnyTextureParamsGeneration();
|
||||||
|
}
|
||||||
|
void NoteBufferChanged() { m_bufferState.NoteBufferChanged(); }
|
||||||
|
Uint64 GetAnyBufferChangeGeneration() const {
|
||||||
|
return m_bufferState.GetAnyBufferChangeGeneration();
|
||||||
|
}
|
||||||
|
// The sixth aggregate lives here rather than on a state container because
|
||||||
|
// the values it guards do too (m_currentVertexAttributes).
|
||||||
|
void NoteVertexAttribDefaultChanged() { ++m_anyVertexAttribDefaultGeneration; }
|
||||||
|
Uint64 GetAnyVertexAttribDefaultGeneration() const {
|
||||||
|
return m_anyVertexAttribDefaultGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which of the three views of m_currentVertexAttributes[index] the last
|
||||||
|
// glVertexAttrib* write filled DIRECTLY. The other two are NUMERIC
|
||||||
|
// conversions of it (SetCurrentVertexAttribute* below), not bit copies, so
|
||||||
|
// four words on a wire are not the value unless the class travels with them:
|
||||||
|
// glVertexAttrib4f(loc, 1.5f, ...) leaves 1 in intValue and 0x3FC00000 in
|
||||||
|
// floatValue. set_vertex_attrib_defaults carries this as MGPAttribValue's
|
||||||
|
// ValueClass so the applier can redo the conversion instead of memcpying one
|
||||||
|
// view into all three.
|
||||||
|
//
|
||||||
|
// It is kept BESIDE the array rather than inside CurrentVertexAttributeValue
|
||||||
|
// because that struct is mirrored into PipeInputs and compared there by a
|
||||||
|
// memcmp whose size assertion (MG_Backend/MGPipe/PipeInputs.cpp) is a file
|
||||||
|
// this package does not own - and because it need not be mirrored: the class
|
||||||
|
// only decides how to REBUILD the three views, so two writes that leave the
|
||||||
|
// three views identical rebuild identically whichever class they carried.
|
||||||
|
Uint32 GetCurrentVertexAttributeClass(Uint index) const {
|
||||||
|
return index < m_currentVertexAttributeClasses.size()
|
||||||
|
? m_currentVertexAttributeClasses[index]
|
||||||
|
: kVertexAttribValueClassFloat;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// RenderState
|
// RenderState
|
||||||
Uint GetRenderStateParametersVersion() const;
|
Uint GetRenderStateParametersVersion() const;
|
||||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||||
@@ -515,6 +589,11 @@ namespace MobileGL {
|
|||||||
Bool m_transformFeedbackPaused = false;
|
Bool m_transformFeedbackPaused = false;
|
||||||
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
|
||||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Uint64 m_anyVertexAttribDefaultGeneration = 0;
|
||||||
|
// Parallel to m_currentVertexAttributes; see GetCurrentVertexAttributeClass.
|
||||||
|
Array<Uint32, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributeClasses{};
|
||||||
|
#endif
|
||||||
Uint64 m_transformFeedbackGeneration = 0;
|
Uint64 m_transformFeedbackGeneration = 0;
|
||||||
// Source of the per-span ids above; never rolls back with an object switch.
|
// Source of the per-span ids above; never rolls back with an object switch.
|
||||||
Uint64 m_transformFeedbackNextGeneration = 0;
|
Uint64 m_transformFeedbackNextGeneration = 0;
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "FramebufferObject.h"
|
#include "FramebufferObject.h"
|
||||||
|
#include "MG_State/GLState/StateObjectDeathNotice.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
|
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
|
||||||
@@ -21,6 +23,27 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return s_nextFramebufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
return s_nextFramebufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
FramebufferObject::~FramebufferObject() {
|
||||||
|
// P4a D-I2: A FRAMEBUFFER HAS A HANDLE AND NO WIRE LIFETIME. PipeCalls.def carries
|
||||||
|
// resource_destroy and five delete_* rows and NO framebuffer delete, because a
|
||||||
|
// framebuffer is not a resource and is not a CSO - it is STATE, and
|
||||||
|
// set_framebuffer_state is the only call that names one - and the catalogue is closed,
|
||||||
|
// so P4a invents no row. The helper is therefore steps 2 and 3 only: the death notice,
|
||||||
|
// raised while the handle still resolves (this is the P2 step-e2 announcement that used
|
||||||
|
// to stand here alone - the last SharedPtr to this object dropping, not the glDelete*
|
||||||
|
// that only marks the name and leaves a still-bound object very much alive), and then
|
||||||
|
// the slot.
|
||||||
|
//
|
||||||
|
// What makes a dangling Fbo unreachable is the frontend's own
|
||||||
|
// MarkFramebufferObjectForDeletion path, which already rebinds any slot holding the
|
||||||
|
// victim to framebuffer 0; and a RECYCLED framebuffer handle can never be suppressed
|
||||||
|
// against its predecessor's record, because Fbo carries Gen and Gen is inside the
|
||||||
|
// record's ContentHash.
|
||||||
|
MG_Pipe::MGPipeEmitFramebufferDestroyAndFree(m_lifetimeId);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// FramebufferAttachmentObject
|
// FramebufferAttachmentObject
|
||||||
FramebufferAttachmentObject::FramebufferAttachmentObject(
|
FramebufferAttachmentObject::FramebufferAttachmentObject(
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget textureUploadTarget, Int level,
|
const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget textureUploadTarget, Int level,
|
||||||
@@ -190,6 +213,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (m_readBuffer == buf) return;
|
if (m_readBuffer == buf) return;
|
||||||
m_readBuffer = buf;
|
m_readBuffer = buf;
|
||||||
++m_objectVersion;
|
++m_objectVersion;
|
||||||
|
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint FramebufferObject::GetExternalIndex() const {
|
Uint FramebufferObject::GetExternalIndex() const {
|
||||||
@@ -201,6 +225,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (member == value) return; \
|
if (member == value) return; \
|
||||||
member = value; \
|
member = value; \
|
||||||
++m_objectVersion; \
|
++m_objectVersion; \
|
||||||
|
MGP_NOTE_AGGREGATE(FramebufferAttachment); \
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultWidth, m_defaultWidth, Int)
|
MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER(DefaultWidth, m_defaultWidth, Int)
|
||||||
@@ -213,5 +238,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void FramebufferObject::BumpAttachmentVersion(FramebufferAttachmentType type) {
|
void FramebufferObject::BumpAttachmentVersion(FramebufferAttachmentType type) {
|
||||||
++m_attachmentVersions[static_cast<SizeT>(type)];
|
++m_attachmentVersions[static_cast<SizeT>(type)];
|
||||||
++m_objectVersion;
|
++m_objectVersion;
|
||||||
|
MGP_NOTE_AGGREGATE(FramebufferAttachment);
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ namespace MobileGL {
|
|||||||
Array<Uint16, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>;
|
Array<Uint16, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>;
|
||||||
|
|
||||||
FramebufferObject(Uint externalIndex);
|
FramebufferObject(Uint externalIndex);
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 step e2. Out of line, and declared only where there is a notice to raise:
|
||||||
|
// in a pull build this class keeps its implicit destructor, which is what keeps
|
||||||
|
// the pull build's symbol set byte-for-byte the pre-P2 one (G1).
|
||||||
|
~FramebufferObject();
|
||||||
|
#endif
|
||||||
|
|
||||||
void AttachTexture(FramebufferAttachmentType type, const SharedPtr<ITextureObject>& texture,
|
void AttachTexture(FramebufferAttachmentType type, const SharedPtr<ITextureObject>& texture,
|
||||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown, int level = 0,
|
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown, int level = 0,
|
||||||
|
|||||||
@@ -25,7 +25,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool ValidateName(Uint index) const;
|
Bool ValidateName(Uint index) const;
|
||||||
Bool ValidateFramebufferObject(Uint index) const;
|
Bool ValidateFramebufferObject(Uint index) const;
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P2 brief D4: "did the attachment set or the default geometry of ANY framebuffer
|
||||||
|
// move". It does NOT cover a BIND - a bind writes a BindingSlot, not the object - so
|
||||||
|
// MGPipeTracker pairs this counter with the bound draw framebuffer identity, which
|
||||||
|
// is one extra load and keeps the bump points on the object where they belong.
|
||||||
|
void NoteAttachmentChanged() { ++m_anyAttachmentGeneration; }
|
||||||
|
Uint64 GetAnyAttachmentGeneration() const { return m_anyAttachmentGeneration; }
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Uint64 m_anyAttachmentGeneration = 0;
|
||||||
|
#endif
|
||||||
UnorderedMap<Uint, SharedPtr<FramebufferObject>> m_framebufferObjects;
|
UnorderedMap<Uint, SharedPtr<FramebufferObject>> m_framebufferObjects;
|
||||||
IndexGenerator<Uint> m_indexGenerator;
|
IndexGenerator<Uint> m_indexGenerator;
|
||||||
Array<BindingSlot<FramebufferObject>, static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount)>
|
Array<BindingSlot<FramebufferObject>, static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount)>
|
||||||
|
|||||||
@@ -408,6 +408,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Free constrained templates rather than members so the struct bodies above stay a verbatim
|
// Free constrained templates rather than members so the struct bodies above stay a verbatim
|
||||||
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
|
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
|
||||||
// struct changes its size, trips the assertion, and the message sends the author here.
|
// struct changes its size, trips the assertion, and the message sends the author here.
|
||||||
|
//
|
||||||
|
// THE SERIALIZER NOW EXISTS (P4a): MG_State/GLState/ProgramState/ProgramArtifactsCodec.
|
||||||
|
// {h,cpp}, beside this header rather than inside it so the check_include_closure.py
|
||||||
|
// "artifacts-header" probe stays untouched. It is two visitors over the tables below - a
|
||||||
|
// writer that appends to a Vector<Uint8> and a reader that consumes one - length-prefixed,
|
||||||
|
// little-endian, with a format-version word first and a MGL_LINKARTIFACTS_SIZE echo
|
||||||
|
// second, so a struct that gained a field and a codec that did not is a mismatch at READ
|
||||||
|
// time rather than a silent truncation. Adding a member to any struct above therefore
|
||||||
|
// means: add its VisitFields row here, update the sizeof number below, and bump
|
||||||
|
// kProgramArtifactsCodecVersion. `LinkArtifacts::program` stays the one deliberate
|
||||||
|
// omission, and the codec has no arm for it.
|
||||||
template <class Self, class V>
|
template <class Self, class V>
|
||||||
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
||||||
void VisitFields(Self& a, V&& v) {
|
void VisitFields(Self& a, V&& v) {
|
||||||
@@ -549,7 +560,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ---- trip wires ----
|
// ---- trip wires ----
|
||||||
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
||||||
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
|
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
|
||||||
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and its serializer when one exists), then update this number");
|
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
// The container-bearing structs have one size per standard library (std::string and
|
// The container-bearing structs have one size per standard library (std::string and
|
||||||
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
|
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
|
||||||
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
|
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
|
||||||
@@ -565,12 +576,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef MGL_LINKARTIFACTS_SIZE
|
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||||
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
|
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
|
||||||
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and its serializer when one exists), then update this number");
|
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
|
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
|
||||||
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and its serializer when one exists), then update this number");
|
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
|
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
|
||||||
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and its serializer when one exists), then update this number");
|
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
|
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
|
||||||
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and its serializer when one exists), then update this number");
|
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
#endif
|
#endif
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// ProgramArtifactsCodec.h. Compiled only under MOBILEGL_PIPE_PUSH (the root CMakeLists.txt
|
||||||
|
// appends it inside `if (MOBILEGL_PIPE_PUSH)`), so the pull build gains no symbol from it.
|
||||||
|
#include "ProgramArtifactsCodec.h"
|
||||||
|
|
||||||
|
#include <bit>
|
||||||
|
#include <cstring>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
namespace {
|
||||||
|
// Little-endian, stated rather than assumed. Every ABI MobileGL ships on is
|
||||||
|
// little-endian; the day one is not, this is a compile error and not a silently
|
||||||
|
// byte-swapped reflection table.
|
||||||
|
static_assert(std::endian::native == std::endian::little,
|
||||||
|
"the program-archive codec writes scalars in native order and MobileGL's "
|
||||||
|
"ABIs are little-endian; a big-endian target needs explicit byte order");
|
||||||
|
|
||||||
|
// ---- the four container shapes the archive is built out of ----
|
||||||
|
//
|
||||||
|
// Detected by SHAPE rather than by naming std::vector / ska::flat_hash_map, because
|
||||||
|
// MobileGL's aliases are not all std:: types (UnorderedMap is ska::flat_hash_map) and
|
||||||
|
// a codec that named them would stop compiling the day one is swapped. The order the
|
||||||
|
// arms are tested in is what makes them unambiguous: String before every container,
|
||||||
|
// maps before sets (a map has both key_type and mapped_type), fixed arrays before
|
||||||
|
// resizable ones.
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveString = std::same_as<T, String>;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveMap = requires {
|
||||||
|
typename T::key_type;
|
||||||
|
typename T::mapped_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveSet = requires { typename T::key_type; } && !ArchiveMap<T> && !ArchiveString<T>;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveFixedArray = requires { std::tuple_size<T>::value; };
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveVector = !ArchiveString<T> && !ArchiveFixedArray<T> && requires(T& t) {
|
||||||
|
t.resize(SizeT{0});
|
||||||
|
t.size();
|
||||||
|
t.begin();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveScalar = std::is_arithmetic_v<T> || std::is_enum_v<T>;
|
||||||
|
|
||||||
|
// The ONE hand-written arm, and it is hand-written because ProgramArtifacts.h gives it
|
||||||
|
// no VisitFields table: glslang::TIntermediate::TUniformInitializer is a plain
|
||||||
|
// aggregate that merely LOOKS like a glslang type (std::string + scalars + two
|
||||||
|
// std::vectors), which is exactly what ProgramTranslationCache.h audited it as when it
|
||||||
|
// decided the archive holds no glslang-owned memory. If a field is added there, this
|
||||||
|
// arm and the format version below both have to move.
|
||||||
|
using UniformInitializer = glslang::TIntermediate::TUniformInitializer;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveUniformInitializer = std::same_as<T, UniformInitializer>;
|
||||||
|
|
||||||
|
// ---- the writer ----
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void PutRaw(Vector<Uint8>& out, const T& value) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>);
|
||||||
|
const SizeT at = out.size();
|
||||||
|
out.resize(at + sizeof(T));
|
||||||
|
std::memcpy(out.data() + at, &value, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PutCount(Vector<Uint8>& out, SizeT count) {
|
||||||
|
PutRaw(out, static_cast<Uint64>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteValue(Vector<Uint8>& out, const T& value);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteSequence(Vector<Uint8>& out, const T& value) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
for (const auto& element : value) WriteValue(out, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteValue(Vector<Uint8>& out, const T& value) {
|
||||||
|
if constexpr (ArchiveScalar<T>) {
|
||||||
|
PutRaw(out, value);
|
||||||
|
} else if constexpr (ArchiveString<T>) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
const SizeT at = out.size();
|
||||||
|
out.resize(at + value.size());
|
||||||
|
if (!value.empty()) std::memcpy(out.data() + at, value.data(), value.size());
|
||||||
|
} else if constexpr (ArchiveMap<T>) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
for (const auto& entry : value) {
|
||||||
|
WriteValue(out, entry.first);
|
||||||
|
WriteValue(out, entry.second);
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveSet<T>) {
|
||||||
|
WriteSequence(out, value);
|
||||||
|
} else if constexpr (ArchiveFixedArray<T>) {
|
||||||
|
// No count: the width is part of the type, and writing one would let a reader
|
||||||
|
// believe a stream that disagrees with the struct.
|
||||||
|
for (const auto& element : value) WriteValue(out, element);
|
||||||
|
} else if constexpr (ArchiveVector<T>) {
|
||||||
|
WriteSequence(out, value);
|
||||||
|
} else if constexpr (ArchiveUniformInitializer<T>) {
|
||||||
|
WriteValue(out, value.name);
|
||||||
|
WriteValue(out, value.basicType);
|
||||||
|
WriteValue(out, value.vectorSize);
|
||||||
|
WriteValue(out, value.matrixCols);
|
||||||
|
WriteValue(out, value.matrixRows);
|
||||||
|
WriteValue(out, value.arraySize);
|
||||||
|
WriteValue(out, value.intValues);
|
||||||
|
WriteValue(out, value.floatValues);
|
||||||
|
} else {
|
||||||
|
// The archive's own structs: TypeFacts, ResourceReflection, XfbVarying. ONE
|
||||||
|
// table serves both directions, so a member added to any of them is carried by
|
||||||
|
// both halves of this codec the moment its VisitFields row is added - and a
|
||||||
|
// type with no table at all is a compile error here rather than a silently
|
||||||
|
// skipped field.
|
||||||
|
VisitFields(value, [&out](const char*, const auto& field) { WriteValue(out, field); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the reader ----
|
||||||
|
|
||||||
|
struct ReadCursor {
|
||||||
|
const Uint8* Bytes = nullptr;
|
||||||
|
SizeT Size = 0;
|
||||||
|
SizeT Pos = 0;
|
||||||
|
Bool Ok = true;
|
||||||
|
|
||||||
|
SizeT Remaining() const { return Size - Pos; }
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
Bool TakeRaw(ReadCursor& in, T& value) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>);
|
||||||
|
if (!in.Ok || in.Remaining() < sizeof(T)) {
|
||||||
|
in.Ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(&value, in.Bytes + in.Pos, sizeof(T));
|
||||||
|
in.Pos += sizeof(T);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A COUNT IS CHECKED AGAINST THE BYTES THAT REMAIN BEFORE ANYTHING IS ALLOCATED. Every
|
||||||
|
// element this codec writes costs at least one byte, so a count larger than the
|
||||||
|
// remaining bytes cannot describe this stream - and refusing it here is what stops a
|
||||||
|
// corrupt or truncated archive from turning into a multi-gigabyte resize before the
|
||||||
|
// element loop notices it has run out.
|
||||||
|
Bool TakeCount(ReadCursor& in, SizeT& count) {
|
||||||
|
Uint64 raw = 0;
|
||||||
|
if (!TakeRaw(in, raw)) return false;
|
||||||
|
if (raw > static_cast<Uint64>(in.Remaining())) {
|
||||||
|
in.Ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
count = static_cast<SizeT>(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ReadValue(ReadCursor& in, T& value);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ReadValue(ReadCursor& in, T& value) {
|
||||||
|
if constexpr (ArchiveScalar<T>) {
|
||||||
|
TakeRaw(in, value);
|
||||||
|
} else if constexpr (ArchiveString<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.assign(reinterpret_cast<const char*>(in.Bytes + in.Pos), count);
|
||||||
|
in.Pos += count;
|
||||||
|
} else if constexpr (ArchiveMap<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
for (SizeT i = 0; i < count && in.Ok; ++i) {
|
||||||
|
typename T::key_type key{};
|
||||||
|
typename T::mapped_type mapped{};
|
||||||
|
ReadValue(in, key);
|
||||||
|
ReadValue(in, mapped);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
value.emplace(Move(key), Move(mapped));
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveSet<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
for (SizeT i = 0; i < count && in.Ok; ++i) {
|
||||||
|
typename T::key_type key{};
|
||||||
|
ReadValue(in, key);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
value.insert(Move(key));
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveFixedArray<T>) {
|
||||||
|
for (auto& element : value) {
|
||||||
|
ReadValue(in, element);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveVector<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
value.resize(count);
|
||||||
|
for (auto& element : value) {
|
||||||
|
ReadValue(in, element);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveUniformInitializer<T>) {
|
||||||
|
ReadValue(in, value.name);
|
||||||
|
ReadValue(in, value.basicType);
|
||||||
|
ReadValue(in, value.vectorSize);
|
||||||
|
ReadValue(in, value.matrixCols);
|
||||||
|
ReadValue(in, value.matrixRows);
|
||||||
|
ReadValue(in, value.arraySize);
|
||||||
|
ReadValue(in, value.intValues);
|
||||||
|
ReadValue(in, value.floatValues);
|
||||||
|
} else {
|
||||||
|
VisitFields(value, [&in](const char*, auto& field) {
|
||||||
|
if (in.Ok) ReadValue(in, field);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The struct-size echo. Under a toolchain whose sizes are not pinned yet
|
||||||
|
// (ProgramArtifacts.h's libc++ branch until the integrator fills it in) this is 0,
|
||||||
|
// which still round-trips within one build - the echo compares what THIS build wrote
|
||||||
|
// against what THIS build expects - and stops mattering the moment the pin lands.
|
||||||
|
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||||
|
inline constexpr Uint64 kLinkArtifactsSizeEcho = MGL_LINKARTIFACTS_SIZE;
|
||||||
|
#else
|
||||||
|
inline constexpr Uint64 kLinkArtifactsSizeEcho = 0;
|
||||||
|
#endif
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EncodeProgramArtifacts(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||||
|
Vector<Uint8>& out) {
|
||||||
|
PutRaw(out, kProgramArtifactsCodecVersion);
|
||||||
|
PutRaw(out, kLinkArtifactsSizeEcho);
|
||||||
|
// `link` is walked through its own VisitFields table, which omits the live
|
||||||
|
// SharedPtr<glslang::TProgram>: 57 of the 58 members. There is no arm here for it and
|
||||||
|
// there must not be one - it points into a glslang arena that no archived instance
|
||||||
|
// owns, and ProgramTranslationCache asserts it is null at insert.
|
||||||
|
WriteValue(out, link);
|
||||||
|
WriteValue(out, spirv);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
|
||||||
|
SpirvArtifacts& spirv) {
|
||||||
|
// Both outputs are left in a DEFINED state on every exit, including every failure:
|
||||||
|
// a caller that ignores the return value gets an empty archive rather than half of a
|
||||||
|
// truncated one.
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
if (bytes == nullptr) return false;
|
||||||
|
|
||||||
|
ReadCursor in{bytes, size, 0, true};
|
||||||
|
Uint32 version = 0;
|
||||||
|
Uint64 sizeEcho = 0;
|
||||||
|
if (!TakeRaw(in, version) || !TakeRaw(in, sizeEcho)) return false;
|
||||||
|
// REFUSED, NOT GUESSED. A different version word or a struct that changed width means
|
||||||
|
// the bytes describe a layout this build does not have; deserialising them anyway
|
||||||
|
// writes garbage into the tail of a reflection table, which is exactly the failure the
|
||||||
|
// two words exist to turn into a clean false.
|
||||||
|
if (version != kProgramArtifactsCodecVersion) return false;
|
||||||
|
if (sizeEcho != kLinkArtifactsSizeEcho) return false;
|
||||||
|
|
||||||
|
ReadValue(in, link);
|
||||||
|
ReadValue(in, spirv);
|
||||||
|
if (!in.Ok) {
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Trailing bytes are a mismatch too: the format accounts for every byte it writes, so
|
||||||
|
// anything left over means the reader and the writer disagree about the shape and the
|
||||||
|
// agreement so far was luck.
|
||||||
|
if (in.Pos != in.Size) {
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Never written, never read, and stated here so it cannot be added by reflex.
|
||||||
|
link.program = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "ProgramArtifacts.h"
|
||||||
|
|
||||||
|
// The reflection ARCHIVE's serializer (P4a, D-H2): create_shader_state's payload is per-stage
|
||||||
|
// SPIR-V plus LinkArtifacts + SpirvArtifacts, whole structs, and until now nothing could turn
|
||||||
|
// those into bytes. Every VisitFields comment in ProgramArtifacts.h said "(and its serializer
|
||||||
|
// when one exists)"; this is it.
|
||||||
|
//
|
||||||
|
// IT LIVES BESIDE THE HEADER RATHER THAN INSIDE IT, deliberately: ProgramArtifacts.h carries
|
||||||
|
// the check_include_closure.py "artifacts-header" probe, which pins that the header is
|
||||||
|
// glslang-free by symbol and reaches no ShaderObject, no SpvcSession, no Config and no
|
||||||
|
// MG_Backend. A codec inside it would have to be inspected against that probe on every edit;
|
||||||
|
// a codec beside it leaves the probe untouched, and this file is compiled only in a push
|
||||||
|
// build (the root CMakeLists.txt appends it inside `if (MOBILEGL_PIPE_PUSH)`).
|
||||||
|
//
|
||||||
|
// WHEN IT ACTUALLY RUNS, and the answer is "not on the monolith hot path at all". In monolith
|
||||||
|
// the archive does not travel: MGPProgramDesc's seven blob refs are declared with Size 0 -
|
||||||
|
// "this record does not declare its blob" - and MGPipeApplyCreateShaderState takes the two
|
||||||
|
// structs by pointer beside the record, so the applier reads the frontend's own archive and
|
||||||
|
// this codec is never called. The VERIFY build is where it is exercised, and it is exercised
|
||||||
|
// as LIVE CODE WITH A GATE rather than as dead code with a unit test: the applier serialises,
|
||||||
|
// deserialises and field-compares before storing, and a mismatch is
|
||||||
|
// Fatal{PipeVerifyDiffer, "program-archive"}. Under split, P5 is what makes it the transport's
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// THE FORMAT, and every part of it is a refusal rather than a guess:
|
||||||
|
// * a VERSION word first, and a MGL_LINKARTIFACTS_SIZE echo second, so a struct that gained
|
||||||
|
// a field and a codec that did not is a MISMATCH AT READ TIME rather than a silent
|
||||||
|
// truncation that deserialises garbage into the tail of a reflection table;
|
||||||
|
// * length-prefixed everything - strings, vectors, maps, sets - with the count checked
|
||||||
|
// against the bytes that remain before a single element is allocated, so a corrupt count
|
||||||
|
// cannot turn into a four-billion-element resize;
|
||||||
|
// * little-endian, which is asserted rather than assumed;
|
||||||
|
// * and `LinkArtifacts::program` is NEVER visited. It is the live glslang TProgram, it is
|
||||||
|
// null for every archived instance by construction, and VisitFields deliberately omits it
|
||||||
|
// (57 of the 58 members). Decode leaves it null.
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
|
||||||
|
// Bumped whenever the byte format changes in a way a previous reader would misread. A
|
||||||
|
// reader that sees a different word REFUSES; it never tries to guess a layout.
|
||||||
|
inline constexpr Uint32 kProgramArtifactsCodecVersion = 1;
|
||||||
|
|
||||||
|
// Appends the archive to `out` (which is not cleared, so a caller may frame it). Never
|
||||||
|
// fails: everything it walks is owned plain data.
|
||||||
|
void EncodeProgramArtifacts(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||||
|
Vector<Uint8>& out);
|
||||||
|
|
||||||
|
// Replaces `link` and `spirv` with what `bytes` describes. Returns false - with both
|
||||||
|
// outputs left in a defined, default state - for a truncated stream, a version mismatch, a
|
||||||
|
// struct-size mismatch, or trailing bytes the format does not account for. `link.program`
|
||||||
|
// is always null on return.
|
||||||
|
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
|
||||||
|
SpirvArtifacts& spirv);
|
||||||
|
|
||||||
|
// How many fields a type's VisitFields table actually visits. The codec walks exactly that
|
||||||
|
// table, so this is what pins "the codec did not quietly grow an arm of its own" - most of
|
||||||
|
// all for LinkArtifacts, whose 58th member is the live TProgram the table omits. It is a
|
||||||
|
// runtime count rather than a static_assert because VisitFields needs an INSTANCE and
|
||||||
|
// these structs carry strings, vectors and maps: none of them is a constant expression.
|
||||||
|
// ProgramArtifactsCodecTest is where it is asserted.
|
||||||
|
template <class T>
|
||||||
|
inline SizeT ProgramArtifactsVisitedFieldCount() {
|
||||||
|
T probe{};
|
||||||
|
SizeT count = 0;
|
||||||
|
VisitFields(probe, [&count](const char*, auto&) { ++count; });
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user