mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
Compare commits
330
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4826806881 | ||
|
|
e7a6a72f6a | ||
|
|
62a7786184 | ||
|
|
ef6227e19b | ||
|
|
9bd6d39403 | ||
|
|
6b681c4a63 | ||
|
|
80a6b39003 | ||
|
|
97b997d5da | ||
|
|
7d80c9678e | ||
|
|
72aa9191b4 | ||
|
|
1e3a74686f | ||
|
|
0fe7bf82d2 | ||
|
|
416cd23c28 | ||
|
|
5f8e8db1b9 | ||
|
|
bdf05514c3 | ||
|
|
bf8b39a867 | ||
|
|
d4504e30f8 | ||
|
|
9087f13308 | ||
|
|
44ffafb2dc | ||
|
|
12b57055b7 | ||
|
|
d0ff647581 | ||
|
|
30d72c5b4e | ||
|
|
d9f4698d98 | ||
|
|
878db2c405 | ||
|
|
77ecde1524 | ||
|
|
510ecd9293 | ||
|
|
440d3c5253 | ||
|
|
83b16561c2 | ||
|
|
275dd3edb4 | ||
|
|
a196ada4c1 | ||
|
|
3aa4d8af1f | ||
|
|
bf86b1ede6 | ||
|
|
087685d19b | ||
|
|
5635e33ffe | ||
|
|
5d99ee435f | ||
|
|
b566bf4db9 | ||
|
|
09d2bb11b7 | ||
|
|
fee3902472 | ||
|
|
da249f30e2 | ||
|
|
8566a288f8 | ||
|
|
2318f6ae44 | ||
|
|
fe3dc1dde8 | ||
|
|
6672778b80 | ||
|
|
6e0e3df372 | ||
|
|
bee22f9d26 | ||
|
|
8952b14024 | ||
|
|
458ccde176 | ||
|
|
901d48a678 | ||
|
|
e8ee7b1a88 | ||
|
|
1154f9a00d | ||
|
|
7ef7c7e543 | ||
|
|
6c7ad0a1bf | ||
|
|
38d4c2372c | ||
|
|
8a239177ac | ||
|
|
87ee17c68c | ||
|
|
aa005720d0 | ||
|
|
c1a7ffac94 | ||
|
|
bdd4bed431 | ||
|
|
10315e71f3 | ||
|
|
bfa087d0f7 | ||
|
|
a1e22c26ab | ||
|
|
bd2b4158e0 | ||
|
|
9c7339b214 | ||
|
|
50815a232e | ||
|
|
d380a01f32 | ||
|
|
7566a0b002 | ||
|
|
42e0f47ebb | ||
|
|
9bbf71990c | ||
|
|
2f8d0f0d51 | ||
|
|
3363258908 | ||
|
|
9c773182bb | ||
|
|
8349babe90 | ||
|
|
1794ac94b1 | ||
|
|
8b31de2f8d | ||
|
|
50fb13430f | ||
|
|
d4f8adcf6d | ||
|
|
795e08f7e6 | ||
|
|
1e7ecab4db | ||
|
|
81b17c0b75 | ||
|
|
d1edf765f5 | ||
|
|
97e07190ac | ||
|
|
a4dcdf989e | ||
|
|
1c113e4b26 | ||
|
|
bf9cfb3079 | ||
|
|
e1818d497a | ||
|
|
d7f66722d1 | ||
|
|
92dc41ebf9 | ||
|
|
feea131d8b | ||
|
|
19f4402fbf | ||
|
|
1350031368 | ||
|
|
0ee3384b22 | ||
|
|
ba3f8d6774 | ||
|
|
3327784fd0 | ||
|
|
ff426da3a9 |
@@ -6,6 +6,10 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
+968
-3
File diff suppressed because it is too large
Load Diff
@@ -34,3 +34,6 @@
|
||||
[submodule "include/ska"]
|
||||
path = include/ska
|
||||
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||
[submodule "3rdparty/flatbuffers"]
|
||||
path = 3rdparty/flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
|
||||
+1
Submodule 3rdparty/flatbuffers added at 7e163021e5
+169
@@ -14,6 +14,26 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
|
||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
|
||||
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
|
||||
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
|
||||
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
|
||||
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
|
||||
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
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
|
||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||
# the whole migration window; it retires with the pull path itself at P13.
|
||||
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||
|
||||
@@ -238,6 +258,8 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/PipeStats.cpp
|
||||
|
||||
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
|
||||
@@ -307,13 +329,16 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
@@ -415,6 +440,85 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MG_Remote (disaggregated transport). Everything below is gated: with the
|
||||
# option OFF not one file here is compiled and no include path is added.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||
# a checkout without the submodule must configure and build, just without the
|
||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||
# lines later. Note this only checks for the RUNTIME headers - flatc is never
|
||||
# built here (see scripts/gen_protocol.py).
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
|
||||
message(WARNING
|
||||
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
|
||||
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
|
||||
"disaggregated shape for this configure; the cached ON takes effect once the "
|
||||
"submodule is present.")
|
||||
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
|
||||
# made the plain re-configure after `git submodule update` stay OFF with no message at
|
||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||
# was, so the next configure - with the submodule there - honours it.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
|
||||
# cache write, for the same reason as the disaggregated fallback above.
|
||||
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||
"configure: with nothing pushed it is the only arm there is")
|
||||
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Transport/Ring.cpp
|
||||
MobileGL/MG_Remote/Transport/Doorbell.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegment.cpp
|
||||
# Both platform halves are listed unconditionally and each is empty on
|
||||
# the other OS, so neither can rot behind an `if (WIN32)` nobody
|
||||
# configures.
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
|
||||
MobileGL/MG_Remote/Transport/FdPassing.cpp
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -465,11 +569,29 @@ set(MOBILEGL_COMPILE_DEF
|
||||
-DASIO_NO_DEPRECATED
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/MobileGL
|
||||
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
|
||||
# line above too; this entry lets the client, the backends and MG_Remote spell them
|
||||
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
|
||||
${spirv-tools_SOURCE_DIR}
|
||||
${spirv-tools_SOURCE_DIR}/include
|
||||
${spirv-tools_BINARY_DIR}
|
||||
@@ -480,6 +602,13 @@ set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# Header-only runtime: an include path, no add_subdirectory, no link
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
@@ -696,3 +825,43 @@ endif()
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
|
||||
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
|
||||
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
|
||||
# so a second executable has to be built with an .so name and exec'd out of
|
||||
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
|
||||
# chain end to end: it is packaged like a library, exec'd from the app's own
|
||||
# untrusted_app process, and writes a marker the parent reads back.
|
||||
#
|
||||
# Off by default and ANDROID-only, so no shipping configuration builds it. The
|
||||
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
|
||||
# ---------------------------------------------------------------------------
|
||||
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
add_executable(MobileGLServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
|
||||
|
||||
# An executable that is named like a shared library still has to be a real
|
||||
# PIE executable: Android has refused non-PIE executables since API 21, and
|
||||
# the name alone does not change what the loader demands of the file.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServer"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServer PRIVATE -pie)
|
||||
|
||||
# AGP packages what the external native build drops into the per-ABI output
|
||||
# directory, and it selects by the .so extension. CMake puts executables in
|
||||
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
|
||||
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
|
||||
# the library directory when the generator gave us one.
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+135
-1
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
|
||||
inline const String ProjectName = "MobileGL";
|
||||
inline const String CoreName = "MobileGL Core";
|
||||
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
|
||||
inline const Version CoreVersion = {26, 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 Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -152,6 +152,19 @@ namespace MobileGL::MG_Config {
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -184,6 +197,15 @@ namespace MobileGL::MG_Config {
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
|
||||
// route for the same write stalls the thread or ghost-copies the whole arena on
|
||||
// this class of Mali driver, and the arena stops costing its size again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
@@ -278,6 +300,118 @@ namespace MobileGL::MG_Config {
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||
// the same capture-less draws and arms the best proven one: the dedicated
|
||||
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||
// far (MG_Pipe::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;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
// counts instead, for triage and for the lane that must survive to read its own
|
||||
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
|
||||
// off.
|
||||
Bool PipeVerifyFatal = true;
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
|
||||
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
|
||||
// green verify run goes red naming it (negative control A). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipeVerifyCorrupt;
|
||||
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
|
||||
// the value) of that field for that verb, an omission indistinguishable from a
|
||||
// forgotten FillPoints.def row, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
// per-draw staging, counted separately in the stats.
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
// the log whenever PipeStats is on, so a device run needs no writable path.
|
||||
String PipeStatsFile;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
// End of Source File Header
|
||||
|
||||
#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 <cstdlib>
|
||||
@@ -159,6 +165,38 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return static_cast<Uint32>(parsedValue);
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
|
||||
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
|
||||
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
|
||||
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
|
||||
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
|
||||
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const String& value = it->second;
|
||||
const char* text = value.c_str();
|
||||
int base = 10;
|
||||
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
text += 2;
|
||||
base = 16;
|
||||
}
|
||||
char* parseEnd = nullptr;
|
||||
errno = 0;
|
||||
const bool negative = value.find('-') != String::npos;
|
||||
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
|
||||
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
|
||||
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
|
||||
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return static_cast<Uint64>(parsedValue);
|
||||
}
|
||||
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
@@ -181,12 +219,14 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
@@ -204,6 +244,41 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::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);
|
||||
#endif
|
||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||
features.PipeVerifyFatal =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
@@ -42,6 +43,11 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
// off, and idempotent.
|
||||
MG_Util::PipeStats::Shutdown();
|
||||
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||
@@ -102,6 +108,10 @@ namespace MobileGL {
|
||||
MGLOG_I("Initializing MobileGL...");
|
||||
MG_ConfigLoader::Init();
|
||||
MGLOG_I("Config loaded");
|
||||
// Immediately after the config load and before anything can count: the MGPipe
|
||||
// boundary counters latch their enable flag here, so every counting site in the
|
||||
// two backends is a load of an already-settled global for the rest of the run.
|
||||
MG_Util::PipeStats::Init();
|
||||
MG_State::Init();
|
||||
MGLOG_D("MG_State initialized");
|
||||
MG_Backend::Init();
|
||||
|
||||
@@ -192,9 +192,23 @@ namespace MobileGL {
|
||||
void (*MemoryBarrierByRegion)(GLbitfield barriers);
|
||||
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
|
||||
GLenum access, GLenum format);
|
||||
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
|
||||
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
|
||||
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
|
||||
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
|
||||
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
|
||||
// 64-bit and float/double widths are derived there from the same answer, which is why
|
||||
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
|
||||
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
|
||||
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
|
||||
// case for at all.
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
|
||||
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
|
||||
// size - all of which are frontend link artifacts on ProgramObject, and
|
||||
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
|
||||
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
|
||||
// one, or a SPIR-V module), in a namespace the application never sees.
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
@@ -364,6 +378,19 @@ namespace MobileGL {
|
||||
Int MaxFragmentShaderStorageBlocks = 8;
|
||||
Int MaxComputeUniformBlocks = 12;
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
|
||||
// axis. These six, with the invocations limit above, are the only indexed limits a
|
||||
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
|
||||
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
|
||||
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
|
||||
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
|
||||
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
|
||||
// answers, like the invocations limit: GL_Getter and the compile environment floor
|
||||
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
|
||||
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
|
||||
// MaxClipDistances' does.
|
||||
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
|
||||
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
@@ -495,6 +522,24 @@ namespace MobileGL {
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||
// independently from the EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||
// varying program-wide (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||
// overrides the detection in either direction at backend init.
|
||||
//
|
||||
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||
// sets it gets standard modules and, at worst, the old honest declines.
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -1255,8 +1255,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.Clear = Clear;
|
||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||
@@ -1417,6 +1415,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
|
||||
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
|
||||
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
|
||||
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||
@@ -1479,6 +1484,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
|
||||
// extension pairs the loader probed, independently, because they really do come
|
||||
// separately. False arms the shared phase-B demotion
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
|
||||
// never names the built-in in those stages and needs no extension.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
|
||||
// exercised on a healthy driver (the pinned integration lane); =0 restores the
|
||||
// detected answer's declines.
|
||||
m_dynamicParameters.SupportsTessellationPointSize =
|
||||
m_GLESCapabilities.TessellationPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
m_dynamicParameters.SupportsGeometryPointSize =
|
||||
m_GLESCapabilities.GeometryPointSizeSupport !=
|
||||
MG_External::GLESCapabilities::PointSizeTier::None;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = false;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the driver's extensions");
|
||||
m_dynamicParameters.SupportsTessellationPointSize = true;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,8 +92,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
Bool InitWindowSurface(NativeWindowType window);
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -41,14 +43,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
return MGB_CTX->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -83,7 +85,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
const auto& indirect =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
@@ -91,7 +93,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
@@ -156,7 +158,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
// statsClass: which MGPipe byte population these bytes belong to. Counted here
|
||||
// rather than at the four call sites so a new tier cannot forget it.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -169,6 +174,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -183,7 +191,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -207,6 +216,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
@@ -417,7 +429,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -532,7 +545,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -737,10 +751,16 @@ void main() {
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
|
||||
return;
|
||||
}
|
||||
// data == nullptr: pure respecify, the compute pass writes the contents, so no
|
||||
// host bytes cross here and nothing is counted.
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
|
||||
@@ -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
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -2294,11 +2295,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -385,8 +386,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
@@ -740,8 +741,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
@@ -785,8 +784,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
@@ -939,6 +938,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
|
||||
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
|
||||
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
|
||||
// them. Not clamped: unlike the block counts these are not amounts an application
|
||||
// allocates, and the frontend already raises them to the GL minimum.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
@@ -1081,6 +1089,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||
// detected answer's declines.
|
||||
{
|
||||
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
supportsStagePointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the device feature");
|
||||
supportsStagePointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||
}
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
#include "DirectVulkanResourceState.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/ErrorState/ErrorInfo.h"
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||
#include <atomic>
|
||||
@@ -77,7 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 blockBindingVersion = 0;
|
||||
Vector<StorageBlockResource> storageBlocks;
|
||||
Vector<BufferVariableResource> bufferVariables;
|
||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||
};
|
||||
|
||||
struct DrawElementsIndirectCommand {
|
||||
@@ -208,16 +209,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
for (auto& module : modules) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < module.entry_point_count; ++entryIndex) {
|
||||
const auto& entryPoint = module.entry_points[entryIndex];
|
||||
if ((entryPoint.shader_stage & SPV_REFLECT_SHADER_STAGE_COMPUTE_BIT) == 0) {
|
||||
continue;
|
||||
}
|
||||
cache.computeWorkGroupSize[0] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.x, 1));
|
||||
cache.computeWorkGroupSize[1] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.y, 1));
|
||||
cache.computeWorkGroupSize[2] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.z, 1));
|
||||
}
|
||||
|
||||
uint32_t bindingCount = 0;
|
||||
SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr);
|
||||
if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) {
|
||||
@@ -277,15 +268,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
if (!MGB_CTX->ValidateProgramName(program)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
auto& programObject = MGB_CTX->GetProgramObject(program);
|
||||
return programObject.get();
|
||||
}
|
||||
|
||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
@@ -344,64 +335,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfi called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfi called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfi called with null GL context");
|
||||
pVulkanRenderer->ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferfv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferuiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferuiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferuiv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferuiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferiv called with null GL context");
|
||||
pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLfloat* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, const GLuint* value) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||
GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
|
||||
pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
|
||||
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
@@ -409,7 +400,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||
return;
|
||||
@@ -452,13 +443,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
|
||||
pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride);
|
||||
}
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
|
||||
|
||||
if (maxdrawcount <= 0) {
|
||||
return;
|
||||
@@ -472,7 +463,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
@@ -503,7 +494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
|
||||
|
||||
DrawIndexedCmd payload{};
|
||||
payload.mode = mode;
|
||||
@@ -530,7 +521,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsIndirect called with null GL context");
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
@@ -540,7 +531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, 1, 0);
|
||||
return;
|
||||
@@ -574,7 +565,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysInstancedBaseInstance called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
|
||||
|
||||
DrawCmd payload{};
|
||||
payload.mode = mode;
|
||||
@@ -589,11 +580,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysIndirect called with null GL context");
|
||||
|
||||
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
|
||||
// (e.g. by a compute shader), so consume them natively on the GPU.
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, 1, 0);
|
||||
return;
|
||||
@@ -623,13 +614,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexImage2D called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexImage2D called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexImage2D called with null GL context");
|
||||
pVulkanRenderer->CopyTexSubImage2D(target, level, 0, 0, x, y, width, height);
|
||||
}
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexSubImage2D called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
@@ -638,32 +629,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
void GenerateMipmap(GLenum target) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GenerateMipmap called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GenerateMipmap called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GenerateMipmap called with null GL context");
|
||||
pVulkanRenderer->GenerateMipmap(target);
|
||||
}
|
||||
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchCompute called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchCompute called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchCompute called with null GL context");
|
||||
pVulkanRenderer->DispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
void DispatchComputeIndirect(GLintptr indirect) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchComputeIndirect called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchComputeIndirect called with null GL context");
|
||||
pVulkanRenderer->DispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
void MemoryBarrier(GLbitfield barriers) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MemoryBarrier called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MemoryBarrier called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MemoryBarrier called with null GL context");
|
||||
pVulkanRenderer->MemoryBarrier(barriers);
|
||||
}
|
||||
|
||||
@@ -682,130 +673,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(void)format;
|
||||
}
|
||||
|
||||
// The two compute limits are the only indexed pnames a backend genuinely owns: they come
|
||||
// from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it
|
||||
// can raise the answer to the GL required minimum. The same six numbers are carried in
|
||||
// DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from
|
||||
// the same limits), which is their MGPCaps carrier once this entry retires - the
|
||||
// AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND
|
||||
// state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit
|
||||
// bindings, the viewport rectangles, the indexed capabilities) and is answered there before
|
||||
// the table is consulted, so the arms this function used to carry for
|
||||
// GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were unreachable duplicates - and not
|
||||
// even faithful ones: the frontend reports the range glBindBufferRange was ASKED for,
|
||||
// verbatim, while these clamped it to the buffer's current storage.
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
|
||||
if (!data) return;
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer");
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
switch (target) {
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
*data = static_cast<GLint>(
|
||||
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]);
|
||||
return;
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
|
||||
if (index >= 3) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
*data = static_cast<GLint>(
|
||||
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]);
|
||||
return;
|
||||
case GL_SHADER_STORAGE_BUFFER_BINDING: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_START: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
*data = static_cast<GLint>(point.GetRange().start);
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto& range = point.GetRange();
|
||||
const auto start = std::min(range.start, obj->GetSize());
|
||||
const auto end = std::min(range.end, obj->GetSize());
|
||||
*data = static_cast<GLint>(end - start);
|
||||
return;
|
||||
}
|
||||
case GL_IMAGE_BINDING_NAME:
|
||||
case GL_IMAGE_BINDING_LEVEL:
|
||||
case GL_IMAGE_BINDING_LAYERED:
|
||||
case GL_IMAGE_BINDING_LAYER:
|
||||
case GL_IMAGE_BINDING_ACCESS:
|
||||
case GL_IMAGE_BINDING_FORMAT: {
|
||||
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
|
||||
if (target == GL_IMAGE_BINDING_NAME) {
|
||||
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
|
||||
} else if (target == GL_IMAGE_BINDING_LEVEL) {
|
||||
*data = imageBinding.Level;
|
||||
} else if (target == GL_IMAGE_BINDING_LAYERED) {
|
||||
*data = imageBinding.Layered;
|
||||
} else if (target == GL_IMAGE_BINDING_LAYER) {
|
||||
*data = imageBinding.Layer;
|
||||
} else if (target == GL_IMAGE_BINDING_ACCESS) {
|
||||
*data = static_cast<GLint>(imageBinding.Access);
|
||||
} else {
|
||||
*data = static_cast<GLint>(imageBinding.Format);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
||||
if (!data) return;
|
||||
switch (target) {
|
||||
case GL_SHADER_STORAGE_BUFFER_START: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
*data = static_cast<GLint64>(point.GetRange().start);
|
||||
return;
|
||||
}
|
||||
case GL_SHADER_STORAGE_BUFFER_SIZE: {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
|
||||
auto& obj = point.GetBoundObject();
|
||||
if (!obj) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto& range = point.GetRange();
|
||||
const auto start = std::min(range.start, obj->GetSize());
|
||||
const auto end = std::min(range.end, obj->GetSize());
|
||||
*data = static_cast<GLint64>(end - start);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
|
||||
if (!params) return;
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject) {
|
||||
params[0] = 0;
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_COMPUTE_WORK_GROUP_SIZE: {
|
||||
auto& cache = GetProgramResourceCache(*programObject);
|
||||
params[0] = cache.computeWorkGroupSize[0];
|
||||
params[1] = cache.computeWorkGroupSize[1];
|
||||
params[2] = cache.computeWorkGroupSize[2];
|
||||
return;
|
||||
}
|
||||
default:
|
||||
params[0] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||
if (!programObject || storageBlockName == nullptr) return;
|
||||
@@ -813,7 +714,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||
: 0;
|
||||
if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
MGB_CTX->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
||||
return;
|
||||
@@ -838,24 +739,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ReadPixels called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ReadPixels called with null GL context");
|
||||
pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTexImage called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTexImage called with null GL context");
|
||||
pVulkanRenderer->GetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTextureImage called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTextureImage called with null GL context");
|
||||
pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels);
|
||||
}
|
||||
|
||||
void Clear(GLbitfield mask) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::Clear called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::Clear called with null GL context");
|
||||
pVulkanRenderer->Clear(mask);
|
||||
}
|
||||
|
||||
@@ -883,7 +784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Uint8* indexBytes = nullptr;
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (indexBufferShared != nullptr) {
|
||||
const SizeT offset = reinterpret_cast<SizeT>(indices);
|
||||
@@ -914,7 +815,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArrays called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
if (count < 2) {
|
||||
@@ -939,7 +840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElements called with null GL context");
|
||||
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
@@ -962,7 +863,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArrays called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArrays called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArrays called with null GL context");
|
||||
if (drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1007,7 +908,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
||||
// declined the whole batch and painted nothing.)
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& vao = *MGB_CTX->GetBoundVertexArray();
|
||||
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) {
|
||||
@@ -1068,13 +969,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
|
||||
}
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
|
||||
if (mode == GL_LINE_LOOP) {
|
||||
Vector<Uint32> closedIndices;
|
||||
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
|
||||
@@ -1098,14 +999,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
|
||||
}
|
||||
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitFramebuffer called with null VulkanRenderer");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::BlitFramebuffer called with null GL context");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::BlitFramebuffer called with null GL context");
|
||||
pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||
}
|
||||
|
||||
@@ -1206,6 +1107,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
|
||||
Vector<Uint32> occlusionSlots;
|
||||
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
|
||||
// draws, where the renderer's reroute is armed (the affected driver's
|
||||
// stream query counts nothing without an open capture; see
|
||||
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
|
||||
// slots above, which keep the span's XFB-active draws.
|
||||
Vector<Uint32> rerouteSlots;
|
||||
// Renderer generation the records were written under (see
|
||||
// g_rendererGeneration). A stale generation resolves as available
|
||||
// with a final zero result: the records' pool indices and frame
|
||||
@@ -1215,11 +1122,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale queries are always safe to delete.
|
||||
Uint64 rendererGeneration = 0;
|
||||
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
||||
// capture saw, so a draw made while the span was paused is invisible to it -
|
||||
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
||||
// emitted regardless. The delta closes that gap at result time.
|
||||
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
|
||||
// counts only what the capture saw, so a draw made while the span was paused is
|
||||
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
|
||||
// processing stage emitted regardless. The delta closes that gap at result time.
|
||||
Uint64 pausedPrimitiveSnapshot = 0;
|
||||
// ...unless the GPU already counted those paused draws when the span opened -
|
||||
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
|
||||
// draw with no open capture, paused ones included) or, where the probe measured
|
||||
// the stream query as counting capture-less draws, through the stream slot the
|
||||
// paused draw still takes. Adding the CPU delta on top would count them twice,
|
||||
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
|
||||
// points write it and it answers 0 for GL_PATCHES.
|
||||
Bool pausedPrimitivesCountedByGpu = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1313,13 +1228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
|
||||
Uint64 primitives = 0;
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
|
||||
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
|
||||
primitives)) {
|
||||
return false;
|
||||
}
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
||||
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
|
||||
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
|
||||
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||
query->pausedPrimitiveSnapshot;
|
||||
}
|
||||
*outNanoseconds = primitives;
|
||||
@@ -1366,7 +1282,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||
query->rendererGeneration = GetRendererGeneration();
|
||||
query->pausedPrimitiveSnapshot =
|
||||
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||
MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
|
||||
// disarms: the answer is then what this span will actually do for every draw.
|
||||
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -1377,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->StopXfbQueryCapture(
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
|
||||
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
|
||||
query->rerouteSlots);
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginOcclusionQuery() {
|
||||
@@ -1411,5 +1331,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Present() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||
pVulkanRenderer->Present();
|
||||
// THE frame boundary for the MGPipe counters, at the backend entry point rather
|
||||
// than inside VulkanRenderer::Present: that function has an early return for the
|
||||
// no-usable-swapchain case, and a suspended frame is still a frame the counters
|
||||
// must close.
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::OnPresent();
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -95,8 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
|
||||
@@ -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
|
||||
@@ -1428,6 +1428,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
if (varying.name == "gl_PointSize") {
|
||||
// A demoted module (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
|
||||
// built-in member - the value lives in the carrier variable the demotion
|
||||
// named - so the capture binds to the carrier directly. The mirror below
|
||||
// must not run for it: reading the now-unwritten member would capture
|
||||
// garbage, and the read itself is the capability access the demotion
|
||||
// exists to remove. Detected off the module's own debug names, so a
|
||||
// composite built from another program's stage answers for the module it
|
||||
// actually contains.
|
||||
const auto carrierIt = idsByName.find(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
|
||||
if (carrierIt != idsByName.end()) {
|
||||
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
|
||||
modified = true;
|
||||
continue;
|
||||
}
|
||||
needsPointSizeMirror = true;
|
||||
pointSizeBufferIndex = varying.bufferIndex;
|
||||
pointSizeOffset = varying.offsetBytes;
|
||||
@@ -3454,6 +3470,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
if (program.PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
|
||||
// rendering assertion above it stays green on a healthy driver whether the
|
||||
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
|
||||
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
|
||||
"demoted to an ordinary varying, because this device cannot host the built-in "
|
||||
"in those stages.");
|
||||
}
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject1D.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
@@ -502,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// alive through the draw via GL binding state. Only the fallback path needs a SharedPtr to
|
||||
// keep the fallback texture alive for the rest of this call.
|
||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
||||
@@ -551,7 +553,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!IsValidSampledImageLayout(resource->layout)) {
|
||||
auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
FramebufferAttachmentType attachmentType = FramebufferAttachmentType::None;
|
||||
Int attachmentLevel = 0;
|
||||
if (drawFbo &&
|
||||
@@ -778,7 +780,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
||||
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
|
||||
const auto* effectiveSampler =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (effectiveSampler == nullptr) return false;
|
||||
@@ -808,7 +810,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||
outTexture.reset();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTexture: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTexture: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSamplerTexture: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -817,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
|
||||
// The slot always holds at least the target's default texture (name 0). While that
|
||||
@@ -833,7 +835,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
|
||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, Uint32 element) {
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTextureRaw: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSamplerTextureRaw: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -843,7 +845,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
|
||||
// touching the refcount (no atomic inc/dec per binding per draw).
|
||||
@@ -988,7 +990,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView& outBufferView) {
|
||||
outBufferView = VK_NULL_HANDLE;
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: frame index out of range");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
@@ -1012,7 +1014,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
|
||||
"ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding);
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
|
||||
@@ -1148,7 +1150,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorBufferInfo& outBufferInfo) const {
|
||||
outBufferInfo = {};
|
||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageBufferDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(),
|
||||
"ResolveStorageBufferDescriptor: binding %u out of range", binding);
|
||||
|
||||
@@ -1185,12 +1187,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? static_cast<GLuint>(atomicCounterBinding)
|
||||
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
||||
const Uint32 bindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
|
||||
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(bufferTarget));
|
||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
|
||||
frontendBinding, blockName.c_str());
|
||||
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
||||
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(bufferTarget, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
|
||||
@@ -1262,7 +1264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorImageInfo& outImageInfo) const {
|
||||
outImageInfo = {};
|
||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null");
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageImageDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveStorageImageDescriptor: binding %u out of range", binding);
|
||||
|
||||
@@ -1291,7 +1293,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
// Legal GL: an image unit with no texture bound makes loads return zero and discards
|
||||
// stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning
|
||||
@@ -1647,7 +1649,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
|
||||
// texture and the sampler override - this runs per binding per full-path draw,
|
||||
// and program-alternating draw streams take the full path on every draw.
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSampledBinding: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSampledBinding: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSampledBinding: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
@@ -1658,7 +1660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
@@ -1802,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
|
||||
outTextures.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
||||
"CollectStorageImageTextures: GL context is null");
|
||||
// Same as the sampled walk: a declined program is refused at bind time, and its declined
|
||||
// binding has no uniform location to reach an image unit through.
|
||||
@@ -1846,7 +1848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
auto* texture = MGB_CTX->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
// ResolveStorageImageDescriptor will substitute the placeholder image for this
|
||||
// binding; include it here for the same reason the sampled walk includes the
|
||||
@@ -1884,7 +1886,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
||||
outBindings.clear();
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE,
|
||||
"CollectSamplerImageFeedback: GL context is null");
|
||||
if (programObj.declinedDescriptors) return true;
|
||||
|
||||
@@ -1934,7 +1936,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
return false;
|
||||
}
|
||||
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& image = MGB_CTX->GetImageTextureBinding(imageUnit);
|
||||
// A sampler view exposes all layers of its target; equal texture plus an
|
||||
// overlapping mip therefore aliases the writable image subresource.
|
||||
if (image.Texture.get() == sampledTexture &&
|
||||
@@ -1964,7 +1966,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* outData = nullptr;
|
||||
VkDeviceSize outSize = 0;
|
||||
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveUniformBufferPayload: GL context is null");
|
||||
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveUniformBufferPayload: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.bindingKinds.size(),
|
||||
"ResolveUniformBufferPayload: binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(programObj.bindingKinds[binding] == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic,
|
||||
@@ -2009,12 +2011,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast<Uint32>(blockIndex));
|
||||
const Uint32 uniformBindingPointCount =
|
||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));
|
||||
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(BufferTarget::Uniform));
|
||||
MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount,
|
||||
"ResolveUniformBufferPayload: frontend UBO binding %u out of range for block '%s'",
|
||||
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
|
||||
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
||||
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
MOBILEGL_ASSERT(bufferObject != nullptr,
|
||||
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
|
||||
@@ -2076,6 +2078,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
out.dynamicOffset = rangeStart;
|
||||
}
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
|
||||
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
|
||||
// payload a split build would have to ship with set_shader_buffers. Espryt binds
|
||||
// the frontend buffer to the driver and contributes nothing here, which is why
|
||||
// the class is named for the payload and not for the call. Counted AFTER the
|
||||
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
|
||||
// here reported a copy that never happened.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
|
||||
static_cast<Uint64>(outSize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2283,6 +2295,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outBuffer = slice.buffer;
|
||||
outRange = ubo.payloadSize;
|
||||
outDynamicOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo && MG_Util::PipeStats::Enabled()) {
|
||||
// Magma's half of stage-ubo-global, so the class means the same on both
|
||||
// backends. The memo hit above returns before this, so a frame that reuses the
|
||||
// slice correctly contributes nothing.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
||||
static_cast<Uint64>(ubo.payloadSize));
|
||||
}
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] =
|
||||
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#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
|
||||
// test's positions) instead 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)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||
//
|
||||
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Uint64& outHash) const {
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||
outHash = memos.Hash;
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
HashType hash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||
return memos.Hash;
|
||||
}
|
||||
hash = ComputeHash(vao);
|
||||
memos.Hash = hash;
|
||||
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||
return hash;
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
if (!vao.GetBackendHashMemo(hash)) {
|
||||
hash = ComputeHash(vao);
|
||||
vao.SetBackendHashMemo(hash);
|
||||
}
|
||||
#endif
|
||||
return hash;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||
// memo does not change that.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||
memos.StateEpoch == m_evictionEpoch) {
|
||||
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *memoEntry;
|
||||
}
|
||||
const BackendVertexInputState& resolved =
|
||||
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||
// the reference is not worth holding across a call that can resize the table.
|
||||
VaoBackendMemos& stamp = MemosFor(vao);
|
||||
stamp.State = &resolved;
|
||||
stamp.StateEpoch = m_evictionEpoch;
|
||||
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||
return resolved;
|
||||
}
|
||||
#endif
|
||||
#if !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||
// time true, so the handle arm above always returns. Written out rather than left to
|
||||
// fall off the end so the function still has a return on every path a compiler sees.
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
#else
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
@@ -83,6 +208,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||
return entry;
|
||||
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
@@ -316,8 +442,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment).
|
||||
// instances (see the member comment). With no legacy arm the memos
|
||||
// live in this factory and die with it, so a per-instance bump is
|
||||
// enough - P2 D12.5.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
++m_evictionEpoch;
|
||||
#endif
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
||||
// so the pull build gains nothing from it.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#include "Config.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.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):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
#endif
|
||||
~VertexInputStateFactory() = default;
|
||||
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
|
||||
// is unchanged. Use this on per-draw paths.
|
||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||
// question ask the factory instead, because that is where the memo lives once the
|
||||
// frontend object stops carrying the backend's state.
|
||||
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
||||
#endif
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
@@ -112,6 +133,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||
//
|
||||
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||
// backend is in another process and its cache entry has no address a client could
|
||||
// store, so the memo has to live on the side that owns the pointee.
|
||||
//
|
||||
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||
struct VaoBackendMemos {
|
||||
// Whose memos these are. The identity table can recycle a slot for a different
|
||||
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||
// the contents are this object's and not its predecessor's.
|
||||
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Hash = 0;
|
||||
Uint32 HashConfigVersion = ~0u;
|
||||
const void* State = nullptr;
|
||||
Uint64 StateEpoch = 0;
|
||||
Uint32 StateConfigVersion = ~0u;
|
||||
};
|
||||
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||
// someone else's.
|
||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#endif
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||
@@ -130,15 +189,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stale memo.
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
||||
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
|
||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||
// factory restarting at a dead factory's epoch value would honor its
|
||||
// dangling entry pointers. The constructor takes a value strictly greater
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
//
|
||||
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
static inline Uint64 s_evictionEpochSource = 0;
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
@@ -229,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
|
||||
(void)kind;
|
||||
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
|
||||
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
|
||||
// absent: its bytes are counted by the caller, which is the only place that
|
||||
// knows whether the payload is the default block (stage-ubo-global) or a named
|
||||
// one repacked into the ring (stage-ubo-named), and counting here as well would
|
||||
// double every uniform byte.
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Index:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Indirect:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::TextureBuffer:
|
||||
case BufferKind::ShaderStorage:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Uniform:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::InitializeTransientArenas() {
|
||||
@@ -339,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource.pendingFullUpload = false;
|
||||
return true;
|
||||
}
|
||||
@@ -353,6 +388,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), 16, staging)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
|
||||
// half of the same bytes and is not counted twice.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commandBuffer == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
@@ -422,6 +462,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +489,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -484,6 +529,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -554,6 +602,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint8* seed = bufferObject.MappedData();
|
||||
if (seed != nullptr) {
|
||||
resource->buffer.Upload(seed, size, 0);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The one-time seed of a persistent map. Everything the app writes AFTER
|
||||
// this goes straight through the mapping and is persistent-map-push
|
||||
// territory (unwired, D4/D-B4), not this class.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
resource->persistentMapped = true;
|
||||
resource->pendingFullUpload = false;
|
||||
@@ -602,6 +657,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->usageFlags = 0;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
resource->pendingFullUpload = false;
|
||||
}
|
||||
|
||||
@@ -681,6 +740,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource->transientSlice = outSlice;
|
||||
resource->transientFrameSerial = m_frameSerial;
|
||||
resource->transientChangeSerial = changeSerial;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
@@ -54,7 +55,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (payload.colorEncoding != ClearColorEncoding::Float) return;
|
||||
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
|
||||
// is exactly right and there is nothing to undo.
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
|
||||
|
||||
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
@@ -610,7 +611,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
@@ -962,7 +963,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
@@ -1108,7 +1109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
@@ -805,7 +807,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||
if (aliveIt == m_aliveObjects.end()) {
|
||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||
const auto& liveTexture = MGB_CTX->GetTextureObject(texture.GetExternalIndex());
|
||||
if (liveTexture && liveTexture.get() == &texture) {
|
||||
aliveTexture = liveTexture;
|
||||
} else {
|
||||
@@ -948,7 +950,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat baseAttachmentFormat =
|
||||
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
|
||||
const VkFormat attachmentFormat =
|
||||
@@ -3150,6 +3152,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
packBox(dst, item.regionLo, item.regionSize);
|
||||
}
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// Same shape split as Espryt's: one union box per item, or one job per rect of
|
||||
// a refined rect list. The box/rect decision is invisible to SSIM and is what
|
||||
// the +6 ms/frame Mali cliff of section 7.3 was, so it is counted apart from
|
||||
// the bytes.
|
||||
Uint64 boxEmissions = 0;
|
||||
Uint64 rectEmissions = 0;
|
||||
Uint64 jobs = 0;
|
||||
for (const auto& item : uploadItems) {
|
||||
if (item.rects.empty()) {
|
||||
++boxEmissions;
|
||||
jobs += isCombinedDepthStencil ? 2u : 1u;
|
||||
} else {
|
||||
++rectEmissions;
|
||||
jobs += static_cast<Uint64>(item.rects.size());
|
||||
}
|
||||
}
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
|
||||
static_cast<Uint64>(stagingSize));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions,
|
||||
static_cast<Uint64>(uploadItems.size()));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, boxEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadRectEmissions, rectEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, jobs);
|
||||
}
|
||||
|
||||
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags uploadSrcAccessMask = 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
#include "Config.h"
|
||||
#include "FrameContext.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "ProgramFactory.h"
|
||||
#include "SwapchainObject.h"
|
||||
@@ -24,6 +25,13 @@
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo
|
||||
// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged.
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
@@ -674,8 +682,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// per object: one group of four slots each, handed out on first use.
|
||||
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||
VkBufferObject m_xfbCounterBuffer;
|
||||
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||
Uint32 m_xfbNextCounterSlot = 0;
|
||||
// Which transform feedback object owns each slot group, by the frontend's never-reused
|
||||
// lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
|
||||
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
|
||||
// inherited the dead one's slot, and since nothing ever removed an entry the map also
|
||||
// grew for the life of the context. A fixed table cannot do either: a group is taken over
|
||||
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
|
||||
// counters can still be resumed never loses them, and a dead object's group comes back.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
|
||||
// Tie-break among reclaimable groups only; never on its own, because the paused span the
|
||||
// groups exist for is by construction the least recently used one.
|
||||
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
|
||||
Uint64 m_xfbCounterSlotUseSerial = 0;
|
||||
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||
// counter-buffer resume on the next captured draw of the same span.
|
||||
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||
@@ -717,15 +735,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> m_xfbQueryActiveSlots[2];
|
||||
Bool m_xfbQuerySlotOpen = false;
|
||||
Uint32 m_xfbQueryOpenSlot = 0;
|
||||
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
|
||||
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
|
||||
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
|
||||
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
|
||||
// finds that defect with a working control - or
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
|
||||
// GENERATED count through this pool instead, whose type the arming picks:
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
|
||||
// query with its rasterizer-discard feature (exact semantics by definition -
|
||||
// the extension exists because GL needs this count without a capture), else a
|
||||
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
|
||||
// per primitive reaching primitive clipping - after every vertex processing
|
||||
// stage, before rasterizer discard - which is the same set).
|
||||
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
|
||||
// every draw with no open capture - a PAUSED span's draws included - takes a
|
||||
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
|
||||
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
|
||||
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
|
||||
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
|
||||
// GL query span may therefore hold slots of both pools.
|
||||
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
|
||||
// VK_EXT_primitives_generated_query: base feature, and the
|
||||
// ...WithRasterizerDiscard feature without which a discarding draw inside the
|
||||
// query is invalid usage (so the reroute never picks the dedicated pool on a
|
||||
// base-only device - GL applications toggle discard freely).
|
||||
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
|
||||
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
|
||||
// tessellationShader was enabled at device creation (it is taken whenever the
|
||||
// device advertises it); gates the probe's PATCHES shape.
|
||||
Bool m_tessellationShaderFeatureEnabled = false;
|
||||
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
|
||||
MG_Util::SelfTest::PrimGenRerouteKind::None;
|
||||
// The bring-up probe measured this device's stream query as counting draws made
|
||||
// with no capture span open (the StreamCounts verdict) - so it counts the
|
||||
// PAUSED-span ones too, through the stream slot they take when nothing is
|
||||
// rerouted. Only the probe can know this, so it stays false wherever the probe
|
||||
// is not consulted (the forced arms), which keeps those lanes' accounting as it
|
||||
// was.
|
||||
Bool m_primGenStreamCountsXfbInactiveDraws = false;
|
||||
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
|
||||
Uint32 m_primGenRerouteSlotCursor = 0;
|
||||
Vector<Uint32> m_primGenRerouteActiveSlots;
|
||||
Bool m_primGenRerouteSlotOpen = false;
|
||||
Uint32 m_primGenRerouteOpenSlot = 0;
|
||||
// Runs the bring-up probe (memoized per process) and decides
|
||||
// m_primGenRerouteKind. Called at the end of device creation: it records on
|
||||
// m_graphicsQueue, which nothing else is using yet.
|
||||
void ArmPrimGenReroute();
|
||||
|
||||
public:
|
||||
// Whether a GENERATED span opened now will have the draws made while the GL
|
||||
// span is PAUSED counted on the GPU - through the reroute pool, which takes
|
||||
// every draw with no open capture, or (where the reroute is not armed because
|
||||
// the stream query was measured to count capture-less draws) through the stream
|
||||
// slot such a draw still takes. The frontend's CPU paused-primitive counter
|
||||
// must not be added on top of either: it would double count, and it cannot
|
||||
// price the draws that matter anyway - only 3 of the ~15 draw entry points
|
||||
// write it and it answers 0 for GL_PATCHES. Read once per span, after
|
||||
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
|
||||
Bool ArePausedDrawsGpuCounted() const;
|
||||
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
|
||||
Bool StartXfbQueryCapture(Uint32 kind);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
|
||||
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
|
||||
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
|
||||
Bool wantGenerated, Uint64& outPrimitives);
|
||||
|
||||
private:
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
|
||||
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
@@ -750,12 +827,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
// VALUE hash of the pipeline-relevant fixed-function state (see
|
||||
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
||||
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
||||
// all entries forever even though the state alternates between two
|
||||
// values the memo already holds.
|
||||
// The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the
|
||||
// handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see
|
||||
// ComputePipelineStateHash), not the monotonic pipeline-state version: the version
|
||||
// never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even
|
||||
// though the state alternates between two values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The HANDLE arm's key component, and the whole of D12.1: the CLIENT already
|
||||
// hashed the pipeline subset of RenderStateParameters and minted a content-
|
||||
// addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache),
|
||||
// so re-hashing the same 396 bytes here was work the boundary had already done.
|
||||
// Two draws share a CSO handle exactly when their pipeline bytes are equal, and
|
||||
// the client's subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||
// so the handle discriminates at least as finely as the hash it replaces.
|
||||
//
|
||||
// renderPassHash STAYS beside it and is what keeps this key complete: the CSO
|
||||
// carries GL state only, while colorAttachmentCount and the rasterization sample
|
||||
// count - which ComputePipelineStateHash folded in through its signature and
|
||||
// through ResolveEffectiveSampleMask - are render-pass facts that the render-pass
|
||||
// hash already separates.
|
||||
//
|
||||
// Null in an entry minted by the legacy arm, so entries of the two arms can never
|
||||
// match each other: the compare below tests BOTH components.
|
||||
MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||
@@ -769,19 +865,142 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 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 /
|
||||
// transform flags) do not already pin down. Equal hash under an equal rest
|
||||
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the
|
||||
// render-state CSO this draw is keyed on, or the null handle when the pre-handle arm
|
||||
// is the one that runs.
|
||||
//
|
||||
// Under the handle arm the memo's state key IS this handle. The client hashed those
|
||||
// 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so
|
||||
// recomputing an overlapping hash here was work the boundary had already done; the
|
||||
// client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read,
|
||||
// so the handle discriminates at least as finely as the hash it replaces. What the
|
||||
// handle does NOT carry is the render-pass side - colorAttachmentCount and the
|
||||
// rasterization sample count, which ComputePipelineStateHash folded in through its
|
||||
// signature and through ResolveEffectiveSampleMask - and that is exactly why
|
||||
// entry.renderPassHash stays in the key beside it.
|
||||
//
|
||||
// The arm is live only when the render-state subsystem is migrated in this run AND the
|
||||
// client has actually bound a CSO. The second half is not belt and braces: a tree whose
|
||||
// tracker does not emit create/bind_render_state yet has no handle to key on, and
|
||||
// delete_render_state clears the binding (MG_Pipe/PipeApply.cpp), so the null handle is
|
||||
// reachable on any tree. Keying every draw on it would alias every render state onto
|
||||
// one memo entry, so a null handle means "fall back to a state hash" - never an abort,
|
||||
// and never a per-draw consultation of the legacy-memo lever: bit 0 is not a Track-H
|
||||
// subsystem (D14 labels only bits 5 and 6 that), and the lever's Fatal is a STARTUP
|
||||
// one, in MagmaPipeValidateSubsystemConfiguration.
|
||||
//
|
||||
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
|
||||
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
|
||||
// at every shipped log level.
|
||||
//
|
||||
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
|
||||
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
|
||||
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
|
||||
// this comment claimed - and this site is on the per-draw pipeline path in the very
|
||||
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
|
||||
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
|
||||
// load of a member that is false exactly once. Single-threaded like the rest of the
|
||||
// renderer, and per renderer rather than per process, which is also the right scope: a
|
||||
// second context that never binds a CSO deserves to say so.
|
||||
//
|
||||
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
|
||||
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
|
||||
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
|
||||
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
|
||||
//
|
||||
// Push-only by construction: the pull build does not compile this function at all, so
|
||||
// its two callers are statement-for-statement what they were (G1).
|
||||
//
|
||||
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
|
||||
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
|
||||
// specifies. In a multi-context process this reads whatever CSO another context last
|
||||
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
|
||||
// consumer, so it is named here rather than left for both reviews to assume the other
|
||||
// caught it.
|
||||
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
|
||||
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
return MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
|
||||
m_pipelineCsoFallbackWarned = true;
|
||||
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
|
||||
"bound; the pipeline memo is running on a state hash, not on the CSO "
|
||||
"handle (no tracker on this build, or a draw between "
|
||||
"delete_render_state and the next bind)");
|
||||
}
|
||||
return boundCso;
|
||||
}
|
||||
// Latch for the warning above. Mutable because the resolve is const and the latch is
|
||||
// not part of the renderer's observable state.
|
||||
mutable Bool m_pipelineCsoFallbackWarned = false;
|
||||
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
|
||||
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
|
||||
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
|
||||
// (version, colorAttachmentCount, sampleCount) proves the bytes are unchanged.
|
||||
//
|
||||
// [deviation from D12.1] The brief deletes this gate and its cached fields outright.
|
||||
// They cannot go while a no-CSO draw is reachable - and it is, on any tree: a draw
|
||||
// between delete_render_state and the next bind has no handle. On a tree whose tracker
|
||||
// binds a CSO these five words are written once and never read again; they retire for
|
||||
// real when the pull path does, at P13.
|
||||
Uint64 ResolveFallbackPipelineStateHash(Uint renderStateVersion, Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) {
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != colorAttachmentCount ||
|
||||
m_pipelineStateHashSampleCount != rasterizationSamples) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
m_pipelineStateHash =
|
||||
ComputePipelineStateHash(colorAttachmentCount, rasterizationSamples);
|
||||
#else
|
||||
m_pipelineStateHash = ComputePipelineSubsetStateHashFallback();
|
||||
#endif
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = colorAttachmentCount;
|
||||
m_pipelineStateHashSampleCount = rasterizationSamples;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
return m_pipelineStateHash;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
#if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The same answer as ComputePipelineStateHash, computed from the P2 chunk table
|
||||
// instead of from a hand-written field list, for the build that compiles no
|
||||
// pre-handle arm (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF). It is the CLIENT's own
|
||||
// hash function - MGPipeComputePipelineSubsetHash over the 396 pipeline bytes - so a
|
||||
// draw keyed on it and a draw keyed on a CSO handle are keyed on the same equivalence
|
||||
// class of state, and the render-pass facts stay separated by renderPassHash either
|
||||
// way. This is what makes the no-legacy build RUNNABLE rather than a configuration
|
||||
// that aborts on the first draw that arrives without a CSO.
|
||||
Uint64 ComputePipelineSubsetStateHashFallback() const;
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// THE PRE-HANDLE ARM (P2 brief D12.1 / D14). Hash of every fixed-function GL state the
|
||||
// pipeline payload reads that the memo key's other fields (mode / program / vertex
|
||||
// input / render pass / transform flags) do not already pin down. Equal hash under an
|
||||
// equal rest of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
//
|
||||
// The handle arm computes none of this: the client hashed the same bytes when it
|
||||
// minted the CSO, so all five cached-hash members below exist only to avoid a
|
||||
// re-hash the handle arm never performs.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
|
||||
VkSampleCountFlagBits rasterizationSamples) const;
|
||||
#endif
|
||||
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
|
||||
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
|
||||
// and the pipeline-state memo word so the two cannot disagree.
|
||||
// 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;
|
||||
// ResolveFallbackPipelineStateHash's cache. Written once and never read again on a
|
||||
// build whose client binds a render-state CSO; see that function for why it survives
|
||||
// the re-key at all.
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
// The sample count the cached hash was computed at. A pipeline-state input now depends on
|
||||
@@ -807,7 +1026,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||
// context whose monotonic version the cache is keyed on.
|
||||
// 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() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
@@ -898,6 +1118,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// common shape), and "the VAO did not move" would then skip the layout
|
||||
// re-resolve for a different VAO.
|
||||
Uint64 vaoLifetimeId = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.4: the handle arm's answer to the same question, and one compare rather
|
||||
// than the pair above. Kept BESIDE them rather than replacing them because the
|
||||
// pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot
|
||||
// is a value struct, not a wire type.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
Uint32 vaoConfigVersion = 0;
|
||||
const void* drawFbo = nullptr;
|
||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
||||
@@ -1172,6 +1399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||
// hash, per-binding live buffer pointers and slice epochs).
|
||||
struct alignas(64) VaoDrawMemo {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an
|
||||
// identity, so the pointer-plus-lifetime-id pair below stops being a key here;
|
||||
// the slot also picks the table entry, so the address hash and the two-way probe
|
||||
// go with it. Null in an entry that has never been claimed.
|
||||
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||
@@ -1198,8 +1432,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||
// stable for the duration of a draw, which the EBO memo handoff
|
||||
// (m_currentDrawResolvedEntry) relies on.
|
||||
//
|
||||
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
|
||||
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
|
||||
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
|
||||
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
|
||||
// whole of review v2's MAJOR 1:
|
||||
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
|
||||
// table with the live VAO set is megabytes on a platform with an LMK, where the
|
||||
// other two memos are 48 B and can afford it;
|
||||
// * this is the ONLY one of the three memos that had a capacity before this package.
|
||||
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
|
||||
// cost on the base ref, so at any working-set size this table is no worse than what
|
||||
// it replaces - and strictly better below capacity, where the handle is a bijection
|
||||
// with the slot and the two-way probe never collides at all. The other two memos
|
||||
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
|
||||
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
|
||||
// derive the same handle for the same VAO. Per renderer, never a process-global: a
|
||||
// global would share one table and one reclamation clock across two live contexts and
|
||||
// outlive every one of them (review v2 minor 4).
|
||||
MagmaPipeIdentityTables m_pipeIdentity;
|
||||
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
|
||||
// first, so there is no second memo in front of it here.
|
||||
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
|
||||
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
|
||||
vao.GetLifetimeId());
|
||||
}
|
||||
#endif
|
||||
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
|
||||
// memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL
|
||||
// build's three readers keep compiling to the very same two loads they always did -
|
||||
// G1 admits no resize, and an out-of-line call here would be one.
|
||||
[[gnu::always_inline]] inline Bool VaoContentHashIfKnown(
|
||||
const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash);
|
||||
#else
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#endif
|
||||
}
|
||||
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
|
||||
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
|
||||
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
|
||||
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
const char* MGPipeVerbName(MGPipeVerb verb) {
|
||||
const auto index = static_cast<SizeT>(verb);
|
||||
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
|
||||
}
|
||||
|
||||
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
|
||||
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
|
||||
}
|
||||
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
|
||||
if (name == nullptr) return std::nullopt;
|
||||
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
|
||||
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
namespace {
|
||||
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
|
||||
|
||||
// Every overload is declared up front: the array overloads recurse into their element
|
||||
// type, and a call inside a template only sees what was declared before the template.
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b);
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
|
||||
template <class T>
|
||||
void CorruptStorage(T& v);
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p);
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p);
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]);
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v);
|
||||
|
||||
// ---- equality over one field's storage ----
|
||||
// O-class storage compares by identity: a raw pointer into the context, or the object a
|
||||
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
|
||||
// C arrays element-wise.
|
||||
template <class T>
|
||||
Bool StorageEqual(T* const& a, T* const& b) {
|
||||
return a == b;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
|
||||
return a.get() == b.get();
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
|
||||
for (SizeT i = 0; i < N; ++i) {
|
||||
if (!StorageEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
|
||||
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
|
||||
}
|
||||
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
|
||||
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
|
||||
// what turns a fourth member into a build break rather than a blind spot.
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
|
||||
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
|
||||
"CurrentVertexAttributeValue grew a member; update the comparator");
|
||||
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b) {
|
||||
return MGPipeFieldEqual(a, b);
|
||||
}
|
||||
|
||||
// ---- corruption of one field's storage ----
|
||||
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
|
||||
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
|
||||
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
|
||||
// flipped address with no control block, an array corrupts its first element, and any
|
||||
// other struct has its first byte XOR'ed with 0x5A.
|
||||
template <class T>
|
||||
T* FlipPointer(T* p) {
|
||||
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p) {
|
||||
p = FlipPointer(p);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p) {
|
||||
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]) {
|
||||
CorruptStorage(a[0]);
|
||||
}
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
|
||||
CorruptStorage(c.Blend);
|
||||
}
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v) {
|
||||
v.floatValue[0] += 1.f;
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T& v) {
|
||||
if constexpr (std::is_same_v<T, Bool>) {
|
||||
v = !v;
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
|
||||
} else if constexpr (std::is_arithmetic_v<T>) {
|
||||
v = static_cast<T>(v + 1);
|
||||
} else {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
|
||||
unsigned char first = 0;
|
||||
std::memcpy(&first, &v, 1);
|
||||
first ^= 0x5A;
|
||||
std::memcpy(&v, &first, 1);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
|
||||
// A forwarded field has no storage and is equal by definition; VisitStorage answers
|
||||
// false for it, hence the explicit sticky test first.
|
||||
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
|
||||
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
|
||||
}
|
||||
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField) {
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
|
||||
if (outField != nullptr) *outField = field;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
|
||||
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
|
||||
CorruptStorage(x);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,736 @@
|
||||
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.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 <MG_Pipe/MGPipe.h>
|
||||
// The frontend types the accessors return. Allowed here: P13 keeps this include for the
|
||||
// verify arm (ARCHITECTURE.md 9.5). This header spells no MG_State global - every read of
|
||||
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
|
||||
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
|
||||
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
|
||||
// there without dragging MG_Remote in.
|
||||
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
|
||||
MOBILEGL_PIPE_VERIFY)
|
||||
#define MOBILEGL_PIPE_POISON 1
|
||||
#else
|
||||
#define MOBILEGL_PIPE_POISON 0
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
|
||||
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
|
||||
// MOBILEGL_ASSERT, which is inert in INFO builds.
|
||||
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
|
||||
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
|
||||
const char* MGPipeVerbName(MGPipeVerb verb);
|
||||
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
|
||||
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
|
||||
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
|
||||
|
||||
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
|
||||
// a read of a field whose stamp is older than the current verb serial is
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
#define MGP_INPUT_CHECK(Field) \
|
||||
do { \
|
||||
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
|
||||
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
|
||||
} \
|
||||
} while (0)
|
||||
#else
|
||||
#define MGP_INPUT_CHECK(Field) ((void)0)
|
||||
#endif
|
||||
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
|
||||
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
|
||||
// against the stored value, and reports the FIRST divergence as
|
||||
// Fatal{PipeVerifyDiffer, "Field@Verb", verb=<serial>, where=read} (the indices go in a
|
||||
// preceding MGLOG_E). Only the live block (gPipeInputs) is verified; a snapshot's own
|
||||
// accessors are plain loads. Off in every other build.
|
||||
struct PipeInputs;
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1);
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) \
|
||||
::MobileGL::MG_Pipe::MGPipeVerifyReadHook(*this, (Field), static_cast<Uint>(Index0), static_cast<Uint>(Index1))
|
||||
#else
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
|
||||
#endif
|
||||
|
||||
// The V/O storage of every field that has storage, by field id. The seven F-class
|
||||
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
|
||||
// is what keeps the comparator and the corruption injector one function each instead of
|
||||
// two sixty-way switches.
|
||||
// clang-format off
|
||||
#define MGP_INPUT_STORAGE_LIST(X) \
|
||||
X(GetActiveTextureUnit, m_activeTextureUnit) \
|
||||
X(GetBlendColor, m_blendColor) \
|
||||
X(GetBlendEquationIndexed, m_blendEquation) \
|
||||
X(GetBlendFuncIndexed, m_blendFunc) \
|
||||
X(GetBoundTransformFeedbackName, m_boundTransformFeedbackName) \
|
||||
X(GetBoundVertexArray, m_boundVertexArray) \
|
||||
X(GetBufferBindingSlot, m_bufferBindingSlot) \
|
||||
X(GetBufferBindingPoint, m_bufferBindingPointBase) \
|
||||
X(GetTouchedBufferBindingPointCount, m_touchedBindingPointCount) \
|
||||
X(GetClampReadColor, m_clampReadColor) \
|
||||
X(GetClearColor, m_clearColor) \
|
||||
X(GetClearDepth, m_clearDepth) \
|
||||
X(GetClearStencil, m_clearStencil) \
|
||||
X(GetColorMaskIndexed, m_colorMask) \
|
||||
X(GetCullFaceMode, m_cullFaceMode) \
|
||||
X(GetCurrentVertexAttribute, m_currentVertexAttribute) \
|
||||
X(GetDepthFunc, m_depthFunc) \
|
||||
X(GetDepthMask, m_depthMask) \
|
||||
X(GetDepthRangeIndexed, m_depthRange) \
|
||||
X(GetFramebufferBindingSlot, m_framebufferBindingSlot) \
|
||||
X(GetImageTextureBinding, m_imageTextureBindingBase) \
|
||||
X(GetLineWidth, m_lineWidth) \
|
||||
X(GetLogicOp, m_logicOp) \
|
||||
X(GetMaxTouchedTextureUnit, m_maxTouchedTextureUnit) \
|
||||
X(GetMinSampleShadingValue, m_minSampleShadingValue) \
|
||||
X(GetPatchDefaultInnerLevel, m_patchDefaultInnerLevel) \
|
||||
X(GetPatchDefaultOuterLevel, m_patchDefaultOuterLevel) \
|
||||
X(GetPatchVertices, m_patchVertices) \
|
||||
X(GetPipelineStateVersion, m_pipelineStateVersion) \
|
||||
X(GetPixelStoreParameters, m_pixelStore) \
|
||||
X(GetPolygonModeFront, m_polygonModeFront) \
|
||||
X(GetPolygonOffsetFactor, m_polygonOffsetFactor) \
|
||||
X(GetPolygonOffsetUnits, m_polygonOffsetUnits) \
|
||||
X(GetPrimitiveRestartIndex, m_primitiveRestartIndex) \
|
||||
X(GetProgramForDispatch, m_programForDispatch) \
|
||||
X(GetProgramForDraw, m_programForDraw) \
|
||||
X(GetProvokingVertexMode, m_provokingVertexMode) \
|
||||
X(GetRenderStateParameters, m_renderState) \
|
||||
X(GetRenderStateParametersVersion, m_renderStateParametersVersion) \
|
||||
X(GetSamplingResolutionGeneration, m_samplingResolutionGeneration) \
|
||||
X(GetScissorBox, m_scissorBox) \
|
||||
X(GetStencilState, m_stencil) \
|
||||
X(GetTextureBindGeneration, m_textureBindGeneration) \
|
||||
X(GetTextureContextId, m_textureContextId) \
|
||||
X(GetTextureUnitObject, m_textureUnitBase) \
|
||||
X(GetTransformFeedbackCapturedVertices, m_transformFeedbackCapturedVertices) \
|
||||
X(GetTransformFeedbackGeneration, m_transformFeedbackGeneration) \
|
||||
X(GetTransformFeedbackPausedPrimitiveCounter, m_transformFeedbackPausedPrimitiveCounter) \
|
||||
X(GetTransformFeedbackProgram, m_transformFeedbackProgram) \
|
||||
X(GetViewport, m_viewport) \
|
||||
X(GetViewportIndexed, m_viewportIndexed) \
|
||||
X(IsCapabilityEnabled, m_capability) \
|
||||
X(IsCapabilityEnabledIndexed, m_capabilityIndexed) \
|
||||
X(IsTransformFeedbackActive, m_transformFeedbackActive) \
|
||||
X(IsTransformFeedbackPaused, m_transformFeedbackPaused) \
|
||||
X(GetBoundTransformFeedbackLifetimeId, m_boundTransformFeedbackLifetimeId)
|
||||
// clang-format on
|
||||
|
||||
// The seven F-class fields, for the arithmetic below and for the sticky table's proof.
|
||||
// The forwarded set IS the sticky set (PipeFields.def marks the same seven rows F and
|
||||
// sticky), so an eighth sticky row without a forwarder is refused here, not by a test.
|
||||
inline constexpr SizeT kMGPipeForwardedFieldCount = 7;
|
||||
static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount,
|
||||
"the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows");
|
||||
|
||||
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
|
||||
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
|
||||
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
|
||||
// sed is type-neutral:
|
||||
//
|
||||
// V (value) copied out of GLContext at fill time by calling the same accessor;
|
||||
// no derivation logic is re-implemented here, which is what keeps the
|
||||
// copy semantically identical by construction.
|
||||
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
|
||||
// slot/array for the accessors that return a non-const reference into
|
||||
// the context. Identity is what phase C turns into a handle.
|
||||
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
|
||||
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
|
||||
// context may be spelled). Sticky: stamped once by the first fill that
|
||||
// sees a live context.
|
||||
//
|
||||
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
|
||||
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
|
||||
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
|
||||
struct PipeInputs {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using BufferObject = MG_State::GLState::BufferObject;
|
||||
using BufferTarget = ::MobileGL::BufferTarget;
|
||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||
using FramebufferTarget = ::MobileGL::FramebufferTarget;
|
||||
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
|
||||
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||
using ITextureObject = MG_State::GLState::ITextureObject;
|
||||
using TextureUnit = MG_State::GLState::TextureUnit;
|
||||
using ImageTextureBinding = MG_State::GLState::ImageTextureBinding;
|
||||
using CurrentVertexAttributeValue = MG_State::GLState::CurrentVertexAttributeValue;
|
||||
|
||||
static constexpr SizeT kBufferTargetCount = static_cast<SizeT>(BufferTarget::BufferTargetCount);
|
||||
static constexpr SizeT kFramebufferTargetCount = static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount);
|
||||
static constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
|
||||
static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
|
||||
static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS;
|
||||
static constexpr SizeT kStencilFaceCount = static_cast<SizeT>(StencilFace::StencilFaceCount);
|
||||
|
||||
// IsCapabilityEnabledIndexed's two indexed capabilities, the only ones GLContext keeps
|
||||
// indexed state for (RenderState::IsCapabilityEnabledIndexed).
|
||||
struct IndexedCapabilities {
|
||||
Bool Blend[kMGMaxDrawBuffers];
|
||||
Bool ScissorTest[kMaxViewports];
|
||||
};
|
||||
|
||||
// ---- identity / liveness (not fields) ----
|
||||
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
|
||||
// must be true as soon as a context exists, fill or no fill, which is what today's
|
||||
// null-context guards test.
|
||||
Bool IsLive() const;
|
||||
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
|
||||
const void* ContextIdentity() const { return m_contextIdentity; }
|
||||
// The verb of the last fill, kVerbCount before the first one.
|
||||
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
const MGPipeFilledState& FilledState() const { return m_filled; }
|
||||
#endif
|
||||
|
||||
// ---- V: values ----
|
||||
Int GetActiveTextureUnit() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetActiveTextureUnit);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetActiveTextureUnit, 0, 0);
|
||||
return m_activeTextureUnit;
|
||||
}
|
||||
const FloatVec4& GetBlendColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendColor, 0, 0);
|
||||
return m_blendColor;
|
||||
}
|
||||
void GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendEquationIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendEquationIndexed, index, 0);
|
||||
if (index >= kMGMaxDrawBuffers) {
|
||||
MOBILEGL_ASSERT(false, "Blend equation index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
color = m_blendEquation[index][0];
|
||||
alpha = m_blendEquation[index][1];
|
||||
}
|
||||
void GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
|
||||
BlendFactor& dstAlpha) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBlendFuncIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendFuncIndexed, index, 0);
|
||||
if (index >= kMGMaxDrawBuffers) {
|
||||
MOBILEGL_ASSERT(false, "Blend func index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
srcRGB = m_blendFunc[index][0];
|
||||
dstRGB = m_blendFunc[index][1];
|
||||
srcAlpha = m_blendFunc[index][2];
|
||||
dstAlpha = m_blendFunc[index][3];
|
||||
}
|
||||
// Dead field: filled, read by no backend since the D21 XFB counter-slot rekey; kept so
|
||||
// the vendored inventory row keeps its mapping (Coverage.def).
|
||||
Uint GetBoundTransformFeedbackName() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackName);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackName, 0, 0);
|
||||
return m_boundTransformFeedbackName;
|
||||
}
|
||||
SizeT GetTouchedBufferBindingPointCount(BufferTarget target) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTouchedBufferBindingPointCount);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTouchedBufferBindingPointCount, static_cast<Uint>(target), 0);
|
||||
return m_touchedBindingPointCount[static_cast<SizeT>(target)];
|
||||
}
|
||||
GLenum GetClampReadColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClampReadColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClampReadColor, 0, 0);
|
||||
return m_clampReadColor;
|
||||
}
|
||||
const FloatVec4& GetClearColor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearColor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearColor, 0, 0);
|
||||
return m_clearColor;
|
||||
}
|
||||
Float GetClearDepth() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearDepth);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearDepth, 0, 0);
|
||||
return m_clearDepth;
|
||||
}
|
||||
Uint32 GetClearStencil() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetClearStencil);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearStencil, 0, 0);
|
||||
return m_clearStencil;
|
||||
}
|
||||
BoolVec4 GetColorMaskIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetColorMaskIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetColorMaskIndexed, index, 0);
|
||||
return m_colorMask[index];
|
||||
}
|
||||
CullFaceMode GetCullFaceMode() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetCullFaceMode);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCullFaceMode, 0, 0);
|
||||
return m_cullFaceMode;
|
||||
}
|
||||
const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetCurrentVertexAttribute);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCurrentVertexAttribute, index, 0);
|
||||
if (index >= kMaxVertexAttribs) {
|
||||
static const CurrentVertexAttributeValue defaultValue{};
|
||||
MGLOG_E_ONCE("PipeInputs::GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
return defaultValue;
|
||||
}
|
||||
return m_currentVertexAttribute[index];
|
||||
}
|
||||
DepthTestFunc GetDepthFunc() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthFunc);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthFunc, 0, 0);
|
||||
return m_depthFunc;
|
||||
}
|
||||
Bool GetDepthMask() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthMask);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthMask, 0, 0);
|
||||
return m_depthMask;
|
||||
}
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetDepthRangeIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthRangeIndexed, index, 0);
|
||||
if (index >= kMaxViewports) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_depthRange[0];
|
||||
}
|
||||
return m_depthRange[index];
|
||||
}
|
||||
Float GetLineWidth() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetLineWidth);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLineWidth, 0, 0);
|
||||
return m_lineWidth;
|
||||
}
|
||||
LogicOperation GetLogicOp() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetLogicOp);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLogicOp, 0, 0);
|
||||
return m_logicOp;
|
||||
}
|
||||
Int GetMaxTouchedTextureUnit() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetMaxTouchedTextureUnit);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMaxTouchedTextureUnit, 0, 0);
|
||||
return m_maxTouchedTextureUnit;
|
||||
}
|
||||
Float GetMinSampleShadingValue() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetMinSampleShadingValue);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMinSampleShadingValue, 0, 0);
|
||||
return m_minSampleShadingValue;
|
||||
}
|
||||
const FloatVec2& GetPatchDefaultInnerLevel() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultInnerLevel);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultInnerLevel, 0, 0);
|
||||
return m_patchDefaultInnerLevel;
|
||||
}
|
||||
const FloatVec4& GetPatchDefaultOuterLevel() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultOuterLevel);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultOuterLevel, 0, 0);
|
||||
return m_patchDefaultOuterLevel;
|
||||
}
|
||||
Uint GetPatchVertices() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPatchVertices);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchVertices, 0, 0);
|
||||
return m_patchVertices;
|
||||
}
|
||||
Uint GetPipelineStateVersion() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPipelineStateVersion);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPipelineStateVersion, 0, 0);
|
||||
return m_pipelineStateVersion;
|
||||
}
|
||||
Uint GetRenderStateParametersVersion() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParametersVersion);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
|
||||
return m_renderStateParametersVersion;
|
||||
}
|
||||
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
|
||||
return m_pixelStore[isUnpack ? 1 : 0];
|
||||
}
|
||||
GLenum GetPolygonModeFront() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonModeFront);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonModeFront, 0, 0);
|
||||
return m_polygonModeFront;
|
||||
}
|
||||
Float GetPolygonOffsetFactor() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetFactor);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetFactor, 0, 0);
|
||||
return m_polygonOffsetFactor;
|
||||
}
|
||||
Float GetPolygonOffsetUnits() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetUnits);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetUnits, 0, 0);
|
||||
return m_polygonOffsetUnits;
|
||||
}
|
||||
Uint32 GetPrimitiveRestartIndex() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetPrimitiveRestartIndex);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPrimitiveRestartIndex, 0, 0);
|
||||
return m_primitiveRestartIndex;
|
||||
}
|
||||
ProvokingVertexMode GetProvokingVertexMode() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProvokingVertexMode);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProvokingVertexMode, 0, 0);
|
||||
return m_provokingVertexMode;
|
||||
}
|
||||
const RenderStateParameters& GetRenderStateParameters() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParameters);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParameters, 0, 0);
|
||||
return m_renderState;
|
||||
}
|
||||
Uint64 GetSamplingResolutionGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
|
||||
return m_samplingResolutionGeneration;
|
||||
}
|
||||
const IntVec4& GetScissorBox() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetScissorBox);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetScissorBox, 0, 0);
|
||||
return m_scissorBox;
|
||||
}
|
||||
const StencilFaceState& GetStencilState(StencilFace face) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetStencilState);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetStencilState, static_cast<Uint>(face), 0);
|
||||
return m_stencil[face == StencilFace::Back ? 1 : 0];
|
||||
}
|
||||
Uint64 GetTextureBindGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
|
||||
return m_textureBindGeneration;
|
||||
}
|
||||
Uint64 GetTextureContextId() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
|
||||
return m_textureContextId;
|
||||
}
|
||||
Uint64 GetTransformFeedbackCapturedVertices() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackCapturedVertices);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackCapturedVertices, 0, 0);
|
||||
return m_transformFeedbackCapturedVertices;
|
||||
}
|
||||
Uint64 GetTransformFeedbackGeneration() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackGeneration);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackGeneration, 0, 0);
|
||||
return m_transformFeedbackGeneration;
|
||||
}
|
||||
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter, 0, 0);
|
||||
return m_transformFeedbackPausedPrimitiveCounter;
|
||||
}
|
||||
Uint64 GetBoundTransformFeedbackLifetimeId() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackLifetimeId);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackLifetimeId, 0, 0);
|
||||
return m_boundTransformFeedbackLifetimeId;
|
||||
}
|
||||
IntVec4 GetViewport() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetViewport);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewport, 0, 0);
|
||||
return m_viewport;
|
||||
}
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetViewportIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewportIndexed, index, 0);
|
||||
if (index >= kMaxViewports) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_viewportIndexed[0];
|
||||
}
|
||||
return m_viewportIndexed[index];
|
||||
}
|
||||
Bool IsCapabilityEnabled(CapabilityInput cap) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabled);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabled, static_cast<Uint>(cap), 0);
|
||||
const auto index = static_cast<SizeT>(cap);
|
||||
return index < kCapabilityCount ? m_capability[index] : false;
|
||||
}
|
||||
// Blend and ScissorTest are the only indexed capabilities GLContext keeps; no backend
|
||||
// asks for another (VulkanRenderer asks Blend). Any other cap is a read the fill cannot
|
||||
// have served: Fatal{UnmigratedPipeInput} naming the field and the verb, the cap in a
|
||||
// preceding MGLOG_E.
|
||||
Bool IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabledIndexed);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabledIndexed, static_cast<Uint>(cap), index);
|
||||
if (cap == CapabilityInput::Blend) {
|
||||
return index < kMGMaxDrawBuffers ? m_capabilityIndexed.Blend[index] : false;
|
||||
}
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
return index < kMaxViewports ? m_capabilityIndexed.ScissorTest[index] : false;
|
||||
}
|
||||
MGLOG_E("PipeInputs::IsCapabilityEnabledIndexed: no indexed storage for cap=%d (index=%u)",
|
||||
static_cast<int>(cap), index);
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::IsCapabilityEnabledIndexed, m_currentVerb);
|
||||
}
|
||||
Bool IsTransformFeedbackActive() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackActive);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackActive, 0, 0);
|
||||
return m_transformFeedbackActive;
|
||||
}
|
||||
Bool IsTransformFeedbackPaused() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackPaused);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackPaused, 0, 0);
|
||||
return m_transformFeedbackPaused;
|
||||
}
|
||||
|
||||
// ---- O: object references ----
|
||||
const SharedPtr<VertexArrayObject>& GetBoundVertexArray() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBoundVertexArray);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundVertexArray, 0, 0);
|
||||
return m_boundVertexArray;
|
||||
}
|
||||
// A target the fill left null (one outside GlobalBufferTargets / BufferBindPointTargets,
|
||||
// or a read before any fill) is a read the fill cannot have served: the poison Fatal,
|
||||
// the target in a preceding MGLOG_E.
|
||||
BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget target) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast<Uint>(target), 0);
|
||||
const auto index = static_cast<SizeT>(target);
|
||||
if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast<int>(target));
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb);
|
||||
}
|
||||
return *m_bufferBindingSlot[index];
|
||||
}
|
||||
BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget target, Uint index) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast<Uint>(target), index);
|
||||
const auto targetIndex = static_cast<SizeT>(target);
|
||||
if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)",
|
||||
static_cast<int>(target), index);
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb);
|
||||
}
|
||||
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
|
||||
// (BufferState.h), so base[index] is the live slot GLContext would hand out.
|
||||
return m_bufferBindingPointBase[targetIndex][index];
|
||||
}
|
||||
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast<Uint>(target), 0);
|
||||
const auto index = static_cast<SizeT>(target);
|
||||
if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) {
|
||||
MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast<int>(target));
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetFramebufferBindingSlot, m_currentVerb);
|
||||
}
|
||||
return *m_framebufferBindingSlot[index];
|
||||
}
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
|
||||
if (m_imageTextureBindingBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
|
||||
}
|
||||
return m_imageTextureBindingBase[unit];
|
||||
}
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
|
||||
if (m_imageTextureBindingBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
|
||||
}
|
||||
return m_imageTextureBindingBase[unit];
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetProgramForDispatch() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0);
|
||||
return m_programForDispatch;
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetProgramForDraw() {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0);
|
||||
return m_programForDraw;
|
||||
}
|
||||
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackProgram);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackProgram, 0, 0);
|
||||
return m_transformFeedbackProgram;
|
||||
}
|
||||
TextureUnit& GetTextureUnitObject(Int unit) {
|
||||
MGP_INPUT_CHECK(MGPipeInputField::GetTextureUnitObject);
|
||||
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureUnitObject, static_cast<Uint>(unit), 0);
|
||||
if (m_textureUnitBase == nullptr) {
|
||||
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetTextureUnitObject, m_currentVerb);
|
||||
}
|
||||
return m_textureUnitBase[unit];
|
||||
}
|
||||
|
||||
// ---- F: forwarded to the live context (MG_Impl/Pipe/PipeFill.cpp); sticky ----
|
||||
// Each takes an argument that is not verb state - a GL name, a lifetime id, a target -
|
||||
// i.e. it is a lookup or a reverse-channel write, not a state read; there is no value
|
||||
// the filler could copy and no verb whose fill could make it stale. Phase C replaces
|
||||
// them with handle tables and callbacks.
|
||||
// They carry no MGP_INPUT_CHECK / MGP_INPUT_VERIFY_READ (the declared exception to
|
||||
// P1 brief D4's "every accessor body"): a forward is a live call, not a stored value,
|
||||
// and InvalidateCompileEnv is reached from backend initialisation before any verb has
|
||||
// filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp
|
||||
// is therefore consulted by no accessor; the tests pin it through
|
||||
// MGPipeInputFieldIsFresh directly.
|
||||
SizeT GetBufferBindingPointCount(BufferTarget target) const;
|
||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
||||
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
|
||||
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
|
||||
void InvalidateCompileEnv();
|
||||
Bool ValidateProgramName(Uint index) const;
|
||||
// Dropped with an MGLOG_E_ONCE when no context is live; today's guarded sites never
|
||||
// reach it without one.
|
||||
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
|
||||
|
||||
// ---- the storage visitor ----
|
||||
// Calls fn(a.<member>, b.<member>) for the field's storage and returns its result; returns
|
||||
// false without calling fn for a forwarded field, which has none. The comparator's
|
||||
// per-field equality and the verify corruption injector are both one call of this.
|
||||
template <class Fn>
|
||||
static Bool VisitStorage(MGPipeInputField field, PipeInputs& a, PipeInputs& b, Fn&& fn) {
|
||||
switch (field) {
|
||||
#define MGP_INPUT_VISIT(Field, Member) \
|
||||
case MGPipeInputField::Field: \
|
||||
return fn(a.Member, b.Member);
|
||||
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
|
||||
#undef MGP_INPUT_VISIT
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
template <class Fn>
|
||||
static Bool VisitStorage(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b, Fn&& fn) {
|
||||
switch (field) {
|
||||
#define MGP_INPUT_VISIT(Field, Member) \
|
||||
case MGPipeInputField::Field: \
|
||||
return fn(a.Member, b.Member);
|
||||
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
|
||||
#undef MGP_INPUT_VISIT
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// 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.
|
||||
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 ----
|
||||
const void* m_contextIdentity = nullptr;
|
||||
Bool m_live = false;
|
||||
MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState m_filled{};
|
||||
#endif
|
||||
|
||||
// ---- V ----
|
||||
Int m_activeTextureUnit = 0;
|
||||
FloatVec4 m_blendColor{};
|
||||
BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]{};
|
||||
BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]{};
|
||||
Uint m_boundTransformFeedbackName = 0;
|
||||
SizeT m_touchedBindingPointCount[kBufferTargetCount]{};
|
||||
GLenum m_clampReadColor = 0;
|
||||
FloatVec4 m_clearColor{};
|
||||
Float m_clearDepth = 0.f;
|
||||
Uint32 m_clearStencil = 0;
|
||||
BoolVec4 m_colorMask[kMGMaxDrawBuffers]{};
|
||||
CullFaceMode m_cullFaceMode{};
|
||||
CurrentVertexAttributeValue m_currentVertexAttribute[kMaxVertexAttribs]{};
|
||||
DepthTestFunc m_depthFunc{};
|
||||
Bool m_depthMask = false;
|
||||
FloatVec2 m_depthRange[kMaxViewports]{};
|
||||
Float m_lineWidth = 0.f;
|
||||
LogicOperation m_logicOp{};
|
||||
Int m_maxTouchedTextureUnit = -1;
|
||||
Float m_minSampleShadingValue = 0.f;
|
||||
FloatVec2 m_patchDefaultInnerLevel{};
|
||||
FloatVec4 m_patchDefaultOuterLevel{};
|
||||
Uint m_patchVertices = 0;
|
||||
Uint m_pipelineStateVersion = 0;
|
||||
Uint m_renderStateParametersVersion = 0;
|
||||
PixelStoreParameters m_pixelStore[2]{}; // [0] = pack, [1] = unpack
|
||||
GLenum m_polygonModeFront = 0;
|
||||
Float m_polygonOffsetFactor = 0.f;
|
||||
Float m_polygonOffsetUnits = 0.f;
|
||||
Uint32 m_primitiveRestartIndex = 0;
|
||||
ProvokingVertexMode m_provokingVertexMode{};
|
||||
RenderStateParameters m_renderState{};
|
||||
Uint64 m_samplingResolutionGeneration = 0;
|
||||
Uint64 m_textureBindGeneration = 0;
|
||||
Uint64 m_textureContextId = 0;
|
||||
IntVec4 m_scissorBox{};
|
||||
StencilFaceState m_stencil[kStencilFaceCount]{};
|
||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||
Uint64 m_transformFeedbackGeneration = 0;
|
||||
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
|
||||
Uint64 m_boundTransformFeedbackLifetimeId = 0;
|
||||
IntVec4 m_viewport{};
|
||||
FloatVec4 m_viewportIndexed[kMaxViewports]{};
|
||||
Bool m_capability[kCapabilityCount]{};
|
||||
IndexedCapabilities m_capabilityIndexed{};
|
||||
Bool m_transformFeedbackActive = false;
|
||||
Bool m_transformFeedbackPaused = false;
|
||||
|
||||
// ---- O ----
|
||||
SharedPtr<VertexArrayObject> m_boundVertexArray;
|
||||
BindingSlot<BufferObject>* m_bufferBindingSlot[kBufferTargetCount]{};
|
||||
BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[kBufferTargetCount]{};
|
||||
BindingSlot<FramebufferObject>* m_framebufferBindingSlot[kFramebufferTargetCount]{};
|
||||
ImageTextureBinding* m_imageTextureBindingBase = nullptr;
|
||||
SharedPtr<ProgramObject> m_programForDispatch;
|
||||
SharedPtr<ProgramObject> m_programForDraw;
|
||||
SharedPtr<ProgramObject> m_transformFeedbackProgram;
|
||||
TextureUnit* m_textureUnitBase = nullptr;
|
||||
};
|
||||
|
||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||
// variable: no .cpp is needed for the definition.
|
||||
//
|
||||
// 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.
|
||||
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
||||
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
|
||||
"MGP_INPUT_STORAGE_LIST plus the seven forwarded fields is not the PipeInputs field set");
|
||||
#undef MGP_INPUT_COUNT_ONE
|
||||
// The docs budget ~20 KB; the block is a few KB.
|
||||
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
|
||||
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
|
||||
// always equal (no storage).
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b);
|
||||
// PipeInputs.cpp. The entry compare: every field in `mask` of the pushed block against the
|
||||
// snapshot, first differing field out. Exported from the shared library on purpose - the
|
||||
// retrace-verify CI job proves it swapped in a verify build by finding this symbol with
|
||||
// nm -D, so a "green" run against a library without the comparator cannot happen.
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField);
|
||||
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
|
||||
// scalar, ^0x5A the first byte of a struct, flip a pointer's low bits - never
|
||||
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
|
||||
// which has nothing to corrupt.
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -11,5 +11,51 @@ endif()
|
||||
add_executable(DriverBench DriverBench.c)
|
||||
target_link_libraries(DriverBench PRIVATE dl)
|
||||
|
||||
# WHY EVERY ENTRY HERE CARRIES A PASS_REGULAR_EXPRESSION.
|
||||
#
|
||||
# DriverBench prints one CSV row per case it ran and exits 0 whatever it ran. Before this, a ctest
|
||||
# entry naming a case therefore could not answer the only question it exists to ask: an argument
|
||||
# matching nothing in kBenchCases selected no case, printed only the header row, and still exited
|
||||
# 0. DriverBench.c now refuses an unknown case name (exit 2), which closes it at the source - but
|
||||
# the entry must be able to go red for the reason it exists WITHOUT depending on that check
|
||||
# staying in the binary, so each entry also requires the case's own output row to appear.
|
||||
#
|
||||
# The regex is what a healthy run of that case prints and nothing else does: the case name at the
|
||||
# start of a line, then the frames / ops-per-frame / median-ms / ns-per-op / fps columns
|
||||
# (run_case()). A rename, a drop from kBenchCases, a boot_egl() failure or
|
||||
# a crash part-way through the case all remove that row and turn the entry red.
|
||||
#
|
||||
# Note that a PASS_REGULAR_EXPRESSION makes ctest ignore the process exit code (cmCTestRunTest:
|
||||
# success is `retVal == 0 || !RequiredRegularExpressions.empty()`), which is why the row itself
|
||||
# has to be the evidence rather than a companion to the rc.
|
||||
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||
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_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||
|
||||
/* A requested case name that matches nothing used to select nothing, print the header row and
|
||||
* exit 0 - so a caller that names a case (run_driver_bench.sh, and the two ctest entries in
|
||||
* CMakeLists.txt) could not tell "the case ran" from "the case has been renamed or deleted".
|
||||
* Refuse it here, before any GL work, so the refusal reaches a caller that has no display
|
||||
* either, and name what does exist so the fix is obvious. */
|
||||
int unknownCases = 0;
|
||||
for (int j = 1; j < argc; ++j) {
|
||||
int known = 0;
|
||||
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||
if (strcmp(argv[j], kBenchCases[i].name) == 0) known = 1;
|
||||
if (!known) {
|
||||
fprintf(stderr, "DriverBench: no case named '%s'\n", argv[j]);
|
||||
unknownCases = 1;
|
||||
}
|
||||
}
|
||||
if (unknownCases) {
|
||||
fprintf(stderr, "DriverBench: the %d cases in kBenchCases are:\n", kBenchCaseCount);
|
||||
for (int i = 0; i < kBenchCaseCount; ++i)
|
||||
fprintf(stderr, " %s\n", kBenchCases[i].name);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (boot_egl()) return 1;
|
||||
build_resources();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/EGLState/Core.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
@@ -527,6 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(Clear);
|
||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||
}
|
||||
|
||||
@@ -535,6 +537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -544,6 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||
}
|
||||
|
||||
@@ -553,6 +557,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -562,6 +567,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||
}
|
||||
|
||||
@@ -570,6 +576,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArrays);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||
}
|
||||
|
||||
@@ -579,6 +586,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
@@ -588,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -596,6 +605,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
@@ -605,6 +615,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawElementsIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||
maxdrawcount, stride);
|
||||
}
|
||||
@@ -615,6 +626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(MultiDrawArraysIndirectCount);
|
||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||
stride);
|
||||
}
|
||||
@@ -625,6 +637,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElementsBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||
basevertex);
|
||||
}
|
||||
@@ -635,6 +648,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawRangeElements);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||
}
|
||||
|
||||
@@ -645,6 +659,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||
}
|
||||
@@ -655,6 +671,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstancedBaseVertex);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||
basevertex);
|
||||
}
|
||||
@@ -665,6 +682,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawElementsInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||
instancecount, baseinstance);
|
||||
}
|
||||
@@ -675,6 +694,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
@@ -683,6 +703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawElementsIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||
}
|
||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
@@ -691,6 +712,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_SET_BASE_INSTANCE(baseinstance);
|
||||
MGP_FILL(DrawArraysInstancedBaseInstance);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||
baseinstance);
|
||||
}
|
||||
@@ -700,6 +723,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysInstanced);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
@@ -708,6 +732,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DrawArraysIndirect);
|
||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||
}
|
||||
|
||||
@@ -739,6 +764,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
|
||||
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchCompute);
|
||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||
}
|
||||
|
||||
@@ -791,6 +817,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||
if (ConditionalRenderDiscardsCommand()) return;
|
||||
MGP_FILL(DispatchComputeIndirect);
|
||||
dispatchComputeIndirect(indirect);
|
||||
}
|
||||
|
||||
@@ -812,6 +839,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
|
||||
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
|
||||
MGP_FILL(PatchParameteri);
|
||||
patchParameteri(pname, value);
|
||||
}
|
||||
}
|
||||
@@ -882,6 +910,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
@@ -903,6 +932,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrier);
|
||||
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
}
|
||||
|
||||
@@ -916,6 +946,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support regional memory barriers."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(MemoryBarrierByRegion);
|
||||
memoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
@@ -1238,6 +1269,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||
MGP_FILL(BeginTransformFeedback);
|
||||
beginXfb(primitiveMode);
|
||||
}
|
||||
}
|
||||
@@ -1320,6 +1352,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Closed while the capture state is still active: a backend that captures
|
||||
// through its own driver reads the capture program and buffer bindings here.
|
||||
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||
MGP_FILL(EndTransformFeedback);
|
||||
endXfb();
|
||||
}
|
||||
MG_State::pGLContext->EndTransformFeedback();
|
||||
@@ -1328,9 +1361,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// the GPU work is all that is required.
|
||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
if (auto sync = backendGL.FenceSync()) {
|
||||
MGP_FILL(ClientWaitSync);
|
||||
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
|
||||
if (backendGL.DeleteSync) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendGL.DeleteSync(sync);
|
||||
}
|
||||
}
|
||||
@@ -1349,6 +1385,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(true);
|
||||
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
|
||||
MGP_FILL(PauseTransformFeedback);
|
||||
pauseXfb();
|
||||
}
|
||||
}
|
||||
@@ -1363,6 +1400,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->SetTransformFeedbackPaused(false);
|
||||
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
|
||||
MGP_FILL(ResumeTransformFeedback);
|
||||
resumeXfb();
|
||||
}
|
||||
}
|
||||
@@ -1568,6 +1606,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
continue;
|
||||
}
|
||||
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
|
||||
MGP_FILL(DeleteTransformFeedback);
|
||||
deleteXfb(id);
|
||||
}
|
||||
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
|
||||
@@ -1599,6 +1638,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MG_State::pGLContext->BindTransformFeedbackObject(id);
|
||||
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
|
||||
MGP_FILL(BindTransformFeedback);
|
||||
bindXfb(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.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/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
@@ -612,10 +621,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
|
||||
//
|
||||
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
|
||||
// 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
|
||||
|
||||
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
MGP_FILL(BlitFramebuffer);
|
||||
MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||
mask, filter);
|
||||
}
|
||||
@@ -629,6 +668,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(readFramebuffer);
|
||||
PipePublishFramebufferByName(drawFramebuffer);
|
||||
#endif
|
||||
MGP_FILL(BlitNamedFramebuffer);
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
dstY1, mask, filter);
|
||||
}
|
||||
@@ -640,6 +684,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfv);
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -650,6 +698,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferfi);
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
@@ -660,6 +712,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferiv);
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -670,6 +726,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebuffer);
|
||||
#endif
|
||||
MGP_FILL(ClearNamedFramebufferuiv);
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -1397,6 +1457,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1423,6 +1486,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
|
||||
@@ -1446,6 +1512,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1473,6 +1542,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
|
||||
@@ -1527,6 +1599,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1629,6 +1704,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
|
||||
/*layered=*/false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
|
||||
@@ -1698,6 +1776,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (renderbuffer == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1707,6 +1788,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!renderbufferObject) return;
|
||||
|
||||
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
|
||||
@@ -1906,6 +1990,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
|
||||
if (!framebufferObject) return;
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
|
||||
@@ -1920,6 +2007,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLenum bufs[] = {buf};
|
||||
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
|
||||
@@ -1929,6 +2019,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!framebufferObject) return;
|
||||
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
|
||||
"NamedFramebufferReadBuffer_State");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
PipePublishFramebufferByName(framebufferObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
|
||||
@@ -2729,24 +2822,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferfi);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||
}
|
||||
|
||||
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferfv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferuiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||
MGP_FILL(ClearBufferiv);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||
}
|
||||
|
||||
@@ -2994,6 +3091,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
@@ -1173,6 +1174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: GetMinComputeWorkGroupSize(index);
|
||||
GLint backendValue = 0;
|
||||
if (getIntegeri) {
|
||||
MGP_FILL(GetIntegeri_v);
|
||||
getIntegeri(target, index, &backendValue);
|
||||
}
|
||||
*data = std::max(backendValue, minimum);
|
||||
@@ -1353,6 +1355,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
@@ -2263,6 +2266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
||||
@@ -3398,6 +3399,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support shader storage block binding."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(ShaderStorageBlockBinding);
|
||||
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -164,6 +165,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void ResetQueryObjectLocked(QueryObject* queryObject) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -178,6 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
|
||||
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
|
||||
if (endTimeElapsedQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndTimeElapsedQuery);
|
||||
endTimeElapsedQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -257,6 +260,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
Uint64 result = 0;
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (queryObject->backendHandle && getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
|
||||
// Not ready. The whole point of the no-wait form is that the caller's
|
||||
@@ -271,6 +275,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -286,6 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
|
||||
MGP_FILL(IsQueryResultAvailable);
|
||||
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
|
||||
return true;
|
||||
}
|
||||
@@ -297,6 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Uint64 result = 0;
|
||||
if (queryObject->backendHandle) {
|
||||
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||
MGP_FILL(GetQueryResult64);
|
||||
if (getQueryResult64 &&
|
||||
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
|
||||
// The backend could not produce the result YET (e.g. a
|
||||
@@ -317,6 +324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// query degrades to a zero result); the backend handle is
|
||||
// consumed and the value cached for later reads.
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -422,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -441,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -519,6 +529,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
MGP_FILL(BeginXfbPrimitivesQuery);
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||
@@ -527,9 +538,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
queryObject->geometryCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
||||
} else if (isOcclusionQuery) {
|
||||
MGP_FILL(BeginOcclusionQuery);
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
MGP_FILL(BeginTimeElapsedQuery);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
@@ -579,6 +592,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
MGP_FILL(EndXfbPrimitivesQuery);
|
||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||
}
|
||||
}
|
||||
@@ -588,6 +602,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->backendHandle = nullptr;
|
||||
@@ -604,6 +619,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isOcclusionQuery) {
|
||||
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
|
||||
endOcclusionQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(EndOcclusionQuery);
|
||||
endOcclusionQuery(queryObject->backendHandle);
|
||||
}
|
||||
queryObject->active = false;
|
||||
@@ -642,6 +658,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
|
||||
MGP_FILL(QueryCounterTimestamp);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||
queryObject->ended = true;
|
||||
@@ -771,6 +788,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||
MGP_FILL(IsTimerQuerySupported);
|
||||
const Bool supported =
|
||||
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
|
||||
*params = supported ? 64 : 0;
|
||||
@@ -912,6 +930,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
||||
for (const auto& [_, queryObject] : orphans) {
|
||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
||||
MGP_FILL(DeleteBackendQuery);
|
||||
deleteBackendQuery(queryObject->backendHandle);
|
||||
}
|
||||
delete queryObject;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GL_Sync.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -56,6 +57,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
MGP_FILL(FenceSync);
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
|
||||
@@ -94,6 +96,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!backendClientWaitSync || !syncObject->backendHandle) {
|
||||
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
|
||||
}
|
||||
MGP_FILL(ClientWaitSync);
|
||||
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
|
||||
@@ -119,6 +122,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
|
||||
if (backendWaitSync && syncObject->backendHandle) {
|
||||
MGP_FILL(WaitSync);
|
||||
backendWaitSync(syncObject->backendHandle, flags, timeout);
|
||||
}
|
||||
}
|
||||
@@ -139,6 +143,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
@@ -174,6 +179,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_SYNC_STATUS: {
|
||||
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
|
||||
MGP_FILL(GetSyncStatus);
|
||||
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
|
||||
backendGetSyncStatus(syncObject->backendHandle);
|
||||
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
|
||||
@@ -227,6 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
||||
for (const auto& [_, syncObject] : orphans) {
|
||||
if (backendDeleteSync && syncObject->backendHandle) {
|
||||
MGP_FILL(DeleteSync);
|
||||
backendDeleteSync(syncObject->backendHandle);
|
||||
}
|
||||
delete syncObject;
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#include <MG_Impl/GLImpl/Sampler/Validators.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.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 {
|
||||
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
|
||||
@@ -1076,6 +1080,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Vector<Uint8> scratch(static_cast<SizeT>(width) * static_cast<SizeT>(height) * bytesPerTexel);
|
||||
{
|
||||
ScopedNeutralPackState neutralPack;
|
||||
MGP_FILL(ReadPixels);
|
||||
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data());
|
||||
}
|
||||
|
||||
@@ -1335,6 +1340,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// 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,
|
||||
@@ -1413,6 +1442,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// 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,
|
||||
@@ -1619,6 +1672,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GenerateMipmap_Backend(GLenum target) {
|
||||
MGP_FILL(GenerateMipmap);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
|
||||
}
|
||||
|
||||
@@ -2102,6 +2156,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
|
||||
return;
|
||||
}
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
|
||||
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
|
||||
// publish through TextureObjectBase::PipePublishParams; the tenth is
|
||||
// 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) {
|
||||
@@ -4024,6 +4102,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexSubImage2D_Backend(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height) {
|
||||
MGP_FILL(CopyTexSubImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||
}
|
||||
|
||||
@@ -4040,6 +4119,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support image-to-image copies."));
|
||||
return;
|
||||
}
|
||||
MGP_FILL(CopyImageSubData);
|
||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
@@ -4461,6 +4541,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void CopyTexImage2D_Backend(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border) {
|
||||
MGP_FILL(CopyTexImage2D);
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D(target, level, internalformat, x, y, width, height,
|
||||
border);
|
||||
}
|
||||
@@ -5071,6 +5152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetTexImage_Backend(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
MGP_FILL(GetTexImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
|
||||
@@ -6453,6 +6535,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
return;
|
||||
@@ -6657,6 +6740,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
|
||||
.Bind(textureObject, level, layered, layer, access, format);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
MGP_FILL(BindImageTexture);
|
||||
bindImageTexture(unit, texture, level, layered, layer, access, format);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -0,0 +1,151 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.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
|
||||
// The fill point (ARCHITECTURE.md 9.2, P1 brief D7). MG_Impl spells MGP_FILL(Verb); as the
|
||||
// statement immediately before every call through gBackendFunctionsTable.GL - after every
|
||||
// early return the call is behind, inside the loop body for a call made in a loop - so the
|
||||
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
|
||||
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
|
||||
// it.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct PipeInputs;
|
||||
|
||||
// PipeFill.cpp. THE VALIDATE POINT (ARCHITECTURE.md 5.1, P2 brief D1). In order:
|
||||
// 1. bump the per-verb serial, record the verb and the context identity;
|
||||
// 2. run the tracker's DIRTY WALK for this verb's class (MG_Impl/Pipe/Tracker.h);
|
||||
// 3. EMIT, for each set dirty bit whose subsystem bit is on in the runtime
|
||||
// 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
|
||||
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
||||
// it aborts as Fatal{UnmigratedPipeInput, "<Field>@<none>"} - which is what such a read
|
||||
// is - instead of naming whichever verb happened to be filled last. Nothing in the GL
|
||||
// entry points calls this: a real verb is always followed by the next verb's fill. It
|
||||
// exists for a caller that drives a backend helper directly and wants its declaration to
|
||||
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
|
||||
void MGPipeLeaveVerb();
|
||||
|
||||
// PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, AT THIS MASK, EMIT FOR THIS P4a FAMILY?
|
||||
// (ID-39, widened by S-3 / ID-41.) The four conjuncts are the operator's per-subsystem bit
|
||||
// in MOBILEGL_PIPE_PUSH, the family's own kMGPipeWired*Subsystem constant (`wired`, which
|
||||
// the caller passes because it lives in the family's emit header and this header may not
|
||||
// include one), and - for the four families P4a migrates - a backend having registered
|
||||
// MGPipeResourceOps (the same per-backend signal `MGPipeResourceSubsystemEnabled()` has
|
||||
// applied to P3a's buffers since the phase began) and every D-K2 dependency bit of the
|
||||
// family being set in the same mask.
|
||||
//
|
||||
// THE LAST TWO CONJUNCTS ARE THE ONES THIS DECLARATION EXISTS FOR, and they are the same
|
||||
// defect twice. Magma (DirectVulkan) registers no table and has no P4a twins; at a mask like
|
||||
// 0x7ff Espryt REFUSES the texture family server-side because D-K2's fourth row says bit 10
|
||||
// requires bit 11. In both cases the client emitted anyway, the applier accepted, the
|
||||
// emitters cleared their per-level dirty flags on that acceptance, and the legacy upload
|
||||
// path that still owed those texels found nothing to upload (66 DirectVulkan cases at ID-39,
|
||||
// 47 DirectGLES cases at ID-41). With them the four families emit NOTHING in that state and
|
||||
// the legacy pull path runs exactly as it does on a pull build.
|
||||
//
|
||||
// D-K2's 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
|
||||
// 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
|
||||
// MOBILEGL_PIPE_POISON_OMIT knob ("<Verb>:<FieldName>") calls this once, on the first
|
||||
// fill; tests call it directly. Both null clears the omission. An unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
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
|
||||
// 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
|
||||
// is the branch that survives P13, which is why it is its own function rather than the
|
||||
// filler's loop.
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#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
|
||||
#define MGP_FILL(Verb) ((void)0)
|
||||
#define MGP_SET_BASE_INSTANCE(BaseInstance) ((void)0)
|
||||
#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,42 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.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 "BackendCapsPeek.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
// Declared in MG_Backend/BackendObjects.h, which also pulls in both backends' headers
|
||||
// and, through them, their loaders; the reference alone is all that is needed here.
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
} // namespace MobileGL::MG_Backend
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]) {
|
||||
#if defined(__ANDROID__)
|
||||
(void)outCount;
|
||||
(void)outSize;
|
||||
return false;
|
||||
#else
|
||||
const auto& backend = MobileGL::MG_Backend::pActiveBackendObject;
|
||||
if (!backend) {
|
||||
return false;
|
||||
}
|
||||
const MobileGL::MG_Backend::DynamicBackendParameters& caps = backend->GetDynamicParameters();
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
outCount[axis] = caps.MaxComputeWorkGroupCount[axis];
|
||||
outSize[axis] = caps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,29 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/BackendCapsPeek.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 one place this module looks past the GL API into the active backend's caps block.
|
||||
//
|
||||
// It exists for exactly one assertion: that the six per-axis compute limits the MGPipe
|
||||
// caps block carries (DynamicBackendParameters::MaxComputeWorkGroupCount/Size, plan B
|
||||
// section 4.4.1) are the same numbers glGetIntegeri_v answers today, since P0.5 retires
|
||||
// the getter in favour of the caps. A separate translation unit, because 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 {
|
||||
|
||||
// Copies the active backend's MaxComputeWorkGroupCount / MaxComputeWorkGroupSize into the
|
||||
// two arrays and returns true. Returns false, touching nothing, where the caps block is
|
||||
// out of reach: on Android this module links the SHIPPING libMobileGL.so, built
|
||||
// -fvisibility=hidden, so no internal symbol resolves; on desktop it links MobileGL_s and
|
||||
// the read is direct.
|
||||
bool PeekComputeWorkGroupCaps(int outCount[3], int outSize[3]);
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -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
|
||||
@@ -26,9 +26,11 @@
|
||||
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
|
||||
// fails on both is a table bug.
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/BackendCapsPeek.h"
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
@@ -378,5 +380,250 @@ namespace MGITest {
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
|
||||
// THE INDEXED AND PER-PROGRAM QUERIES THAT NAME FRONTEND STATE, pinned on both lanes.
|
||||
//
|
||||
// Both backends used to carry their own arms for GL_SHADER_STORAGE_BUFFER_* and
|
||||
// GL_IMAGE_BINDING_* inside GLFunctionsTable::GetIntegeri_v, and their own
|
||||
// GetInteger64i_v / GetProgramiv table entries. None of it was reachable: GL_Getter and
|
||||
// GL_Program answer every one of these pnames from the frontend's own state and return
|
||||
// before the table is consulted. The duplicates did not even agree - the backend arms
|
||||
// clamped a bound range to the buffer's current storage, which GL 4.6 core tables
|
||||
// 23.4/23.5 do not permit - so the code was one refactor away from becoming the answer.
|
||||
// These cases pin what the frontend actually reports, so a future move of any of it back
|
||||
// behind the interface has to keep saying the same thing.
|
||||
TEST_F(AdvertisedLimitsScenario, IndexedBufferBindingsAreReportedVerbatimOnBothWidths) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
// A range that is NOT the whole buffer, so a clamp to the store would be visible.
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, buffer, 256, 512);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
GLint binding32 = -1;
|
||||
GLint start32 = -1;
|
||||
GLint size32 = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding32);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start32);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size32);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(binding32, static_cast<GLint>(buffer));
|
||||
EXPECT_EQ(start32, 256);
|
||||
EXPECT_EQ(size32, 512);
|
||||
|
||||
// The 64-bit width has to agree pname for pname. It has no backend entry of its own
|
||||
// and derives everything from the 32-bit answer above plus its own buffer arm.
|
||||
GLint64 binding64 = -1;
|
||||
GLint64 start64 = -1;
|
||||
GLint64 size64 = -1;
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_BINDING, 1, &binding64);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_START, 1, &start64);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &size64);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(binding64, static_cast<GLint64>(buffer));
|
||||
EXPECT_EQ(start64, static_cast<GLint64>(256));
|
||||
EXPECT_EQ(size64, static_cast<GLint64>(512));
|
||||
|
||||
// An unbound index answers zero rather than erroring or leaking the driver's answer.
|
||||
GLint unbound = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, 0, &unbound);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(unbound, 0);
|
||||
|
||||
// THE ARM THAT SEPARATES VERBATIM FROM CLAMPED. GL 4.6 core tables 23.4/23.5 report
|
||||
// the size glBindBufferRange was ASKED for; it does not follow the buffer, so
|
||||
// shrinking the store underneath the binding must not move it. A clamp to the
|
||||
// current storage - which is exactly what both backends' deleted arms did - answers
|
||||
// 128 here, and answers 0 for the bind-then-allocate shape
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-binding uses.
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint startAfterShrink = -1;
|
||||
GLint sizeAfterShrink = -1;
|
||||
GLint64 sizeAfterShrink64 = -1;
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_START, 1, &startAfterShrink);
|
||||
glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink);
|
||||
glGetInteger64i_v(GL_SHADER_STORAGE_BUFFER_SIZE, 1, &sizeAfterShrink64);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(startAfterShrink, 256)
|
||||
<< "the bound range's start followed the buffer through a re-specification";
|
||||
EXPECT_EQ(sizeAfterShrink, 512)
|
||||
<< "the bound range's size was clamped to the buffer's current 128-byte storage; the range is "
|
||||
"state of the BINDING POINT and is reported verbatim";
|
||||
EXPECT_EQ(sizeAfterShrink64, static_cast<GLint64>(512))
|
||||
<< "the 64-bit width disagreed with the 32-bit one about the same pname";
|
||||
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
TEST_F(AdvertisedLimitsScenario, ImageUnitBindingsAreReportedFromTheFrontendState) {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
(void)FirstGLError();
|
||||
if (maxImageUnits < 2) GTEST_SKIP() << "no image units to bind on this lane";
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
glBindImageTexture(1, texture, 1, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
|
||||
struct Expectation {
|
||||
GLenum pname;
|
||||
const char* name;
|
||||
GLint expected;
|
||||
};
|
||||
const Expectation expectations[] = {
|
||||
{GL_IMAGE_BINDING_NAME, "GL_IMAGE_BINDING_NAME", static_cast<GLint>(texture)},
|
||||
{GL_IMAGE_BINDING_LEVEL, "GL_IMAGE_BINDING_LEVEL", 1},
|
||||
{GL_IMAGE_BINDING_LAYERED, "GL_IMAGE_BINDING_LAYERED", GL_FALSE},
|
||||
{GL_IMAGE_BINDING_LAYER, "GL_IMAGE_BINDING_LAYER", 0},
|
||||
{GL_IMAGE_BINDING_ACCESS, "GL_IMAGE_BINDING_ACCESS", GL_READ_ONLY},
|
||||
{GL_IMAGE_BINDING_FORMAT, "GL_IMAGE_BINDING_FORMAT", GL_RGBA8},
|
||||
};
|
||||
for (const Expectation& expectation : expectations) {
|
||||
GLint value = -424242;
|
||||
glGetIntegeri_v(expectation.pname, 1, &value);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name;
|
||||
EXPECT_EQ(value, expectation.expected) << expectation.name;
|
||||
|
||||
// Same pname through the wide width - it must not fall through to a driver that
|
||||
// knows nothing about MobileGL's image-unit state.
|
||||
GLint64 wide = -424242;
|
||||
glGetInteger64i_v(expectation.pname, 1, &wide);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << expectation.name << " (64-bit)";
|
||||
EXPECT_EQ(wide, static_cast<GLint64>(expectation.expected)) << expectation.name << " (64-bit)";
|
||||
}
|
||||
|
||||
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
glDeleteTextures(1, &texture);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
// glGetProgramiv(GL_COMPUTE_WORK_GROUP_SIZE) is a LINK ARTIFACT of the program the
|
||||
// application wrote. DirectVulkan used to answer it from its own spirv-reflect cache and
|
||||
// DirectGLES by forwarding to the driver's ESSL program - neither of which the
|
||||
// application ever named - while GL_Program.cpp has always answered it from
|
||||
// ProgramObject::GetComputeLocalSize. This pins the declared local size on both lanes.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeLocalSizeComesFromTheLinkedProgram) {
|
||||
static const char* kSource = R"(#version 430 core
|
||||
layout(local_size_x = 4, local_size_y = 3, local_size_z = 2) in;
|
||||
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
||||
void main() { g_data[gl_LocalInvocationIndex] = 1u; }
|
||||
)";
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
glShaderSource(shader, 1, &kSource, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
glDeleteShader(shader);
|
||||
(void)FirstGLError();
|
||||
GTEST_SKIP() << "no compute shader support on this lane: " << log;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
glDeleteProgram(program);
|
||||
(void)FirstGLError();
|
||||
GTEST_SKIP() << "the compute program did not link on this lane: " << log;
|
||||
}
|
||||
|
||||
GLint localSize[3] = {-1, -1, -1};
|
||||
glGetProgramiv(program, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_EQ(localSize[0], 4);
|
||||
EXPECT_EQ(localSize[1], 3);
|
||||
EXPECT_EQ(localSize[2], 2);
|
||||
|
||||
// A program with no compute stage must answer INVALID_OPERATION, not a stale or
|
||||
// defaulted (1, 1, 1) - the frontend's rule, and the one a backend that answers from
|
||||
// its own reflection cache cannot express.
|
||||
const GLuint empty = glCreateProgram();
|
||||
GLint ignored[3] = {0, 0, 0};
|
||||
glGetProgramiv(empty, GL_COMPUTE_WORK_GROUP_SIZE, ignored);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION))
|
||||
<< "GL 4.6 core 7.13: the query is only defined for a linked program with a compute shader";
|
||||
|
||||
glDeleteProgram(empty);
|
||||
glDeleteProgram(program);
|
||||
(void)FirstGLError();
|
||||
}
|
||||
|
||||
// THE SIX COMPUTE LIMITS THAT OUTLIVE THE GETTER. GL_MAX_COMPUTE_WORK_GROUP_COUNT and
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, are the only indexed pnames the
|
||||
// DEVICE answers rather than the frontend (glGetIntegeri_v on Espryt, VkPhysicalDevice-
|
||||
// Limits on Magma), and therefore the only ones that have to cross the MGPipe boundary
|
||||
// once GetIntegeri_v is retired (plan B section 4.4.6 / P0.5). They ride in MGPCaps by
|
||||
// inclusion, as DynamicBackendParameters::MaxComputeWorkGroupCount/Size, filled by both
|
||||
// backends at capability init. This case pins that the caps copy and the live getter
|
||||
// answer are one number - the getter floors the backend's raw answer at the GL 4.3
|
||||
// minimum, so the comparison is against the floored caps value - and pins the
|
||||
// GL-visible half on every lane: answerability, the floors, vector/indexed agreement
|
||||
// and the index bound. On a lane where the caps block is out of reach (Android links
|
||||
// the shipping .so) only the GL-visible half runs.
|
||||
TEST_F(AdvertisedLimitsScenario, ComputeWorkGroupLimitsAreTheCapsBlocksAnswer) {
|
||||
struct Axis {
|
||||
GLenum pname;
|
||||
const char* name;
|
||||
GLint minimum[3]; // GL 4.3 core table 23.60
|
||||
};
|
||||
const Axis axes[] = {
|
||||
{GL_MAX_COMPUTE_WORK_GROUP_COUNT, "GL_MAX_COMPUTE_WORK_GROUP_COUNT", {65535, 65535, 65535}},
|
||||
{GL_MAX_COMPUTE_WORK_GROUP_SIZE, "GL_MAX_COMPUTE_WORK_GROUP_SIZE", {1024, 1024, 64}},
|
||||
};
|
||||
int capsCount[3] = {0, 0, 0};
|
||||
int capsSize[3] = {0, 0, 0};
|
||||
const bool capsVisible = PeekComputeWorkGroupCaps(capsCount, capsSize);
|
||||
|
||||
for (const Axis& axis : axes) {
|
||||
GLint indexed[3] = {-1, -1, -1};
|
||||
for (GLuint i = 0; i < 3; ++i) {
|
||||
glGetIntegeri_v(axis.pname, i, &indexed[i]);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name << "[" << i << "]";
|
||||
EXPECT_GE(indexed[i], axis.minimum[i])
|
||||
<< axis.name << "[" << i << "] = " << indexed[i]
|
||||
<< " is below the GL 4.3 core table 23.60 minimum " << axis.minimum[i];
|
||||
}
|
||||
GLint vector[3] = {-1, -1, -1};
|
||||
glGetIntegerv(axis.pname, vector);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << axis.name;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(vector[i], indexed[i])
|
||||
<< axis.name << "[" << i << "]: the vector query and the indexed query disagree";
|
||||
}
|
||||
GLint outOfRange = -424242;
|
||||
glGetIntegeri_v(axis.pname, 3, &outOfRange);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_VALUE))
|
||||
<< axis.name << "[3]: an index past the three axes is INVALID_VALUE (GL 4.6 core 22.1)";
|
||||
|
||||
if (!capsVisible) continue;
|
||||
const int* capsAxis = axis.pname == GL_MAX_COMPUTE_WORK_GROUP_COUNT ? capsCount : capsSize;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(std::max(capsAxis[i], axis.minimum[i]), indexed[i])
|
||||
<< axis.name << "[" << i << "]: MGPCaps carries " << capsAxis[i]
|
||||
<< " but glGetIntegeri_v answers " << indexed[i]
|
||||
<< " - the caps block and the getter path must be one number, because P0.5 retires "
|
||||
"the getter in favour of the caps";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -226,25 +226,21 @@ void main() {
|
||||
}
|
||||
|
||||
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
|
||||
// between. The DirectGLES backend queues app SubData ranges for the draw-time staged-copy
|
||||
// flush (the upload ring) instead of uploading in place, and readback of a GPU-written
|
||||
// buffer overwrites the frontend shadow with the driver copy - so if the readback path
|
||||
// forgets to flush the queued range first, the newer CPU write is REVERTED by the readback
|
||||
// and offset 0 reads the dispatch's value instead of the reseed. Offset 4 pins the other
|
||||
// direction: the flush must not clobber GPU results outside the written range.
|
||||
// between. Each backend has its own way to invert this pair, and both are pinned here.
|
||||
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
|
||||
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
|
||||
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
|
||||
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
|
||||
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
|
||||
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
|
||||
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
|
||||
// the deferred frame command buffer. Unless the frontend retires that pending work before
|
||||
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
|
||||
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
|
||||
// pins the other direction for both: the upload must leave bytes outside its range - the
|
||||
// dispatch's results - untouched.
|
||||
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
// DirectGLES-only for now. DirectVulkan fails this case with or without the upload
|
||||
// ring, on revisions that predate it: its buffer uploads submit immediately while the
|
||||
// dispatch sits in the deferred frame command buffer, so the GPU increments the
|
||||
// RESEEDED value (reads 4242 + increments instead of 4242) - a pre-existing
|
||||
// upload-vs-recorded-work ordering gap in that backend, kept visible here rather than
|
||||
// silently absorbed. Un-skip once DirectVulkan orders app uploads against already
|
||||
// recorded GPU work.
|
||||
if (Gl().BackendName() != std::string("DirectGLES")) {
|
||||
GTEST_SKIP() << "SubData-after-dispatch ordering is a known DirectVulkan gap; this case pins the "
|
||||
"DirectGLES readback pre-flush only";
|
||||
}
|
||||
|
||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||
MakeCounterBuffer(1, {0u});
|
||||
|
||||
@@ -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
@@ -0,0 +1,508 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.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 - MESH-ARENA-SIZED BUFFERS, END TO END.
|
||||
//
|
||||
// A buffer store of at least 16MiB is adopted into the backend's persistently and
|
||||
// coherently mapped GPU storage the moment it is defined (BufferObject::
|
||||
// TryAdoptLargeStorage): the CPU shadow is dropped and every later write lands
|
||||
// directly in GPU-visible memory with no per-write driver call. Minecraft 26.3
|
||||
// streams chunk meshes into 128MB vertex arenas with plain glNamedBufferSubData -
|
||||
// on Mali, every driver-mediated route for that write into a busy mutable store
|
||||
// either parks the calling thread or ghost-copies the whole arena on a driver
|
||||
// worker (~167ms per touched arena: the recurring in-world hiccup this adoption
|
||||
// removed). Every existing buffer scenario uses stores far below the threshold,
|
||||
// so without this file the adopted path would have zero coverage.
|
||||
//
|
||||
// What is pinned, deliberately through the same API mix Minecraft uses:
|
||||
// * a glBufferSubData written AFTER the arena was drawn (in flight) reaches the
|
||||
// next draw - the write-visibility contract adoption must not weaken;
|
||||
// * 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
|
||||
// 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 <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 {
|
||||
|
||||
// Comfortably past the 16MiB adoption threshold, and the vertex payload sits
|
||||
// deep inside the store so an implementation that quietly clamped or aliased
|
||||
// the adopted range would miss it.
|
||||
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(24) * 1024 * 1024;
|
||||
constexpr GLintptr kVertexOffset = GLintptr(20) * 1024 * 1024;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 430 core
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec3 a_color;
|
||||
out vec3 v_color;
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 430 core
|
||||
in vec3 v_color;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(v_color, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kMarkerComputeSource = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Arena { uint word; };
|
||||
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 {
|
||||
float x, y;
|
||||
float r, g, b;
|
||||
};
|
||||
|
||||
// A full-viewport quad, colored uniformly so one center readback speaks for
|
||||
// the whole draw.
|
||||
std::vector<Vertex> QuadVertices(float r, float g, float b) {
|
||||
return {
|
||||
{-1.f, -1.f, r, g, b}, {1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b},
|
||||
{-1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b}, {-1.f, 1.f, r, g, b},
|
||||
};
|
||||
}
|
||||
|
||||
class LargeArenaAdoptionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_program = LinkProgram(kVertexSource, kFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_arena);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
// The NULL-data definition is the adoption point (and Minecraft's
|
||||
// arena-creation idiom).
|
||||
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),
|
||||
reinterpret_cast<void*>(kVertexOffset));
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_compute != 0) glDeleteProgram(m_compute);
|
||||
m_vao = 0;
|
||||
m_arena = 0;
|
||||
m_program = 0;
|
||||
m_compute = 0;
|
||||
}
|
||||
|
||||
unsigned int CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("shader did not compile: ") + log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
unsigned int LinkProgram(const char* vs, const char* fs) {
|
||||
const GLuint v = CompileStage(GL_VERTEX_SHADER, vs);
|
||||
if (v == 0) return 0;
|
||||
const GLuint f = CompileStage(GL_FRAGMENT_SHADER, fs);
|
||||
if (f == 0) {
|
||||
glDeleteShader(v);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, v);
|
||||
glAttachShader(program, f);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(v);
|
||||
glDeleteShader(f);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[2048] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
m_buildLog = std::string("program did not link: ") + log;
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
void UploadQuad(float r, float g, float b) {
|
||||
const auto vertices = QuadVertices(r, g, b);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
}
|
||||
|
||||
void DrawQuad(GLuint vao = 0) {
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(vao != 0 ? vao : m_vao);
|
||||
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> px = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
px.data());
|
||||
return px;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_compute = 0;
|
||||
unsigned int m_vao = 0;
|
||||
unsigned int m_arena = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The Minecraft shape: the arena is drawn, the frame retires, and a
|
||||
// glBufferSubData rewrites the SAME vertex bytes while the previous frame's
|
||||
// draw may still be in flight. The next draw must show the NEW bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, SubDataAfterAnInFlightDrawReachesTheNextDraw) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
UploadQuad(1.f, 0.f, 0.f);
|
||||
DrawQuad();
|
||||
auto px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[0], 200) << "the first draw from the adopted arena never landed";
|
||||
EXPECT_LT(px[1], 50);
|
||||
|
||||
Gl().EndFrame();
|
||||
|
||||
UploadQuad(0.f, 1.f, 0.f);
|
||||
DrawQuad();
|
||||
px = CenterPixel();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_GT(px[1], 200) << "the cross-frame rewrite of the adopted arena did not reach the draw; "
|
||||
"the old color means the write went to bytes the draw no longer reads";
|
||||
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
|
||||
// back exactly those bytes.
|
||||
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const auto vertices = QuadVertices(0.25f, 0.5f, 0.75f);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||
std::vector<Vertex> read(vertices.size());
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
|
||||
GLsizeiptr(read.size() * sizeof(Vertex)), read.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(0, std::memcmp(read.data(), vertices.data(), read.size() * sizeof(Vertex)))
|
||||
<< "GetBufferSubData of the adopted arena returned different bytes than the SubData wrote";
|
||||
}
|
||||
|
||||
// A GPU write through an SSBO binding of the adopted arena must be visible to
|
||||
// a CPU readback - the path that waits out the GPU and reads the coherent
|
||||
// mapping directly.
|
||||
TEST_F(LargeArenaAdoptionScenario, GpuWriteIntoTheArenaIsReadBack) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
GLint maxComputeStorageBlocks = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
|
||||
if (maxComputeStorageBlocks < 1) {
|
||||
GTEST_SKIP() << "no compute shader storage blocks on this driver";
|
||||
}
|
||||
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
|
||||
ASSERT_NE(compute, 0u) << m_buildLog;
|
||||
m_compute = glCreateProgram();
|
||||
glAttachShader(m_compute, compute);
|
||||
glLinkProgram(m_compute);
|
||||
glDeleteShader(compute);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(m_compute, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
|
||||
const unsigned int seed = 0u;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(seed), &seed);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
|
||||
glUseProgram(m_compute);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
unsigned int marker = 0;
|
||||
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(marker, 0xC0FFEEu)
|
||||
<< "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
|
||||
@@ -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,269 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.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 MOBILEGL_PIPE_VERIFY COMPARATOR IS ARMED, AND SAYS SO, AND CAN GO RED.
|
||||
//
|
||||
// The third CI mode (ARCHITECTURE.md 13.2-(2)) runs the whole integration suite with two state
|
||||
// models in one address space: the PipeInputs block the frontend fills at every verb boundary,
|
||||
// and a SnapshotFromGLContext() taken from the live GLContext. A green run of that mode is only
|
||||
// worth something if the comparator was actually RUNNING - and "MOBILEGL_PIPE_VERIFY=1 against a
|
||||
// library that was not built with -DMOBILEGL_PIPE_VERIFY=ON" is a no-op that looks exactly like a
|
||||
// clean pass. That is the failure mode this scenario exists to make impossible:
|
||||
//
|
||||
// Armed - the environment says the comparator is on for this process, so the
|
||||
// library must SAY it armed. It asserts a library observable against
|
||||
// the environment, the same shape UnlocatedIoBlockScenario's arming
|
||||
// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration
|
||||
// use. A lane whose library never armed FAILS here; it never passes.
|
||||
// CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is
|
||||
// perturbed before the entry compare, so a comparator that works must
|
||||
// report Fatal{PipeVerifyDiffer, "<Field>@<Verb>"}. A comparator that
|
||||
// compares nothing stays quiet and this case goes red.
|
||||
//
|
||||
// The observable is the library's own log, because MG_Config is not reachable from this module
|
||||
// (on Android it links the SHIPPING libMobileGL.so, built -fvisibility=hidden) and the arming
|
||||
// signal is a latched MGLOG_I. The ctest entry sets MOBILEGL_LOG_FILE_PATH; this only reads it.
|
||||
//
|
||||
// Note on scope, and why Armed runs in a lane of its own. The log file is opened with
|
||||
// fopen(path, "w") at the first log write of a process (MG_Util/Debug/Log.cpp, InitFile), so each
|
||||
// process TRUNCATES it. That is fine for one process and false for many: in the ambient Verify.
|
||||
// lane, 400-odd sibling entries share the one MOBILEGL_LOG_FILE_PATH, and CI runs that lane with
|
||||
// `ctest -j 4`, so a neighbour's bring-up can truncate the file between this case's draw and its
|
||||
// read. Every existing scenario in this suite that reads the library log (UnlocatedIoBlockScenario,
|
||||
// the primgen reroute, the point-size demotion) is registered in a FILTERED lane with a log path of
|
||||
// its own for exactly that reason, and this case now follows them: it runs in the VerifyArming.
|
||||
// entries, which set MGITEST_PIPE_ARMING_LANE=1 and their own log, and skips everywhere else.
|
||||
//
|
||||
// What that proves, stated honestly: the arming line is a property of (this library, this
|
||||
// environment), not of an individual test body, and the VerifyArming. entry runs the same library
|
||||
// with the same MOBILEGL_PIPE_VERIFY=1 as its ~400 ambient siblings. One process per backend is
|
||||
// therefore the whole of the evidence available for "the lane armed" - the per-process claim the
|
||||
// shared log CANNOT support, because it only ever holds the last writer.
|
||||
//
|
||||
// Within the process: the arming line is latched at the FIRST fill, which may be the harness
|
||||
// bring-up rather than this test's draw, so the arming search is whole-file on purpose; the
|
||||
// divergence search is restricted to the bytes this case appended, which is where a differ belongs.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#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 {
|
||||
|
||||
// The three strings the comparator contracts to print (the brief's D8 reporting shape).
|
||||
// They are spelled here once so a rename of either half is one compile-visible edit.
|
||||
constexpr const char* kArmedLine = "MGPipe: verify armed";
|
||||
constexpr const char* kDifferPrefix = "Fatal{PipeVerifyDiffer";
|
||||
constexpr const char* kUnmigratedPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
// Set by the VerifyArming. ctest entries and by nothing else. It is a HARNESS variable, not
|
||||
// a library knob (hence the MGITEST_ prefix): the library never reads it. It exists because
|
||||
// this case reads a log file, and a log file is a per-LANE resource - see the note at the
|
||||
// top of the file.
|
||||
constexpr const char* kArmingLaneMarker = "MGITEST_PIPE_ARMING_LANE";
|
||||
|
||||
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 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
// Reads the environment the way MG_ConfigLoader does (ScenarioFixture.h documents the
|
||||
// rule); a string knob is "set" when it is present and non-empty, which is exactly what
|
||||
// MG_ConfigLoader's QueryEnvVariable turns into a non-empty Features member.
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
class PipeVerifyArmingScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The library log this process is writing, or an empty path when none was configured.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static std::string LibraryLog() { return LibraryLogSince(0); }
|
||||
|
||||
// One frame that crosses several verb boundaries: a clear (kClear), a draw (kDraw) and
|
||||
// a readback (kReadback). Three of the nine fill classes, so an entry compare that only
|
||||
// ran for one of them still has something to say.
|
||||
void DrawOneFrame() {
|
||||
HeadlessGL& gl = Gl();
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
m_centre = pixel;
|
||||
}
|
||||
|
||||
Rgba8 m_centre{};
|
||||
};
|
||||
|
||||
// THE CASE THAT FAILS A LANE WHOSE LIBRARY NEVER ARMED.
|
||||
//
|
||||
// Every other entry in the integration-verify lane renders the same frames it renders in the
|
||||
// ambient lane and would be just as green against a library with no comparator compiled in -
|
||||
// which is precisely how a verify lane goes green having verified nothing. This case is the
|
||||
// one that cannot: the environment pins MOBILEGL_PIPE_VERIFY=1, therefore the library must
|
||||
// have said "MGPipe: verify armed" in its own log, and if it did not, the mode is not running.
|
||||
TEST_F(PipeVerifyArmingScenario, Armed) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet(kArmingLaneMarker)) {
|
||||
GTEST_SKIP() << "this case reads the library's log file, so it runs in the VerifyArming. "
|
||||
"lane, which owns a log path no other entry writes to. In the ambient "
|
||||
"Verify. lane 400-odd entries share one path and each truncates it "
|
||||
"(Log.cpp opens it \"w\"), so a whole-file read here would race a "
|
||||
"neighbour under ctest -j 4. Set by the ctest entry, never by hand.";
|
||||
}
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs MOBILEGL_PIPE_VERIFY=1 for the whole process, which is "
|
||||
"what the Verify. ctest entries set; with the variable unset the "
|
||||
"comparator is dormant even in a build that compiled it in";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY is pinned on but MOBILEGL_LOG_FILE_PATH is not "
|
||||
"set, so the library has nowhere to record that it armed; the Verify. "
|
||||
"ctest entries set both";
|
||||
}
|
||||
if (StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is armed in this process, so a divergence "
|
||||
"is the EXPECTED outcome and asserting on its absence here would be "
|
||||
"backwards; the VerifyCorrupted. lane owns that half";
|
||||
}
|
||||
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Whole file, not just the appended bytes: the arming line is latched at the FIRST fill
|
||||
// of the process, which may already have happened during the harness bring-up. The file
|
||||
// is truncated at this process's first log write, so it still carries nothing else.
|
||||
const std::string whole = LibraryLog();
|
||||
EXPECT_NE(whole.find(kArmedLine), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and "
|
||||
"read back, and the library never reported arming the comparator. Either this "
|
||||
"library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane "
|
||||
"is verifying nothing), or the arming MGLOG_I is gone. Log:\n"
|
||||
<< whole;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos)
|
||||
<< "the comparator reported a push/pull divergence on an ordinary frame:\n"
|
||||
<< appended;
|
||||
EXPECT_EQ(appended.find(kUnmigratedPrefix), std::string::npos)
|
||||
<< "a backend read a field the verb's fill table does not list (add the row to "
|
||||
"MG_Pipe/FillPoints.def, never mark the field sticky):\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
// NEGATIVE CONTROL A (gate G4): a deliberately corrupted snapshot field must turn a green
|
||||
// verify run red, naming that field and the verb it diverged on.
|
||||
//
|
||||
// It runs in its own lane (VerifyCorrupted.) because the knob is process-wide, and with
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and this case can
|
||||
// read the report back out of the log. The CI step that runs the SAME knob against the
|
||||
// ambient lane - where FATAL keeps its default - asserts the other half: there, the
|
||||
// divergence must abort and ctest must go red.
|
||||
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReported) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "this case is the comparator's negative control and needs "
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=<FieldName> for the whole process, which "
|
||||
"is what the VerifyCorrupted. ctest entries set";
|
||||
}
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is set but MOBILEGL_PIPE_VERIFY is not, so "
|
||||
"the comparator is dormant and there is nothing to corrupt";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to report "
|
||||
"the divergence; the VerifyCorrupted. ctest entries set both";
|
||||
}
|
||||
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_VERIFY_CORRUPT");
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
const std::string expected = std::string(kDifferPrefix) + ", \"" + knob + "@";
|
||||
EXPECT_NE(appended.find(expected), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY_CORRUPT=" << knob
|
||||
<< " perturbs that field in the snapshot arm before every entry compare, so a working "
|
||||
"comparator must have reported " << expected << "...\". It reported nothing, which "
|
||||
"means the comparator is not comparing - and every green entry in this lane is "
|
||||
"green for no reason. Log appended by this case:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,522 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PointSizeDemotionScenario.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 gl_PointSize DEMOTION IS CLIENT-INVISIBLE, AND IT ACTUALLY ARMS.
|
||||
//
|
||||
// On a device that hosts the built-in in tessellation/geometry stages (llvmpipe and
|
||||
// lavapipe both do), gl_PointSize travels as itself; on one that does not (the Mali
|
||||
// devices this exists for), phase B demotes it to an ordinary varying
|
||||
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram) and the capture
|
||||
// machinery follows it there. This scenario runs in BOTH configurations and must hand
|
||||
// back identical bytes: the ambient registrations take the native path, and the
|
||||
// PointSizeDemotion. registrations pin MOBILEGL_POINT_SIZE_DEMOTION=1 so the demotion
|
||||
// runs on the same healthy drivers - CopyImagePacked16Scenario's dual-configuration
|
||||
// contract, applied to a value chain instead of a storage format.
|
||||
//
|
||||
// The VALUE is the whole contract: every case writes gl_PointSize in one stage, reads it
|
||||
// back out of gl_in[] in the next, and captures it by name under rasterizer discard, so
|
||||
// one wrong link anywhere in VS -> TCS -> TES -> GS -> capture lands in the readback.
|
||||
// The RASTERIZED size is deliberately not asserted anywhere: with the built-in unhosted
|
||||
// it falls back to 1.0 by spec on both targets, which is exactly the honest residue the
|
||||
// demotion documents (point_rendering-style bodies keep failing truthfully).
|
||||
//
|
||||
// The assertions are on the captured BYTES against a CPU-computed reference, never on
|
||||
// the absence of a GL error: every failure this guards against is silent.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#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 {
|
||||
|
||||
constexpr float kPoison = -987654.0f;
|
||||
|
||||
const char* const kFragmentSource = R"(#version 460 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The full chain, with per-vertex VARIATION seeded in the vertex stage so a control
|
||||
// invocation that read or wrote the wrong slot changes the sum: 2,3,4 arrive, 3,4,5
|
||||
// leave, the evaluation stage sums its patch to 12, the geometry stage doubles what
|
||||
// it read to 24.
|
||||
const char* const kChainVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 2.0 + float(gl_VertexID);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessControlSource = R"(#version 460 core
|
||||
layout(vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainTessEvalSource = R"(#version 460 core
|
||||
layout(triangles, equal_spacing, cw, point_mode) in;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kChainGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// The geometry-only chain: no tessellation required of the stack at all.
|
||||
const char* const kPointVertexSource = R"(#version 460 core
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
gl_PointSize = 7.0;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPointGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// A capture stage that only READS the incoming point size and never writes its own.
|
||||
// Legal GL, and the shape that separates "the demotion arms" from "the demotion knows
|
||||
// a capture is coming": with the built-in gone, only the capture request can put a
|
||||
// carrier back for a by-name capture to bind to.
|
||||
const char* const kReadOnlyGeometrySource = R"(#version 460 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out float g_echo;
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
g_echo = gl_in[0].gl_PointSize;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kEchoFragmentSource = R"(#version 460 core
|
||||
in float g_echo;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(g_echo, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PointSizeDemotionScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
|
||||
const std::vector<const char*>& varyings) {
|
||||
m_buildLog.clear();
|
||||
std::vector<GLuint> shaders;
|
||||
bool ok = true;
|
||||
for (const auto& [stage, source] : stages) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
GLuint program = 0;
|
||||
if (ok) {
|
||||
program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) {
|
||||
glAttachShader(program, shader);
|
||||
}
|
||||
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()),
|
||||
varyings.data(), GL_INTERLEAVED_ATTRIBS);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
m_buildLog = InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
if (program != 0) m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// One capture span over `vertexCount` vertices of `drawMode`, recorded as
|
||||
// GL_POINTS. The buffer is poison-filled first so bytes the capture never wrote
|
||||
// name themselves.
|
||||
std::vector<float> RunCaptureSpan(GLuint program, GLenum drawMode, GLsizei vertexCount,
|
||||
std::size_t capturedFloats) {
|
||||
const std::vector<float> poison(capturedFloats, kPoison);
|
||||
GLuint xfbBuffer = 0;
|
||||
glGenBuffers(1, &xfbBuffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
poison.data(), GL_STATIC_COPY);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||
|
||||
glBindVertexArray(m_vao);
|
||||
glUseProgram(program);
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
glBeginTransformFeedback(GL_POINTS);
|
||||
glDrawArrays(drawMode, 0, vertexCount);
|
||||
glEndTransformFeedback();
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
|
||||
std::vector<float> readback(capturedFloats, kPoison);
|
||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
|
||||
readback.data());
|
||||
glUseProgram(0);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
glDeleteBuffers(1, &xfbBuffer);
|
||||
return readback;
|
||||
}
|
||||
|
||||
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data,
|
||||
std::size_t index, float expected,
|
||||
float epsilon = 1e-4f) {
|
||||
if (index >= data.size()) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is past the capture buffer";
|
||||
}
|
||||
const float actual = data[index];
|
||||
if (actual == kPoison) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " still holds the poison value - the capture "
|
||||
<< "never reached these bytes (expected " << expected << ")";
|
||||
}
|
||||
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
||||
return ::testing::AssertionFailure()
|
||||
<< "component " << index << " is " << actual << ", expected " << expected;
|
||||
}
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// The library log, for the arming case. Same machinery and same reasoning as
|
||||
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, the file
|
||||
// is appended to by every process in the lane, and only bytes appended after the
|
||||
// snapshot may satisfy an assertion.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
|
||||
private:
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
};
|
||||
|
||||
// The five-stage chain. 24.0 can only arrive if the vertex mirror, both control-stage
|
||||
// redirects (read AND write), the evaluation stage's three gl_in reads and the
|
||||
// geometry stage's read all carried the right value - one wrong link and the sum
|
||||
// moves. point_mode with every level at 1 emits three points; the first record proves
|
||||
// the mechanism, exactly as TessellationXfbCaptureScenario reasons.
|
||||
TEST_F(PointSizeDemotionScenario, TheValueSurvivesTheFiveStageChainIntoTheCapture) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_GEOMETRY_SHADER, kChainGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 24.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The same chain without a geometry stage: the capture then binds to the evaluation
|
||||
// stage's value (the sum, 12.0) - which is also the boundary where a demoted program
|
||||
// switches its capture carrier from the Io chain to the capture name.
|
||||
TEST_F(PointSizeDemotionScenario, TheEvaluationStageOwnsTheCaptureWithoutAGeometryStage) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
||||
<< Gl().RendererString() << ")";
|
||||
}
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 12.0f));
|
||||
|
||||
// The GL query surface keeps the truthful spelling whatever the backends renamed
|
||||
// underneath: reflection is a phase-A product and the demotion happens after it.
|
||||
char varyingName[64] = {};
|
||||
GLsizei nameLength = 0;
|
||||
GLsizei varyingSize = 0;
|
||||
GLenum varyingType = 0;
|
||||
glGetTransformFeedbackVarying(program, 0, sizeof(varyingName), &nameLength, &varyingSize,
|
||||
&varyingType, varyingName);
|
||||
EXPECT_STREQ(varyingName, "gl_PointSize");
|
||||
EXPECT_EQ(varyingType, static_cast<GLenum>(GL_FLOAT));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The geometry-only chain: gl_in[0].gl_PointSize read straight off the vertex stage,
|
||||
// no tessellation involved - the VS -> GS boundary of the demotion on its own.
|
||||
TEST_F(PointSizeDemotionScenario, AGeometryOnlyChainCarriesTheVertexValue) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f));
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE CAPTURE-REQUEST PATH, END TO END - the half no unit test can reach, because the
|
||||
// request travels from glTransformFeedbackVaryings through phase A's resolved capture
|
||||
// set and the phase-B handoff before it reaches the demotion.
|
||||
//
|
||||
// The geometry stage READS gl_in[0].gl_PointSize and never writes gl_PointSize, which
|
||||
// is enough to arm the demotion (glslang declares GeometryPointSize on a read) but not
|
||||
// enough to create an output carrier on its own. Only the capture request can, and if
|
||||
// that request never arrives the program does not merely lose the point-size column:
|
||||
// DirectGLES respells the driver-side capture to a name no stage declares and the
|
||||
// WHOLE capture set fails to link, while DirectVulkan mirrors a built-in the demotion
|
||||
// just removed and can unwind far enough to drop the Xfb execution mode. Either way
|
||||
// g_echo - an ordinary varying with nothing to do with point size - comes back poison,
|
||||
// which is what this asserts. gl_PointSize itself is captured but never asserted: no
|
||||
// stage writes it, so GL leaves its value undefined.
|
||||
TEST_F(PointSizeDemotionScenario, ACaptureSurvivesAStageThatOnlyReadsThePointSize) {
|
||||
if (!Ready()) return;
|
||||
// The NATIVE Espryt path cannot do this at all, and never could: with the built-in
|
||||
// hosted, the geometry stage's ESSL simply does not declare gl_PointSize unless it
|
||||
// writes it, so the driver rejects the capture request with "varying undeclared"
|
||||
// and the program becomes unusable. That is a pre-existing ES limitation the
|
||||
// demotion happens to REPAIR - the carrier is a real, seeded, declared varying -
|
||||
// so this case has something to assert only where the demotion is armed. Magma
|
||||
// consumes SPIR-V and answers on both paths, which keeps the negative control.
|
||||
if (Gl().BackendName() == "DirectGLES" &&
|
||||
AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "Espryt cannot capture a gl_PointSize its capture stage never "
|
||||
"writes without the demotion; the PointSizeDemotion. ctest entry "
|
||||
"runs this same case with MOBILEGL_POINT_SIZE_DEMOTION=1";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kReadOnlyGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kEchoFragmentSource}},
|
||||
{"g_echo", "gl_PointSize"});
|
||||
ASSERT_NE(program, 0u)
|
||||
<< "the capture set failed to link. On a demoting configuration this is the "
|
||||
"capture request never reaching the demotion, so the point-size capture was "
|
||||
"respelled to a carrier no stage declares. Build log: "
|
||||
<< m_buildLog;
|
||||
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 2);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f))
|
||||
<< "the unrelated varying captured alongside gl_PointSize did not survive; the "
|
||||
"point-size capture took the whole set with it";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE DEMOTION SILENTLY STOPS BEING ARMED.
|
||||
//
|
||||
// Everything above captures the right bytes on llvmpipe and lavapipe whether the
|
||||
// demotion ran or not - these machines host the built-in - so those cases pin that
|
||||
// the demotion does no HARM and can say nothing about whether it happened. The
|
||||
// arming is where the cheap mistake lives: MOBILEGL_POINT_SIZE_DEMOTION maps onto
|
||||
// the two Supports*PointSize capability bits INVERTED (forcing the demotion on
|
||||
// means declaring the built-in UNHOSTED), and a swap of those arms - or a dropped
|
||||
// env bit anywhere between ConfigLoader, the backend init, CompileEnv and the L1
|
||||
// key - would disable the device repair with every rendering case still green.
|
||||
//
|
||||
// Same machinery as UnlocatedIoBlockScenario's arming case: the environment says
|
||||
// the demotion is pinned on, therefore the library must SAY it demoted something.
|
||||
// The observable is the latched MGLOG_I each backend emits when it first builds a
|
||||
// demoted program; both spell "demoted to an ordinary varying", so this one case
|
||||
// covers both pinned lanes without a backend gate.
|
||||
TEST_F(PointSizeDemotionScenario, TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the demotion pinned ON for the whole process, which "
|
||||
"is what the PointSizeDemotion. ctest entries do with "
|
||||
"MOBILEGL_POINT_SIZE_DEMOTION=1; with the variable unset the detected "
|
||||
"capabilities decide, and on this machine the built-in is hosted - so "
|
||||
"there would be nothing to observe";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_POINT_SIZE_DEMOTION is pinned on but MOBILEGL_LOG_FILE_PATH "
|
||||
"is not set, so the library has nowhere to record that it demoted "
|
||||
"anything; the PointSizeDemotion. ctest entries set both";
|
||||
}
|
||||
|
||||
// Taken BEFORE the program is built, so the line this looks for can only be one
|
||||
// this process wrote.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
|
||||
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
|
||||
{GL_GEOMETRY_SHADER, kPointGeometrySource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
||||
{"gl_PointSize"});
|
||||
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
||||
// Drawn as well as built, so a stack that defers its backend program to first
|
||||
// use still reaches the build the latched line fires in - and the capture must
|
||||
// STILL be right through the carrier.
|
||||
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
|
||||
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f))
|
||||
<< "the pinned-on lane did not even capture correctly";
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("demoted to an ordinary varying"), std::string::npos)
|
||||
<< "MOBILEGL_POINT_SIZE_DEMOTION is pinned ON, a geometry program reading and "
|
||||
"writing gl_PointSize was built and captured, and no backend ever reported "
|
||||
"demoting it. The demotion is not armed - check the override mapping in the "
|
||||
"backend inits (it is inverted on purpose), the CompileEnv accessors, and "
|
||||
"ProgramSpirvTask's verdict plumbing. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,413 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PoisonOmissionScenario.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 - NEGATIVE CONTROL B (gate G5): AN OMITTED FILL POINT ABORTS ON THAT VERB, AND ONLY THERE.
|
||||
//
|
||||
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
|
||||
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
|
||||
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
|
||||
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
|
||||
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
|
||||
// still copying the value, which is indistinguishable from a row that was never written.
|
||||
//
|
||||
// The scenario asserts both halves of "on THAT verb, and only there":
|
||||
//
|
||||
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
|
||||
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
|
||||
// mask, and the draw's own fields are stamped normally) and the
|
||||
// following glGenerateMipmap must abort naming exactly that pair.
|
||||
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
|
||||
// no Fatal at all. Without this half, "it aborted" would say nothing
|
||||
// about WHY: a poison that fired on every verb would look just as red.
|
||||
//
|
||||
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
|
||||
// entries, the second in the ambient Verify. entries (it skips when the knob IS set).
|
||||
//
|
||||
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
|
||||
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
|
||||
// sequence cannot run in the test process, and the harness's own bring-up pre-flight
|
||||
// (Harness/HeadlessGL.cpp) already establishes the shape: run it where a SIGABRT is a datum in
|
||||
// waitpid() instead of a dead lane. But that pre-flight forks BEFORE any context exists, and this case
|
||||
// cannot: the fixture has already brought one up. A bare fork() of a process holding a live Vulkan
|
||||
// device inherits the driver's mutexes with no threads to release them, and the child wedges on its
|
||||
// first submit - measured here as a 120s timeout on DirectVulkan and a clean pass on DirectGLES, which
|
||||
// is exactly the kind of backend-shaped flake a control must not have. So the child immediately
|
||||
// execve()s a fresh copy of this same test binary, filtered to the worker case below, which brings up
|
||||
// its own context from scratch and knows nothing about the parent's.
|
||||
//
|
||||
// The child gets its OWN MOBILEGL_LOG_FILE_PATH for the same reason: the library opens its log with
|
||||
// fopen(path, "w"), so a child sharing the parent's path would truncate the file the parent is about
|
||||
// to read.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||
#define MGITEST_POISON_HAVE_FORK 1
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
extern char** environ;
|
||||
#else
|
||||
#define MGITEST_POISON_HAVE_FORK 0
|
||||
#endif
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// What the PoisonOmitted. ctest entry and the CI negative-control step name. The pair is
|
||||
// spelled here so the assertion below is about the exact string the poison contracts to
|
||||
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
|
||||
constexpr const char* kOmittedVerb = "GenerateMipmap";
|
||||
constexpr const char* kOmittedField = "GetActiveTextureUnit";
|
||||
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
// Set only in the re-executed child, so the worker case below runs in that process and skips
|
||||
// everywhere else (including in the ambient lanes, where it is registered like any other case).
|
||||
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
|
||||
constexpr const char* kWorkerFilter =
|
||||
"--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
|
||||
|
||||
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 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// Where the child is told to write ITS log. Empty when the lane configured no log path at
|
||||
// all, in which case the signal is the only evidence and the text assertions are skipped.
|
||||
std::string ChildLogPath() {
|
||||
const std::filesystem::path parent = LibraryLogPath();
|
||||
if (parent.empty()) return {};
|
||||
return (parent.string() + ".poison-child");
|
||||
}
|
||||
|
||||
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>());
|
||||
}
|
||||
|
||||
class PoisonOmissionScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The sequence under test. Deliberately in this order: the DRAW comes first and must
|
||||
// survive - if the poison fired there, the "only that verb" half would be false and the
|
||||
// SIGABRT the parent waits for would prove nothing.
|
||||
void RunSequence() {
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// A two-level texture, so glGenerateMipmap has real work to do and cannot be
|
||||
// short-circuited into a no-op by a backend that inspects the level count first.
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
unsigned char pixels[8 * 8 * 4];
|
||||
for (std::size_t i = 0; i < sizeof(pixels); ++i) {
|
||||
pixels[i] = static_cast<unsigned char>(i);
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
|
||||
|
||||
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
|
||||
|
||||
// The sequence is the WHOLE datum this child reports, so a GL error in it must be
|
||||
// part of the answer rather than something only a human reading stderr would see.
|
||||
// WithoutOmissionCompletes reads the child's exit status, and the status is built
|
||||
// from HasFailure() below - so this EXPECT is what turns "the mipmap was rejected"
|
||||
// into a red parent instead of a vacuous "it exited 0, the poison did not fire".
|
||||
EXPECT_EQ(FirstGLError(), 0u)
|
||||
<< "the draw + glGenerateMipmap sequence the poison controls are about raised a "
|
||||
"GL error, so neither control is measuring what it claims to measure";
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// fork() + execve() of this same binary, filtered to the worker case, with the marker and
|
||||
// the child's own log path added to the environment. Everything that allocates happens
|
||||
// BEFORE the fork; between fork and execve only async-signal-safe work is done.
|
||||
static bool RunSequenceInAChildProcess(int& outStatus, std::string& outReason) {
|
||||
std::vector<std::string> env;
|
||||
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
|
||||
const std::string text(*entry);
|
||||
if (text.rfind("MOBILEGL_LOG_FILE_PATH=", 0) == 0) continue;
|
||||
if (text.rfind(std::string(kChildMarker) + "=", 0) == 0) continue;
|
||||
env.push_back(text);
|
||||
}
|
||||
env.push_back(std::string(kChildMarker) + "=1");
|
||||
const std::string childLog = ChildLogPath();
|
||||
if (!childLog.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(childLog, ec);
|
||||
env.push_back("MOBILEGL_LOG_FILE_PATH=" + childLog);
|
||||
}
|
||||
|
||||
std::vector<char*> envp;
|
||||
envp.reserve(env.size() + 1);
|
||||
for (std::string& entry : env) envp.push_back(entry.data());
|
||||
envp.push_back(nullptr);
|
||||
|
||||
std::string exe = "/proc/self/exe";
|
||||
std::string arg0 = "MobileGLIntegrationTest";
|
||||
std::string filter = kWorkerFilter;
|
||||
char* argv[] = {arg0.data(), filter.data(), nullptr};
|
||||
|
||||
std::fflush(nullptr);
|
||||
const pid_t child = fork();
|
||||
if (child < 0) {
|
||||
outReason = "fork() failed";
|
||||
return false;
|
||||
}
|
||||
if (child == 0) {
|
||||
execve(exe.c_str(), argv, envp.data());
|
||||
// execve only returns on failure; _exit, never exit(), because every atexit
|
||||
// handler in this address space belongs to the parent's copy of the world.
|
||||
std::fprintf(stderr, "[itest] poison child: execve(/proc/self/exe) failed\n");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
constexpr int kTimeoutMs = 120000;
|
||||
int waitedMs = 0;
|
||||
for (;;) {
|
||||
const pid_t reaped = waitpid(child, &outStatus, WNOHANG);
|
||||
if (reaped == child) return true;
|
||||
if (reaped < 0) {
|
||||
outReason = "waitpid on the poison worker failed";
|
||||
return false;
|
||||
}
|
||||
if (waitedMs >= kTimeoutMs) {
|
||||
kill(child, SIGKILL);
|
||||
(void)waitpid(child, &outStatus, 0);
|
||||
outReason = "the poison worker made no progress in 120s and was killed";
|
||||
return false;
|
||||
}
|
||||
timespec nap{0, 10 * 1000 * 1000};
|
||||
nanosleep(&nap, nullptr);
|
||||
waitedMs += 10;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string DescribeStatus(int status) {
|
||||
if (WIFEXITED(status)) return "exited with status " + std::to_string(WEXITSTATUS(status));
|
||||
if (WIFSIGNALED(status)) return "died on signal " + std::to_string(WTERMSIG(status));
|
||||
return "ended in an unrecognised way";
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
// The worker. It is a normal registered case so that the re-executed child can be selected
|
||||
// with nothing but --gtest_filter, and it skips in every process that is not that child.
|
||||
TEST_F(PoisonOmissionScenario, TheSequenceThePoisonControlsRun) {
|
||||
if (std::getenv(kChildMarker) == nullptr) {
|
||||
GTEST_SKIP() << "this case is the body the two poison controls run in a child process; "
|
||||
"it does nothing unless " << kChildMarker << " is set, which only the "
|
||||
"re-exec below does";
|
||||
}
|
||||
if (!Ready()) return;
|
||||
|
||||
RunSequence();
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// _exit, and not a return into gtest's teardown: this process exists to reach the verb
|
||||
// above and its exit status is the datum the parent reads. A normal teardown of a live
|
||||
// context could add signals of its own to that answer.
|
||||
//
|
||||
// HasFailure(), not 0: RunSequence() is full of ASSERT_/EXPECT_ macros, and a fatal one
|
||||
// (the shader failing to compile, say) RETURNS from RunSequence before the draw and the
|
||||
// glGenerateMipmap ever happen. Exiting 0 there would have WithoutOmissionCompletes pass
|
||||
// on a child that ran none of the sequence it is the control for - green because nothing
|
||||
// happened. The child's assertion text is on its stderr, which ctest captures.
|
||||
std::fflush(nullptr);
|
||||
_exit(::testing::Test::HasFailure() ? 1 : 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
|
||||
GTEST_SKIP() << "this case is the poison's negative control and needs "
|
||||
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
|
||||
"is what the PoisonOmitted. ctest entries set";
|
||||
}
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
|
||||
const std::string expectedPair = std::string(kOmittedField) + "@" + kOmittedVerb;
|
||||
if (knob != std::string(kOmittedVerb) + ":" + kOmittedField) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
|
||||
<< "how to provoke " << kOmittedVerb << ":" << kOmittedField;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
ASSERT_TRUE(WIFSIGNALED(status))
|
||||
<< "with the stamp of " << expectedPair << " omitted, the glGenerateMipmap in the child "
|
||||
<< "had to read a field its verb never filled and abort. It " << DescribeStatus(status)
|
||||
<< " instead - the poison is not armed (a build without MOBILEGL_PIPE_POISON, a filler "
|
||||
"that stamps what it was told to skip, or a backend that no longer reads the field "
|
||||
"through the accessor). Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WTERMSIG(status), SIGABRT)
|
||||
<< "the child died on signal " << WTERMSIG(status) << " rather than SIGABRT; the poison "
|
||||
"reports through MGLOG_F + std::abort(), so any other signal is a different crash. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
|
||||
if (ChildLogPath().empty()) {
|
||||
GTEST_SKIP() << "the abort happened, but the lane set no MOBILEGL_LOG_FILE_PATH, so the "
|
||||
"Fatal's text cannot be read back; the PoisonOmitted. ctest entries set it";
|
||||
}
|
||||
EXPECT_NE(childLog.find(std::string(kFatalPrefix) + ", \"" + expectedPair + "\""),
|
||||
std::string::npos)
|
||||
<< "the child aborted, but not with Fatal{UnmigratedPipeInput, \"" << expectedPair
|
||||
<< "\"} - that message is the whole diagnostic value of the poison. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("@DrawArrays"), std::string::npos)
|
||||
<< "the draw that ran BEFORE the omitted verb also tripped the poison, so the omission "
|
||||
"is not scoped to its verb: the fill classes are wrong, or the stamps are global. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
// The sibling control, in the ambient Verify. lanes: the same sequence with the knob UNSET
|
||||
// must run to completion and log no Fatal at all.
|
||||
//
|
||||
// It deliberately does NOT skip when MOBILEGL_PIPE_POISON_OMIT is set. This is the entry
|
||||
// CI's always-on negative control B exports the knob at: a green entry that the omission
|
||||
// turns red is the whole proof that the poison is armed, and an entry that politely skipped
|
||||
// itself would report that green either way. Nothing else in the integration suite calls
|
||||
// glGenerateMipmap, so this case is also the only possible target for that control.
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
|
||||
"build there is nothing for this control to be a control OF";
|
||||
}
|
||||
const bool omissionArmed = StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT");
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
const std::string note =
|
||||
omissionArmed
|
||||
? std::string(
|
||||
" NOTE: MOBILEGL_PIPE_POISON_OMIT is set in this process, so this failure is "
|
||||
"what CI's negative control B is asking for - the poison IS armed, and this "
|
||||
"entry going red is the proof.")
|
||||
: std::string();
|
||||
ASSERT_TRUE(WIFEXITED(status))
|
||||
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
|
||||
<< DescribeStatus(status)
|
||||
<< ". If it aborted, the poison is firing on a field the verb's fill table SHOULD list - "
|
||||
"add the row to MG_Pipe/FillPoints.def, never mark the field sticky."
|
||||
<< note << " Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WEXITSTATUS(status), 0)
|
||||
<< "the child " << DescribeStatus(status)
|
||||
<< ". Status 1 is the child's OWN assertion failing inside the sequence (it exits "
|
||||
"HasFailure() ? 1 : 0), so its gtest output on this job's stderr names the line; "
|
||||
"anything else came from the harness. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("Fatal{"), std::string::npos)
|
||||
<< "an unpoisoned run logged a Fatal:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,554 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.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 - GL_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
|
||||
// INACTIVE.
|
||||
//
|
||||
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
|
||||
// capture or no capture. The CTS leans its whole tessellation suite on that - the
|
||||
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
|
||||
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
|
||||
// tessellation tests are sized from the answer - so a backend that answers 0
|
||||
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
|
||||
//
|
||||
// DirectVulkan serves the query from the transform-feedback stream query's
|
||||
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
|
||||
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
|
||||
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
|
||||
// the pair reads back 0. Where the bring-up probe measures that defect with a
|
||||
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
|
||||
// renderer accumulates XFB-inactive draws through the best proven substitute
|
||||
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
|
||||
// rasterizer discard included), else pipeline statistics over clipping-stage
|
||||
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
|
||||
// answer, so on this machine they hold the reroute to the same numbers the
|
||||
// healthy stream path must produce - the "two pools must agree" assertion - and
|
||||
// on a healthy driver they pin the stream path itself.
|
||||
//
|
||||
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
|
||||
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
|
||||
// is a different mechanism with its own tests.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#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 {
|
||||
|
||||
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||
if (log != nullptr) *log = buffer.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
|
||||
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
|
||||
// same program and simply never begin a span.
|
||||
const char* const kVertexSource = R"(#version 430 core
|
||||
out vec4 vs_out_value;
|
||||
void main() {
|
||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vs_out_value = vec4(1.0);
|
||||
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
|
||||
// triangle per patch - the count the tessellation cases assert.
|
||||
const char* const kTessVertexSource = R"(#version 430 core
|
||||
void main() {
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
const char* const kTessControlSource = R"(#version 430 core
|
||||
layout(vertices = 1) out;
|
||||
void main() {
|
||||
gl_TessLevelOuter[0] = 1.0;
|
||||
gl_TessLevelOuter[1] = 1.0;
|
||||
gl_TessLevelOuter[2] = 1.0;
|
||||
gl_TessLevelOuter[3] = 1.0;
|
||||
gl_TessLevelInner[0] = 1.0;
|
||||
gl_TessLevelInner[1] = 1.0;
|
||||
}
|
||||
)";
|
||||
const char* const kTessEvalSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
void main() {
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The same tessellation pipeline with something to capture, so that
|
||||
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
|
||||
// open (but paused) capture span AND a tessellator in one program.
|
||||
const char* const kTessEvalCaptureSource = R"(#version 430 core
|
||||
layout(triangles, equal_spacing, cw) in;
|
||||
out vec4 te_out_value;
|
||||
void main() {
|
||||
te_out_value = vec4(1.0);
|
||||
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != std::string("DirectVulkan")) {
|
||||
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
|
||||
<< Gl().BackendName()
|
||||
<< " answers this query from a different mechanism";
|
||||
}
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenQueries(2, m_queries);
|
||||
ASSERT_NE(m_queries[0], 0u);
|
||||
ASSERT_NE(m_queries[1], 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
|
||||
m_queries[0] = m_queries[1] = 0;
|
||||
for (const GLuint program : m_programs) {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
// captureVarying: the name to record with glTransformFeedbackVaryings, or
|
||||
// nullptr for a program that can never open a capture span.
|
||||
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
|
||||
const char* captureVarying) {
|
||||
std::vector<GLuint> shaders;
|
||||
for (const auto& [type, source] : stages) {
|
||||
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
|
||||
if (shader == 0) {
|
||||
for (const GLuint built : shaders) glDeleteShader(built);
|
||||
return 0;
|
||||
}
|
||||
shaders.push_back(shader);
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
if (captureVarying != nullptr) {
|
||||
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
|
||||
}
|
||||
glLinkProgram(program);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status == GL_FALSE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint BuildCaptureProgram() {
|
||||
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
|
||||
}
|
||||
|
||||
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTessGenLevel < 1) return 0;
|
||||
return BuildProgram(
|
||||
{{GL_VERTEX_SHADER, kTessVertexSource},
|
||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
||||
{GL_TESS_EVALUATION_SHADER,
|
||||
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
|
||||
withCaptureVarying ? "te_out_value" : nullptr);
|
||||
}
|
||||
|
||||
// A capture span that is open but PAUSED. The pause closes the capture, so
|
||||
// every draw inside it is XFB-inactive at the backend - the stream query's
|
||||
// silent case - while the GL span stays active. `program` must be the one
|
||||
// that is bound: GL requires the same program at resume.
|
||||
void BeginPausedSpan() {
|
||||
glGenBuffers(1, &m_captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glPauseTransformFeedback();
|
||||
}
|
||||
|
||||
void EndPausedSpan() {
|
||||
glResumeTransformFeedback();
|
||||
glEndTransformFeedback();
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
|
||||
m_captureBuffer = 0;
|
||||
}
|
||||
|
||||
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
|
||||
GLuint QueryGenerated(const std::function<void()>& record) {
|
||||
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
||||
record();
|
||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
||||
GLuint generated = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
|
||||
return generated;
|
||||
}
|
||||
|
||||
static GLenum DrainGLErrors() {
|
||||
const GLenum first = glGetError();
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
|
||||
GLuint m_captureBuffer = 0;
|
||||
std::vector<GLuint> m_programs;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// The plain shape: no capture object was ever bound, no span begun, no
|
||||
// rasterizer discard - just a GENERATED query around two triangles. On a
|
||||
// healthy driver the stream query answers it; on an affected one the armed
|
||||
// reroute must produce the same 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
|
||||
"inactive (GL 4.6 core 13.4)";
|
||||
}
|
||||
|
||||
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
|
||||
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
|
||||
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
|
||||
// tests' capture buffers on the affected device.
|
||||
//
|
||||
// On lavapipe this case holds through the dedicated
|
||||
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
|
||||
// what makes a discarded draw countable there - llvmpipe's clipping
|
||||
// statistics AND stream query both read 0 under discard).
|
||||
//
|
||||
// The value-conditioned skip below is deliberate and narrow, for a stack
|
||||
// with NO counter that survives discard: there this case is unfalsifiable,
|
||||
// and a red would indict MobileGL for a hole the bring-up probe already
|
||||
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
|
||||
// exact-zero answer IS the capability signal - any wrong nonzero count
|
||||
// still fails - and on every driver that counts discarded draws at all the
|
||||
// full assertion runs. The device probe list holds this shape on the Mali.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
glEnable(GL_RASTERIZER_DISCARD);
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
|
||||
glDisable(GL_RASTERIZER_DISCARD);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (generated == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
|
||||
"primitives-generated query, clipping statistics) survives "
|
||||
"rasterizer discard for an XFB-inactive draw on this stack - the "
|
||||
"shape is unfalsifiable here; the bring-up probe measures the same "
|
||||
"hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(generated, 2u)
|
||||
<< "rasterizer discard drops primitives after clipping and must not hide them from "
|
||||
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
|
||||
"tessellator with";
|
||||
}
|
||||
|
||||
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
|
||||
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
|
||||
// protocol derives everything from. Undiscarded, so that the answer is
|
||||
// holdable on this machine through whichever accounting path is armed (the
|
||||
// discard interaction is the case above's business, measured separately).
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram();
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u)
|
||||
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
|
||||
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
|
||||
}
|
||||
|
||||
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
|
||||
// captured one, then another XFB-inactive one. The GENERATED answer must
|
||||
// accumulate across the two accounting paths the armed reroute splits them
|
||||
// into (stream slots for the captured draw, statistics slots for the
|
||||
// others), and WRITTEN must stay exactly the captured draw's count - the
|
||||
// pairing the stream path exists to keep exact. Undiscarded, so the
|
||||
// accumulation invariant is holdable on this machine (see the discard
|
||||
// case's comment); the triangles rasterize into the harness framebuffer,
|
||||
// which nothing here reads.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
GLuint captureBuffer = 0;
|
||||
glGenBuffers(1, &captureBuffer);
|
||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
||||
const GLuint generated = QueryGenerated([]() {
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
|
||||
glBeginTransformFeedback(GL_TRIANGLES);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
|
||||
glEndTransformFeedback();
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
|
||||
});
|
||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
||||
|
||||
GLuint written = 0xFFFFFFFFu;
|
||||
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
|
||||
glDeleteBuffers(1, &captureBuffer);
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
|
||||
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
|
||||
}
|
||||
|
||||
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
|
||||
//
|
||||
// glPauseTransformFeedback closes the capture without closing the span, so a
|
||||
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
|
||||
// exactly as silent for it as for a draw with no span at all - while
|
||||
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
|
||||
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
|
||||
// silences). The frontend does keep a CPU counter for paused draws, but it can
|
||||
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
|
||||
// these draws are the reroute's business like any other - and the trap on the
|
||||
// other side is counting them TWICE, once in each accounting.
|
||||
//
|
||||
// Each case measures the SAME draw twice: once with no span open at all (the
|
||||
// capability control - what this stack can count) and once inside the paused
|
||||
// span, and requires the two to agree. That differential is what makes these
|
||||
// cases falsifying rather than vacuous: a stack where no counter reaches a
|
||||
// capture-less draw fails the control and skips, while a stack that counts the
|
||||
// unpaused draw and answers 0 for the paused one - which is what excluding
|
||||
// paused draws from the reroute produced - fails, instead of skipping into
|
||||
// green.
|
||||
|
||||
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
|
||||
// CPU delta, this reads 2.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
|
||||
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
|
||||
"draws are CPU-counted on top of whatever the stream query says";
|
||||
}
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half of the comparison proves nothing; the "
|
||||
"bring-up probe measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "one triangle drawn while the capture span is paused is still one primitive "
|
||||
"generated - counted once, by whichever accounting owns it, never by two of them "
|
||||
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
|
||||
}
|
||||
|
||||
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
|
||||
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
|
||||
// the CTS's tessellator-measuring shape. Excluding paused draws from the
|
||||
// reroute left this counted by nothing at all on the affected device.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
|
||||
if (program == 0) {
|
||||
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
|
||||
}
|
||||
glUseProgram(program);
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||
|
||||
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
|
||||
"on this stack, so the paused half proves nothing; the bring-up probe "
|
||||
"measures the same hole and the POST row reports it";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 1u)
|
||||
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
|
||||
"exactly one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "pausing the capture does not stop the tessellator from generating that triangle, "
|
||||
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
|
||||
"patch draw left out of the reroute is counted by nothing at all";
|
||||
}
|
||||
|
||||
// The other half of the same hole: the instanced entry points never reach the
|
||||
// frontend's paused accounting either, so a paused instanced draw excluded from
|
||||
// the reroute is likewise counted by nothing.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
const GLuint unpaused =
|
||||
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
BeginPausedSpan();
|
||||
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
|
||||
EndPausedSpan();
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
if (unpaused == 0u) {
|
||||
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
|
||||
"stack, so the paused half proves nothing";
|
||||
}
|
||||
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
|
||||
EXPECT_EQ(paused, unpaused)
|
||||
<< "four instances generate four primitives whether or not the capture span is "
|
||||
"paused, and no instanced entry point reaches the frontend's paused accounting";
|
||||
}
|
||||
|
||||
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
|
||||
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
|
||||
// is green here whether the reroute ran or not (that is the "two pools
|
||||
// agree" point), so none of them can say the pinned lane actually exercised
|
||||
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
|
||||
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
|
||||
// XFB-inactive draw inside a GENERATED span must make the renderer say -
|
||||
// through its latched MGLOG_I - that it engaged the reroute. It reads
|
||||
// MG_Config not at all (on Android this module links the shipping library)
|
||||
// and trusts only the log bytes appended after it started.
|
||||
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
|
||||
if (!Ready()) return;
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
|
||||
"is what the PrimGenReroute. ctest entry does with "
|
||||
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
|
||||
"decides and this machine's verdict is its own business";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
|
||||
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
|
||||
"record that it rerouted anything; the PrimGenReroute. ctest "
|
||||
"entry sets both";
|
||||
}
|
||||
|
||||
const GLuint program = BuildCaptureProgram();
|
||||
ASSERT_NE(program, 0u) << BuildLog();
|
||||
glUseProgram(program);
|
||||
|
||||
// Taken BEFORE the draw, so the line this looks for can only be one this
|
||||
// process wrote for this span. The latch fires on the FIRST rerouted
|
||||
// draw, which is inside the query below.
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
|
||||
EXPECT_EQ(DrainGLErrors(), 0u);
|
||||
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
|
||||
"a GENERATED query, and the renderer never reported engaging the reroute. The "
|
||||
"quirk is not armed - check the override mapping "
|
||||
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
|
||||
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // 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
|
||||
// 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 <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
@@ -50,6 +65,31 @@ void main() {
|
||||
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
|
||||
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 {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -114,6 +154,56 @@ void main() {
|
||||
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;
|
||||
GLuint m_buffer = 0;
|
||||
std::string m_buildLog;
|
||||
@@ -153,4 +243,68 @@ void main() {
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,280 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/Coverage.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 hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call
|
||||
// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57
|
||||
// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py).
|
||||
//
|
||||
// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and
|
||||
// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc
|
||||
// with the per-accessor table and prints the coverage summary. Rows matching neither are
|
||||
// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes
|
||||
// "regenerate and git diff --exit-code with 0 UNMAPPED".
|
||||
//
|
||||
// Three pseudo-calls stand for read points that do NOT become a forward call:
|
||||
// kClientResolved - the frontend answers it itself; the server is never asked
|
||||
// (section 4.4.6: "the server answers nothing the client can answer").
|
||||
// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1).
|
||||
// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr<MG_State...>, which
|
||||
// becomes an MGPipeHandle parameter; there is no single call to name.
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
// X(Accessor, PipeCall)
|
||||
#define MGP_COVERAGE_ACCESSOR_LIST(X) \
|
||||
X(GetActiveTextureUnit, SetSamplerViews) \
|
||||
X(GetBlendColor, SetDynamicState) \
|
||||
X(GetBlendEquationIndexed, CreateRenderState) \
|
||||
X(GetBlendFuncIndexed, CreateRenderState) \
|
||||
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
|
||||
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
|
||||
X(GetBoundVertexArray, BindVertexElements) \
|
||||
/* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \
|
||||
/* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \
|
||||
/* target argument), but by supplying the target from the EMISSION SITE, which */ \
|
||||
/* knows it exactly. The split, target by target, in BufferTarget's OWN */ \
|
||||
/* spelling (BufferObject.h:15-33) so the list cross-checks mechanically: */ \
|
||||
/* Vertex, and the per-attribute buffer of a VAO -> set_vertex_buffers */ \
|
||||
/* Index -> set_index_buffer */ \
|
||||
/* DrawIndirect, Parameter -> set_indirect_buffers */ \
|
||||
/* Uniform, ShaderStorage, AtomicCounter, */ \
|
||||
/* TransformFeedback -> set_shader_buffers */ \
|
||||
/* CopyRead, CopyWrite, PixelPack, PixelUnpack, */ \
|
||||
/* Texture -> still pulled: the */ \
|
||||
/* transfer and pixel-store targets have no call of their own yet; */ \
|
||||
/* DispatchIndirect -> still pulled, and */ \
|
||||
/* it is the one a reader most plausibly assumes set_indirect_buffers */ \
|
||||
/* covers: BOTH backends read it (SyncBoundBuffer at every */ \
|
||||
/* glDispatchComputeIndirect) and NO call carries it - MGPIndirectBuffers */ \
|
||||
/* is the DrawIndirect + Parameter pair only - so the field stays the */ \
|
||||
/* fill loop's for this target; */ \
|
||||
/* Query -> still pulled: no */ \
|
||||
/* backend reads it at all and no call names it. */ \
|
||||
/* That is 15 of 15; a target missing from this list is a target a later */ \
|
||||
/* phase would retire the pull for on the strength of a split that never */ \
|
||||
/* covered it. */ \
|
||||
/* THE ROW STAYS ONE ROW, and that is structural rather than a shortcut: this */ \
|
||||
/* list IS the MGPipeInputField enum and the PipeInputs field set, and the */ \
|
||||
/* field is ONE array (m_bufferBindingSlot[kBufferTargetCount]) that a second */ \
|
||||
/* row of the same name could only duplicate. So the split lives here and in */ \
|
||||
/* the emitters, and the row keeps naming set_indirect_buffers for the plan's */ \
|
||||
/* explicit replacement of the DrawIndirect/Parameter pair. It is deliberately */ \
|
||||
/* NOT in the EMITTED list below: seven targets above are still pulled, and a */ \
|
||||
/* row there says "the whole field is supplied", which for this field would be */ \
|
||||
/* the same half-truth GetPixelStoreParameters is kept out for. */ \
|
||||
X(GetBufferBindingSlot, SetIndirectBuffers) \
|
||||
X(GetBufferBindingPoint, SetShaderBuffers) \
|
||||
X(GetBufferBindingPointCount, SetShaderBuffers) \
|
||||
X(GetTouchedBufferBindingPointCount, SetShaderBuffers) \
|
||||
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(GetPixelStoreParameters, SetPixelPackState) \
|
||||
X(GetPolygonModeFront, CreateRenderState) \
|
||||
X(GetPolygonOffsetFactor, SetDynamicState) \
|
||||
X(GetPolygonOffsetUnits, SetDynamicState) \
|
||||
X(GetPrimitiveRestartIndex, DrawVbo) \
|
||||
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||
X(GetProgramForDraw, SetDrawProgram) \
|
||||
X(GetProgramObject, CreateShaderState) \
|
||||
/* ANSWERED by P2: it is in the pipeline half. SetProvokingVertexMode calls */ \
|
||||
/* BumpVersions(), and the chunk table's rule is exactly that, so it rides */ \
|
||||
/* pipeline chunk P4 - a strict superset of what ComputePipelineStateHash used */ \
|
||||
/* to hash (MGPipeRenderStateSpans.cpp records the provenance). */ \
|
||||
X(GetProvokingVertexMode, CreateRenderState) \
|
||||
X(GetRenderStateParameters, CreateRenderState) \
|
||||
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||
X(GetSamplingResolutionGeneration, SetSamplerViews) \
|
||||
X(GetScissorBox, SetDynamicState) \
|
||||
X(GetStencilState, CreateRenderState) \
|
||||
X(GetTextureBindGeneration, SetSamplerViews) \
|
||||
X(GetTextureContextId, SetSamplerViews) \
|
||||
X(GetTextureObject, SetSamplerViews) \
|
||||
X(GetTextureUnitObject, SetSamplerViews) \
|
||||
X(GetTransformFeedbackCapturedVertices, DrawVbo) \
|
||||
X(GetTransformFeedbackGeneration, SetStreamOutputTargets) \
|
||||
X(GetTransformFeedbackPausedPrimitiveCounter, EndStreamOutput) \
|
||||
X(GetTransformFeedbackProgram, SetStreamOutputTargets) \
|
||||
X(GetViewport, SetDynamicState) \
|
||||
X(GetViewportIndexed, SetDynamicState) \
|
||||
X(IsCapabilityEnabled, CreateRenderState) \
|
||||
X(IsCapabilityEnabledIndexed, CreateRenderState) \
|
||||
X(IsTransformFeedbackActive, BeginStreamOutput) \
|
||||
X(IsTransformFeedbackPaused, PauseStreamOutput) \
|
||||
X(InvalidateCompileEnv, kClientResolved) \
|
||||
X(ValidateProgramName, kClientResolved) \
|
||||
X(RecordError, kReverseChannel) \
|
||||
/* The D21 XFB counter-slot rekey's reads (VulkanRenderer.cpp); the calls they */ \
|
||||
/* map to are GetTransformFeedbackGeneration's. */ \
|
||||
X(GetBoundTransformFeedbackLifetimeId, SetStreamOutputTargets) \
|
||||
X(HasOpenTransformFeedbackSpan, SetStreamOutputTargets)
|
||||
|
||||
// X(Accessor, Reason) - the STICKY fields (P1 brief D6): the only PipeInputs fields whose
|
||||
// value is valid across verbs, so the poison's per-verb generation does not apply to them.
|
||||
// Exactly the seven F-class (forwarded) accessors, and the argument for each is the same:
|
||||
// it takes an argument that is not verb state - a GL name, a lifetime id, a target - i.e.
|
||||
// it is a lookup or a reverse-channel write, not a state read; there is no value the
|
||||
// filler could copy and no verb whose fill could make it stale; phase C replaces them
|
||||
// with handle tables and callbacks. None of the version/generation accessors is sticky:
|
||||
// those change under verbs and are precisely what the poison must protect. The verify
|
||||
// lane's Fatal{UnmigratedPipeInput} is fixed by a FillPoints.def row, never by a row here.
|
||||
// gen_pipe.py refuses a name that is not an accessor above.
|
||||
#define MGP_COVERAGE_STICKY_LIST(X) \
|
||||
X(GetBufferBindingPointCount, "keyed by target: a constexpr capacity table, not verb state") \
|
||||
X(GetProgramObject, "keyed by GL name: an object lookup, not verb state") \
|
||||
X(GetTextureObject, "keyed by GL name: an object lookup, not verb state") \
|
||||
X(HasOpenTransformFeedbackSpan, "keyed by lifetime id: an object lookup, not verb state") \
|
||||
X(ValidateProgramName, "keyed by GL name: a name-table lookup, not verb state") \
|
||||
X(InvalidateCompileEnv, "reverse channel: a write into the frontend, not a state read") \
|
||||
X(RecordError, "reverse channel: a write into the frontend, not a state read")
|
||||
|
||||
// X(DeltaKind, PipeCall) - for inventory rows with no accessor in the member column.
|
||||
// Read by gen_pipe.py ONLY, never by the C++ preprocessor: the delta kinds are the
|
||||
// inventory's own free-text labels, not C tokens.
|
||||
#define MGP_COVERAGE_DELTA_LIST(X) \
|
||||
X(handle-ify (wire handle), kStructuralHandle) \
|
||||
X(Buffer ops delta, ResourceRespecify)
|
||||
|
||||
// X(Accessor, PipeCall) - the EMITTED list (P2 brief D5): which P2 call now SUPPLIES this
|
||||
// PipeInputs field, so the per-verb residual fill loop no longer has to pull it out of
|
||||
// GLContext. gen_pipe.py turns it into kMGPipeFieldEmittedBy[] (generated/PipeFilled.inc);
|
||||
// a field with no row here keeps going through the fill loop, which is what makes the
|
||||
// MOBILEGL_PIPE_PUSH bitmask a true per-subsystem A/B rather than an all-or-nothing switch.
|
||||
//
|
||||
// Every name must be an accessor in MGP_COVERAGE_ACCESSOR_LIST and every call must be a
|
||||
// real call in PipeCalls.def; gen_pipe.py refuses anything else.
|
||||
//
|
||||
// The one row whose call differs from the accessor list's is GetPrimitiveRestartIndex:
|
||||
// coverage maps it onto draw_vbo because that is where a backend reads it, but the VALUE
|
||||
// travels in dynamic chunk D6, so set_dynamic_state is what supplies it.
|
||||
//
|
||||
// GetPixelStoreParameters is DELIBERATELY ABSENT, and the reason is the shape of the field
|
||||
// rather than of the call. The field is PipeInputs::m_pixelStore[2] - pack AND unpack - and
|
||||
// set_pixel_pack_state carries the PACK half only, deliberately and permanently (D10,
|
||||
// ARCHITECTURE.md 4.6 D5: nothing on the far side of the boundary reads unpack state). A row
|
||||
// here says "this field is supplied, the fill loop may skip it", which would be a half-truth:
|
||||
// the moment the render-state bitmask has its bit set, the unpack half would be written by
|
||||
// nothing while its poison stamp said it was published, so neither the poison nor the verify
|
||||
// comparator could see it. Until the field is split, the whole of it keeps going through the
|
||||
// fill loop and the pack half is simply written twice.
|
||||
//
|
||||
// P3a ADDS ONE ROW, GetBoundVertexArray -> BindVertexElements, and it is the vertex-input
|
||||
// family's only candidate: GetBufferBindingSlot is polymorphic over a target set P3a covers
|
||||
// only part of (see its comment above) and GetCurrentVertexAttribute has been here since P2.
|
||||
// The row is INERT until the vertex-input subsystem is wired - MG_Impl/Pipe/PipeFill.cpp's
|
||||
// kMGPipeWiredSubsystems does not carry that bit at the contract commit, because the emitters
|
||||
// beside it are still stubs - which is exactly the guard that lets a row land before the call
|
||||
// that carries it exists.
|
||||
//
|
||||
// A NOTE FOR THE COMMIT THAT WIRES IT, because it is not visible from this file: the field is
|
||||
// a shared pointer to the frontend VAO, and the vertex-input calls supply the CONFIGURATION
|
||||
// (the applier's MGPipeVertexElementsRecord), not the object. So the row is shape-only in the
|
||||
// same sense GetCurrentVertexAttribute's is, and it stays so until the backend's twin
|
||||
// resolution reads the applier's BoundVertexElements instead of the object - at which point
|
||||
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
|
||||
// rather than silently by this row's presence.
|
||||
//
|
||||
// P4a ADDS SIX ROWS, and the same note applies to every one of them: each is SHAPE-ONLY, each
|
||||
// lands in PipeFill.cpp's EmittedCallSuppliesTheWholeField FALSE arm, and the decision is
|
||||
// taken THERE rather than inherited from a row's presence here. The rows and their calls:
|
||||
//
|
||||
// GetFramebufferBindingSlot -> SetFramebufferState GetProgramForDraw -> SetDrawProgram
|
||||
// GetImageTextureBinding -> SetShaderImages GetProgramForDispatch -> SetDispatchProgram
|
||||
// GetTextureUnitObject -> SetSamplerViews GetMaxTouchedTextureUnit -> SetSamplerViews
|
||||
//
|
||||
// Five of the six are the pointer-storage case GetBoundVertexArray already documents: the
|
||||
// field is a BindingSlot<FramebufferObject>, an ImageTextureBinding, a TextureUnit or a
|
||||
// SharedPtr<ProgramObject> - frontend heap references - and the calls carry eight-byte
|
||||
// {slot, gen} handles and resolved descriptors. The applier has no way to produce a pointer
|
||||
// and P4a deliberately does not give it one; skipping the pull would leave every one of those
|
||||
// mirrors null on every draw of every push build. What retires those pulls is not a better
|
||||
// applier, it is the phase where the backend stops reading a frontend object at all.
|
||||
//
|
||||
// THE SIXTH 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,340 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/FillPoints.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 per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the
|
||||
// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into
|
||||
// generated/PipeFillPoints.inc:
|
||||
//
|
||||
// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class
|
||||
// MGP_FILL_CLASS_LIST the verb classes
|
||||
// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read
|
||||
//
|
||||
// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable
|
||||
// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and
|
||||
// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum
|
||||
// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every
|
||||
// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go
|
||||
// through BackendObject virtuals and read no frontend state, so they are not verbs here.
|
||||
//
|
||||
// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every
|
||||
// class and are not listed. The verify lane is the oracle for this table: a
|
||||
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
|
||||
// row, never by marking the field sticky.
|
||||
//
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// THE VERDICT ON THE EIGHT STATICALLY OVER-APPROXIMATED ROWS (P2 brief C.1, MEASUREMENTS.md
|
||||
// section 4). Every one of them is KEPT, and the reason is the same in all three groups: the
|
||||
// row is not a guess, it names a concrete backend path, and the only evidence that could
|
||||
// retire it is DYNAMIC - a corpus that never reaches the path proves nothing, because a row
|
||||
// removed on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build.
|
||||
//
|
||||
// kReadback + IsTransformFeedbackActive / IsTransformFeedbackPaused
|
||||
// KEPT. The depth/stencil read emulation draws (ScopedEmulationDrawState, DirectGLES.cpp)
|
||||
// and pauses an active capture around its own draw, so a glReadPixels of a depth or
|
||||
// stencil attachment reads the transform-feedback state exactly as a draw does. Reached
|
||||
// only when the emulation is armed, which is a driver-shaped decision, so no desktop
|
||||
// corpus can decide it.
|
||||
//
|
||||
// kTextureOp + IsCapabilityEnabled, kDispatch + IsCapabilityEnabled
|
||||
// KEPT. Magma's GenerateMipmap materialises a texture's queued clear before it blits and
|
||||
// PrepareStorageImageTextures does the same for every storage image a dispatch writes;
|
||||
// both go through VkClearManager::PreCompensateSrgbClearColor, which reads
|
||||
// GL_FRAMEBUFFER_SRGB. The P2 contract gave that capability real storage for the first
|
||||
// time, so this row went from reading a compile-time constant to reading real state -
|
||||
// which is the opposite of a row that could be dropped.
|
||||
//
|
||||
// kBlitOrCopy / kTextureOp + the shader blit's viewport and vertex/buffer bindings
|
||||
// (GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode, GetBufferBindingPoint)
|
||||
// KEPT. TryBlitToDefaultFramebufferWithShader is a real draw of a backend-owned helper
|
||||
// program: ApplyGLViewportState -> ComputeGLViewport reads viewport 0 and its depth range,
|
||||
// GetOrCreateBlitPipeline -> SelectProvokingVertexMode reads the provoking vertex, and
|
||||
// BindProgramUniformBuffers' block resolvers read the frontend binding points. It is taken
|
||||
// when a blit's destination is the default framebuffer and the driver cannot do it
|
||||
// natively - again a driver-shaped decision.
|
||||
//
|
||||
// What WOULD retire a row: the poison build already answers "was this field read at this
|
||||
// verb" exactly (MOBILEGL_PIPE_POISON_OMIT withholds one field's stamp for one verb and a
|
||||
// read of it aborts naming the pair). Turning that into a retirement gate means running the
|
||||
// omission across the full CTS caselist on both devices, not the desktop corpus.
|
||||
//
|
||||
// P3a STATUS: still not done, and deliberately not done here. P3a is one of the five
|
||||
// architecture boundaries that owe a full gl44to46 caselist run on both devices, so the
|
||||
// omission sweep rides that run rather than duplicating it - the verdict lands with the
|
||||
// caselist result at the phase exit, off the critical path, and every row above stays in
|
||||
// place until it does. A row retired on desktop evidence would be retired on evidence that
|
||||
// cannot support it, which is the reason this item exists rather than a scheduling excuse.
|
||||
// ---------------------------------------------------------------------------------------
|
||||
//
|
||||
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
// X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h),
|
||||
// in declaration order. gen_pipe.py parses that struct and refuses a row set that is not exactly its member set.
|
||||
#define MGP_FILL_VERB_LIST(X) \
|
||||
X(DrawArrays, kDraw) \
|
||||
X(DrawElements, kDraw) \
|
||||
X(DrawElementsBaseVertex, kDraw) \
|
||||
X(MultiDrawArrays, kDraw) \
|
||||
X(MultiDrawElements, kDraw) \
|
||||
X(MultiDrawElementsBaseVertex, kDraw) \
|
||||
X(MultiDrawElementsIndirect, kDraw) \
|
||||
X(MultiDrawArraysIndirect, kDraw) \
|
||||
X(MultiDrawElementsIndirectCount, kDraw) \
|
||||
X(MultiDrawArraysIndirectCount, kDraw) \
|
||||
X(DrawRangeElementsBaseVertex, kDraw) \
|
||||
X(DrawRangeElements, kDraw) \
|
||||
X(DrawElementsInstancedBaseVertexBaseInstance, kDraw) \
|
||||
X(DrawElementsInstancedBaseVertex, kDraw) \
|
||||
X(DrawElementsInstancedBaseInstance, kDraw) \
|
||||
X(DrawElementsInstanced, kDraw) \
|
||||
X(DrawArraysInstancedBaseInstance, kDraw) \
|
||||
X(DrawArraysInstanced, kDraw) \
|
||||
X(DrawElementsIndirect, kDraw) \
|
||||
X(DrawArraysIndirect, kDraw) \
|
||||
X(Clear, kClear) \
|
||||
X(ClearBufferfi, kClear) \
|
||||
X(ClearBufferfv, kClear) \
|
||||
X(ClearBufferuiv, kClear) \
|
||||
X(ClearBufferiv, kClear) \
|
||||
X(ClearNamedFramebufferfv, kClear) \
|
||||
X(ClearNamedFramebufferfi, kClear) \
|
||||
X(ClearNamedFramebufferiv, kClear) \
|
||||
X(ClearNamedFramebufferuiv, kClear) \
|
||||
X(BlitFramebuffer, kBlitOrCopy) \
|
||||
X(BlitNamedFramebuffer, kBlitOrCopy) \
|
||||
X(CopyTexImage2D, kBlitOrCopy) \
|
||||
X(CopyTexSubImage2D, kBlitOrCopy) \
|
||||
X(CopyImageSubData, kBlitOrCopy) \
|
||||
X(GenerateMipmap, kTextureOp) \
|
||||
X(ReadPixels, kReadback) \
|
||||
X(GetTexImage, kReadback) \
|
||||
X(GetTextureImage, kReadback) \
|
||||
X(DispatchCompute, kDispatch) \
|
||||
X(DispatchComputeIndirect, kDispatch) \
|
||||
X(MemoryBarrier, kQuery) \
|
||||
X(MemoryBarrierByRegion, kQuery) \
|
||||
X(BindImageTexture, kTextureOp) \
|
||||
X(GetIntegeri_v, kQuery) \
|
||||
X(ShaderStorageBlockBinding, kProgramOp) \
|
||||
X(FenceSync, kQuery) \
|
||||
X(ClientWaitSync, kQuery) \
|
||||
X(WaitSync, kQuery) \
|
||||
X(DeleteSync, kQuery) \
|
||||
X(GetSyncStatus, kQuery) \
|
||||
X(IsTimerQuerySupported, kQuery) \
|
||||
X(BeginTimeElapsedQuery, kQuery) \
|
||||
X(EndTimeElapsedQuery, kQuery) \
|
||||
X(QueryCounterTimestamp, kQuery) \
|
||||
X(IsQueryResultAvailable, kQuery) \
|
||||
X(GetQueryResult64, kQuery) \
|
||||
X(DeleteBackendQuery, kQuery) \
|
||||
X(BeginOcclusionQuery, kQuery) \
|
||||
X(EndOcclusionQuery, kQuery) \
|
||||
X(BeginXfbPrimitivesQuery, kQuery) \
|
||||
X(EndXfbPrimitivesQuery, kQuery) \
|
||||
X(PatchParameteri, kQuery) \
|
||||
X(BeginTransformFeedback, kXfbSpan) \
|
||||
X(EndTransformFeedback, kXfbSpan) \
|
||||
X(PauseTransformFeedback, kXfbSpan) \
|
||||
X(ResumeTransformFeedback, kXfbSpan) \
|
||||
X(BindTransformFeedback, kXfbSpan) \
|
||||
X(DeleteTransformFeedback, kXfbSpan) \
|
||||
X(GetGpuTimestampNs, kQuery)
|
||||
|
||||
// X(Class) - the nine verb classes (ARCHITECTURE.md:153 names eight; kProgramOp is split out because
|
||||
// ShaderStorageBlockBinding is the one non-draw verb that syncs Espryt's render state and textures).
|
||||
#define MGP_FILL_CLASS_LIST(X) \
|
||||
X(kDraw) X(kDispatch) X(kClear) X(kBlitOrCopy) X(kTextureOp) X(kReadback) X(kXfbSpan) X(kProgramOp) X(kQuery)
|
||||
|
||||
// X(Class, Field) - the may-read table. A field named here is filled and stamped at every verb of the class;
|
||||
// a read of a field NOT named here is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
|
||||
// Derived from the verified reachability of every backend read (both backends, union), P1 brief D7.
|
||||
#define MGP_FILL_FIELD_LIST(X) \
|
||||
/* kDraw: every draw entry of both backends */ \
|
||||
X(kDraw, GetBoundVertexArray) \
|
||||
X(kDraw, GetProgramForDraw) \
|
||||
X(kDraw, GetBufferBindingSlot) \
|
||||
X(kDraw, GetBufferBindingPoint) \
|
||||
X(kDraw, GetTouchedBufferBindingPointCount) \
|
||||
X(kDraw, GetTextureUnitObject) \
|
||||
X(kDraw, GetTextureContextId) \
|
||||
X(kDraw, GetTextureBindGeneration) \
|
||||
X(kDraw, GetMaxTouchedTextureUnit) \
|
||||
X(kDraw, GetSamplingResolutionGeneration) \
|
||||
X(kDraw, GetImageTextureBinding) \
|
||||
X(kDraw, GetCurrentVertexAttribute) \
|
||||
X(kDraw, GetRenderStateParameters) \
|
||||
X(kDraw, GetRenderStateParametersVersion) \
|
||||
X(kDraw, GetPipelineStateVersion) \
|
||||
X(kDraw, GetViewport) \
|
||||
X(kDraw, GetViewportIndexed) \
|
||||
X(kDraw, GetDepthRangeIndexed) \
|
||||
X(kDraw, GetScissorBox) \
|
||||
X(kDraw, IsCapabilityEnabled) \
|
||||
X(kDraw, IsCapabilityEnabledIndexed) \
|
||||
X(kDraw, GetBlendColor) \
|
||||
X(kDraw, GetBlendFuncIndexed) \
|
||||
X(kDraw, GetBlendEquationIndexed) \
|
||||
X(kDraw, GetColorMaskIndexed) \
|
||||
X(kDraw, GetLogicOp) \
|
||||
X(kDraw, GetDepthFunc) \
|
||||
X(kDraw, GetDepthMask) \
|
||||
X(kDraw, GetStencilState) \
|
||||
X(kDraw, GetCullFaceMode) \
|
||||
X(kDraw, GetPolygonModeFront) \
|
||||
X(kDraw, GetPolygonOffsetFactor) \
|
||||
X(kDraw, GetPolygonOffsetUnits) \
|
||||
X(kDraw, GetLineWidth) \
|
||||
X(kDraw, GetMinSampleShadingValue) \
|
||||
X(kDraw, GetProvokingVertexMode) \
|
||||
X(kDraw, GetPatchVertices) \
|
||||
X(kDraw, GetPatchDefaultOuterLevel) \
|
||||
X(kDraw, GetPatchDefaultInnerLevel) \
|
||||
X(kDraw, GetPrimitiveRestartIndex) \
|
||||
X(kDraw, GetFramebufferBindingSlot) \
|
||||
X(kDraw, IsTransformFeedbackActive) \
|
||||
X(kDraw, IsTransformFeedbackPaused) \
|
||||
X(kDraw, GetTransformFeedbackProgram) \
|
||||
X(kDraw, GetTransformFeedbackGeneration) \
|
||||
X(kDraw, GetBoundTransformFeedbackLifetimeId) \
|
||||
X(kDraw, GetTransformFeedbackCapturedVertices) \
|
||||
/* kDispatch: the patch fields are Espryt's SyncCurrentProgram -> */ \
|
||||
/* AttachPassthroughTessControlStage (Managers.cpp) */ \
|
||||
X(kDispatch, GetProgramForDispatch) \
|
||||
X(kDispatch, GetBufferBindingSlot) \
|
||||
X(kDispatch, GetBufferBindingPoint) \
|
||||
X(kDispatch, GetTouchedBufferBindingPointCount) \
|
||||
X(kDispatch, GetTextureUnitObject) \
|
||||
X(kDispatch, GetTextureContextId) \
|
||||
X(kDispatch, GetTextureBindGeneration) \
|
||||
X(kDispatch, GetMaxTouchedTextureUnit) \
|
||||
X(kDispatch, GetSamplingResolutionGeneration) \
|
||||
X(kDispatch, GetImageTextureBinding) \
|
||||
X(kDispatch, GetFramebufferBindingSlot) \
|
||||
/* Magma's PrepareStorageImageTextures materialises a queued clear for every */ \
|
||||
/* storage image the dispatch writes, and the clear pre-compensates its colour */ \
|
||||
/* against GL_FRAMEBUFFER_SRGB (VkClearManager::PreCompensateSrgbClearColor). */ \
|
||||
X(kDispatch, IsCapabilityEnabled) \
|
||||
X(kDispatch, GetPatchVertices) \
|
||||
X(kDispatch, GetPatchDefaultOuterLevel) \
|
||||
X(kDispatch, GetPatchDefaultInnerLevel) \
|
||||
/* kClear */ \
|
||||
X(kClear, GetRenderStateParameters) \
|
||||
X(kClear, GetRenderStateParametersVersion) \
|
||||
X(kClear, GetViewport) \
|
||||
X(kClear, IsCapabilityEnabled) \
|
||||
X(kClear, GetFramebufferBindingSlot) \
|
||||
X(kClear, GetClearColor) \
|
||||
X(kClear, GetClearDepth) \
|
||||
X(kClear, GetClearStencil) \
|
||||
X(kClear, GetScissorBox) \
|
||||
X(kClear, GetColorMaskIndexed) \
|
||||
X(kClear, GetDepthMask) \
|
||||
X(kClear, GetStencilState) \
|
||||
X(kClear, GetTextureUnitObject) \
|
||||
X(kClear, GetTextureContextId) \
|
||||
X(kClear, GetSamplingResolutionGeneration) \
|
||||
X(kClear, GetTextureBindGeneration) \
|
||||
X(kClear, GetMaxTouchedTextureUnit) \
|
||||
X(kClear, GetImageTextureBinding) \
|
||||
/* kBlitOrCopy */ \
|
||||
X(kBlitOrCopy, GetFramebufferBindingSlot) \
|
||||
X(kBlitOrCopy, IsCapabilityEnabled) \
|
||||
X(kBlitOrCopy, GetScissorBox) \
|
||||
X(kBlitOrCopy, IsTransformFeedbackActive) \
|
||||
X(kBlitOrCopy, IsTransformFeedbackPaused) \
|
||||
X(kBlitOrCopy, GetRenderStateParameters) \
|
||||
X(kBlitOrCopy, GetRenderStateParametersVersion) \
|
||||
X(kBlitOrCopy, GetViewport) \
|
||||
X(kBlitOrCopy, GetActiveTextureUnit) \
|
||||
X(kBlitOrCopy, GetTextureUnitObject) \
|
||||
X(kBlitOrCopy, GetTextureContextId) \
|
||||
X(kBlitOrCopy, GetSamplingResolutionGeneration) \
|
||||
X(kBlitOrCopy, GetTextureBindGeneration) \
|
||||
X(kBlitOrCopy, GetMaxTouchedTextureUnit) \
|
||||
X(kBlitOrCopy, GetImageTextureBinding) \
|
||||
X(kBlitOrCopy, GetColorMaskIndexed) \
|
||||
X(kBlitOrCopy, GetDepthMask) \
|
||||
X(kBlitOrCopy, GetStencilState) \
|
||||
/* Magma's shader blit to the default framebuffer */ \
|
||||
/* (TryBlitToDefaultFramebufferWithShader) is a real draw of a backend-owned */ \
|
||||
/* helper program: it sets the dynamic viewport through ApplyGLViewportState */ \
|
||||
/* -> ComputeGLViewport (viewport 0 and its depth range), picks the pipeline's */ \
|
||||
/* provoking vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode, */ \
|
||||
/* and binds the helper's descriptors through BindProgramUniformBuffers, whose */ \
|
||||
/* buffer-block resolvers read the frontend binding points. */ \
|
||||
X(kBlitOrCopy, GetViewportIndexed) \
|
||||
X(kBlitOrCopy, GetDepthRangeIndexed) \
|
||||
X(kBlitOrCopy, GetProvokingVertexMode) \
|
||||
X(kBlitOrCopy, GetBufferBindingPoint) \
|
||||
/* kTextureOp */ \
|
||||
X(kTextureOp, GetActiveTextureUnit) \
|
||||
X(kTextureOp, GetTextureUnitObject) \
|
||||
X(kTextureOp, GetImageTextureBinding) \
|
||||
X(kTextureOp, GetTextureContextId) \
|
||||
X(kTextureOp, GetSamplingResolutionGeneration) \
|
||||
X(kTextureOp, GetTextureBindGeneration) \
|
||||
X(kTextureOp, GetMaxTouchedTextureUnit) \
|
||||
/* Magma's GenerateMipmap materialises the texture's queued clear before it */ \
|
||||
/* blits (MaterializePendingClearForTexture -> PreCompensateSrgbClearColor, */ \
|
||||
/* which reads GL_FRAMEBUFFER_SRGB), and a depth texture takes the shader path */ \
|
||||
/* (GenerateDepthMipmapWithShader -> BindProgramUniformBuffers), whose sampler */ \
|
||||
/* resolver reads the draw framebuffer for the feedback-loop check and whose */ \
|
||||
/* buffer-block resolvers read the frontend binding points. */ \
|
||||
X(kTextureOp, IsCapabilityEnabled) \
|
||||
X(kTextureOp, GetFramebufferBindingSlot) \
|
||||
X(kTextureOp, GetBufferBindingPoint) \
|
||||
/* kReadback */ \
|
||||
X(kReadback, GetPixelStoreParameters) \
|
||||
X(kReadback, GetBufferBindingSlot) \
|
||||
X(kReadback, GetFramebufferBindingSlot) \
|
||||
X(kReadback, GetActiveTextureUnit) \
|
||||
X(kReadback, GetTextureUnitObject) \
|
||||
X(kReadback, GetClampReadColor) \
|
||||
X(kReadback, IsCapabilityEnabled) \
|
||||
X(kReadback, GetRenderStateParameters) \
|
||||
X(kReadback, GetRenderStateParametersVersion) \
|
||||
X(kReadback, GetViewport) \
|
||||
X(kReadback, GetTextureContextId) \
|
||||
X(kReadback, GetSamplingResolutionGeneration) \
|
||||
X(kReadback, GetTextureBindGeneration) \
|
||||
X(kReadback, GetMaxTouchedTextureUnit) \
|
||||
X(kReadback, GetImageTextureBinding) \
|
||||
/* The depth/stencil read emulation draws (ScopedEmulationDrawState, */ \
|
||||
/* DirectGLES.cpp) and pauses an active capture around its own draw, so a */ \
|
||||
/* readback reads the transform-feedback state exactly as a draw does. */ \
|
||||
X(kReadback, IsTransformFeedbackActive) \
|
||||
X(kReadback, IsTransformFeedbackPaused) \
|
||||
/* kXfbSpan */ \
|
||||
X(kXfbSpan, GetTransformFeedbackProgram) \
|
||||
X(kXfbSpan, GetBufferBindingPoint) \
|
||||
X(kXfbSpan, GetTouchedBufferBindingPointCount) \
|
||||
X(kXfbSpan, GetTransformFeedbackCapturedVertices) \
|
||||
X(kXfbSpan, IsTransformFeedbackActive) \
|
||||
X(kXfbSpan, IsTransformFeedbackPaused) \
|
||||
X(kXfbSpan, GetTransformFeedbackGeneration) \
|
||||
X(kXfbSpan, GetBoundTransformFeedbackLifetimeId) \
|
||||
/* kProgramOp: ShaderStorageBlockBinding syncs Espryt's render state and textures */ \
|
||||
X(kProgramOp, GetRenderStateParameters) \
|
||||
X(kProgramOp, GetRenderStateParametersVersion) \
|
||||
X(kProgramOp, GetViewport) \
|
||||
X(kProgramOp, IsCapabilityEnabled) \
|
||||
X(kProgramOp, GetFramebufferBindingSlot) \
|
||||
X(kProgramOp, GetTextureUnitObject) \
|
||||
X(kProgramOp, GetTextureContextId) \
|
||||
X(kProgramOp, GetSamplingResolutionGeneration) \
|
||||
X(kProgramOp, GetTextureBindGeneration) \
|
||||
X(kProgramOp, GetMaxTouchedTextureUnit) \
|
||||
X(kProgramOp, GetImageTextureBinding) \
|
||||
/* kQuery: Magma's transform feedback query end reads the paused counter */ \
|
||||
/* (DirectVulkan.cpp); every other verb in the class reads nothing and */ \
|
||||
/* its fill is a serial bump */ \
|
||||
X(kQuery, GetTransformFeedbackPausedPrimitiveCounter)
|
||||
|
||||
// clang-format on
|
||||
@@ -0,0 +1,163 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipe.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 "MGPipeCallbacks.h"
|
||||
#include "MGPipeHandles.h"
|
||||
#include "MGPipeHostSpan.h"
|
||||
#include "MGPipeTypes.h"
|
||||
|
||||
// The MGPipe boundary (plan B section 4).
|
||||
//
|
||||
// The two interface tables are FUNCTION-POINTER STRUCTS, not virtual bases. Three reasons
|
||||
// out of this repository rather than out of gallium: the boundary already is a
|
||||
// function-pointer struct sitting on one hook point in MG_Backend/Init.cpp; a nullptr entry
|
||||
// already means "not implemented, frontend falls back", which is exactly what a
|
||||
// not-yet-migrated subsystem needs to say while it keeps pulling; and MG_Test already
|
||||
// substitutes this table to mock a backend. The rare EGL and caps surface stays on
|
||||
// pActiveBackendObject's virtual functions.
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// Unscoped on purpose: PipeCalls.def spells these as bare tokens so the same file can
|
||||
// be read by the C++ preprocessor and by scripts/gen_pipe.py.
|
||||
enum MGPipeCallClass : Uint8 {
|
||||
kScreen,
|
||||
kCtxCso,
|
||||
kCtxState,
|
||||
kCtxObject,
|
||||
kCtxVerb,
|
||||
kCtxQuery,
|
||||
kCallClassCount,
|
||||
};
|
||||
|
||||
enum MGPipeCallFlags : Uint32 {
|
||||
kNone = 0,
|
||||
// The caller must not proceed until the server has acknowledged. Rare by design.
|
||||
kNeedsAck = 1u << 0,
|
||||
// Carries an MGPBlobRef.
|
||||
kHasBlob = 1u << 1,
|
||||
// Carries a variable-length array after the fixed payload.
|
||||
kVarTail = 1u << 2,
|
||||
// Carries an MGHostSpan - the one shape that changes with the transport.
|
||||
kHostSpan = 1u << 3,
|
||||
// Answers into an MGPReplySlot; never blocks.
|
||||
kReplySlot = 1u << 4,
|
||||
// May be null in a backend's table. A null entry is a real answer ("this backend
|
||||
// does not implement it"), not an error: DirectVulkan deliberately leaves
|
||||
// buffer_subdata_resident unregistered, and SetSwapInterval likewise.
|
||||
kOptional = 1u << 5,
|
||||
};
|
||||
|
||||
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
|
||||
// 4.5.2): MG_Pipe/MGPipeRenderStateSpans.{h,cpp}, which landed with P2 and computes
|
||||
// every chunk boundary with offsetof. Include that header to use it; what stays here
|
||||
// is the generated member list at the bottom of this file, which is what the chunk
|
||||
// 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
|
||||
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
||||
#include "PipeCalls.def"
|
||||
|
||||
// G1: the two interface tables. A null entry means "not implemented" (section 4.1).
|
||||
#include "generated/PipeTables.inc"
|
||||
|
||||
// The installed tables. Zero-initialized, so an un-installed MGPipe is every entry
|
||||
// null - which is precisely the pre-migration state.
|
||||
inline MGPipeScreen gMGPipeScreen{};
|
||||
inline MGPipeContext gMGPipeContext{};
|
||||
|
||||
// G2: monolith thunks. These are what MG_Impl call sites move onto, replacing
|
||||
// gBackendFunctionsTable.GL.* one name at a time.
|
||||
#include "generated/PipeThunks.inc"
|
||||
|
||||
// G3: wire records, their size assertions, and the applier's bounds precondition.
|
||||
#include "generated/PipeWire.inc"
|
||||
|
||||
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
|
||||
#include "generated/PipeVerify.inc"
|
||||
|
||||
// G5: PipeInputs field ids and the per-verb poison generations.
|
||||
#include "generated/PipeFilled.inc"
|
||||
|
||||
// G5b: the verb enum (one per GLFunctionsTable entry), the verb classes and their
|
||||
// may-read field masks - what MGPipeFillForVerb fills and what a poison build lets a
|
||||
// verb read (FillPoints.def).
|
||||
#include "generated/PipeFillPoints.inc"
|
||||
|
||||
// G6: the backend read inventory's coverage table.
|
||||
#include "generated/PipeCoverage.inc"
|
||||
|
||||
// G7: the render-state pipeline subset, by member name.
|
||||
#include "generated/PipeSpanTable.inc"
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,61 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeCallbacks.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 "MGPipeHandles.h"
|
||||
#include "MGPipeTypes.h"
|
||||
|
||||
// The backend -> frontend reverse channel, named (plan B section 7.1).
|
||||
//
|
||||
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
|
||||
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification, texture
|
||||
// re-send requests or default-framebuffer geometry, because in Mesa the state tracker and
|
||||
// the driver share an address space. Naming them as ten callbacks plus one forward
|
||||
// terminator (MGPipeContext::ResourceSubDataComplete) is the deliberate deviation (D8).
|
||||
//
|
||||
// Installed at context creation. In a monolith these are direct calls; under split they are
|
||||
// records on the reverse channel, and their ORDER is a correctness requirement rather than
|
||||
// an optimization (section 7.4).
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct MGPipeCallbacks {
|
||||
// A driver-detected GL error that only the server could have seen.
|
||||
void (*OnGlError)(Uint32 code);
|
||||
// Ranges of a resource the GPU wrote; retires MarkGpuWritten.
|
||||
void (*OnGpuWritten)(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges);
|
||||
void (*OnBufferWriteback)(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes);
|
||||
void (*OnTextureWriteback)(MGPipeHandle res, const MGPBox* box, MGPBlobRef bytes);
|
||||
// The one new stall class in this design (D-B6): the server recast a texture and
|
||||
// needs its texels back. The client answers with zero or more ResourceSubData
|
||||
// records terminated by ResourceSubDataComplete carrying the same pullSerial.
|
||||
void (*OnTexturePullRequest)(MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount,
|
||||
Uint64 pullSerial);
|
||||
// SHAPE ONLY, never bytes: the client owns the CPU shadow and allocates the levels
|
||||
// itself.
|
||||
void (*OnMipLevelsGenerated)(MGPipeHandle res, Uint16 base, Uint16 count);
|
||||
// Retires the layering inversion where the swapchain writes into MG_Impl's
|
||||
// pDefaultFramebufferInfo.
|
||||
void (*OnSurfaceChanged)(const MGPSurfaceInfo* info);
|
||||
void (*OnCapsInvalidated)();
|
||||
// <= WARN is lossy, >= ERROR is lossless and rate limited.
|
||||
void (*OnLog)(Uint8 level, const char* text);
|
||||
// The XFB scatter is a read-modify-write of the CLIENT's shadow, so the server
|
||||
// hands back the packed scratch and the client scatters (section 7.2.1).
|
||||
void (*OnXfbScatterReady)(MGPipeHandle scratch, Uint64 packedStride, Uint64 vertices);
|
||||
};
|
||||
|
||||
// Ten, and the count is asserted so an eleventh cannot be added without touching the
|
||||
// transport's reverse-channel record table.
|
||||
inline constexpr SizeT kMGPipeCallbackCount = 10;
|
||||
static_assert(sizeof(MGPipeCallbacks) == kMGPipeCallbackCount * sizeof(void (*)()),
|
||||
"MGPipeCallbacks gained or lost a callback");
|
||||
|
||||
// Null-initialized: a backend that installs nothing sends nothing.
|
||||
inline MGPipeCallbacks gMGPipeCallbacks{};
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,109 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeHandles.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>
|
||||
|
||||
// MGPipe object identity (plan B section 4.2).
|
||||
//
|
||||
// A handle is a {slot, gen} pair minted by the CLIENT and never by the server: no create_*
|
||||
// call in the catalogue returns a server-cast handle, which is the deliberate deviation
|
||||
// from gallium (D1) that lets the whole catalogue be remoted with ZERO creation round
|
||||
// trips.
|
||||
//
|
||||
// Slots are dense and allocated PER KIND, so the server's object table is an array rather
|
||||
// than a hash map. The allocator is a free list plus a high-water mark and has nothing to
|
||||
// do with MG_State's IndexGenerator - that container's LIFO name reuse is the very problem
|
||||
// {slot, gen} exists to close.
|
||||
namespace MobileGL::MG_Pipe {
|
||||
enum class MGPipeKind : Uint8 {
|
||||
None = 0,
|
||||
Buffer = 1,
|
||||
Texture,
|
||||
Renderbuffer,
|
||||
Framebuffer,
|
||||
Xfb,
|
||||
RenderStateCso,
|
||||
VertexElementsCso,
|
||||
SamplerCso,
|
||||
SamplerViewCso,
|
||||
ShaderCso,
|
||||
Fence,
|
||||
Query,
|
||||
Context,
|
||||
KindCount,
|
||||
};
|
||||
|
||||
// 8 bytes, POD, passed by value in a register pair.
|
||||
//
|
||||
// Gen increments only when a SLOT IS REUSED - never on a respecify - so {slot, gen} is
|
||||
// unique until the same slot has been recycled 2^32 times. That bound is documented
|
||||
// rather than defended at runtime in release builds: at one recycle per frame at
|
||||
// 1000 fps a single slot would take ~50 days of continuous churn to wrap, and the
|
||||
// debug allocator asserts on the wrap.
|
||||
//
|
||||
// Two generations exist in this design and they are strictly separate (section 4.2.2):
|
||||
// this one is the CLIENT's answer to "is this still the same GL object", while MGGen is
|
||||
// the SERVER's own epoch for "did I recast my driver object". Interface rule: no MGPipe
|
||||
// call may require the client to supply or know MGGen.
|
||||
struct MGPipeHandle {
|
||||
Uint32 Slot;
|
||||
Uint32 Gen;
|
||||
|
||||
friend constexpr Bool operator==(const MGPipeHandle& a, const MGPipeHandle& b) {
|
||||
return a.Slot == b.Slot && a.Gen == b.Gen;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(MGPipeHandle) == 8, "MGPipeHandle is the 8-byte {slot, gen} pair");
|
||||
static_assert(alignof(MGPipeHandle) == 4, "MGPipeHandle must not gain padding on the wire");
|
||||
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
|
||||
|
||||
// Reserved handles (section 4.2.1).
|
||||
// {0, 0} is null for every kind.
|
||||
// {0, 1} of kind Framebuffer is the DEFAULT framebuffer. It exists so the four
|
||||
// pDefaultFramebufferInfo->defaultFBO identity comparisons in DirectGLES retire into
|
||||
// an ordinary handle compare.
|
||||
inline constexpr MGPipeHandle kMGPipeNullHandle{0, 0};
|
||||
inline constexpr MGPipeHandle kMGPipeDefaultFramebuffer{0, 1};
|
||||
|
||||
inline constexpr Bool MGPipeHandleIsNull(const MGPipeHandle& handle) {
|
||||
return handle.Slot == 0 && handle.Gen == 0;
|
||||
}
|
||||
|
||||
// Slot 0 of every kind is reserved (null, and the default framebuffer for kind
|
||||
// Framebuffer), so a real allocation starts at 1.
|
||||
inline constexpr Uint32 kMGPipeFirstAllocatableSlot = 1;
|
||||
|
||||
// ShaderCso slot space. The top 1/16 of it is reserved for PROGRAM PIPELINE COMPOSITES
|
||||
// (section 5.6.3): a composite is minted client-side out of the stage programs bound to
|
||||
// a pipeline object, and the server never learns it is a composite - it is just another
|
||||
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
||||
// lifetime bookkeeping out of the ordinary program slot allocator.
|
||||
//
|
||||
// THE ONE ENTRY POINT INTO THE BAND is MGPipeSlotAllocator::AllocateComposite(lifetimeId)
|
||||
// (MG_Impl/Pipe/SlotAllocator.h, P4a D-H7). MGPipeSlotAllocator::Allocate REFUSES the band
|
||||
// for kind ShaderCso, which is what makes "an ordinary program can never be handed a
|
||||
// composite slot" a property of the allocator rather than of its callers; the band carries
|
||||
// its own exhaustion assert, so exhausting it is a named Fatal rather than silent slot
|
||||
// theft from ordinary programs. A composite's slot has TWO independent release paths - the
|
||||
// pipeline cache's LRU eviction and the composite ProgramObject's own destructor - and
|
||||
// both go through one client-side death helper (MG_Pipe/PipeMutation.h's
|
||||
// MGPipeEmitShaderCsoDestroyAndFree), whose second call is a proven no-op because Free
|
||||
// refuses a slot that is not live at that generation.
|
||||
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
||||
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
||||
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
||||
|
||||
inline constexpr Bool MGPipeIsCompositeShaderSlot(Uint32 slot) {
|
||||
return slot >= kMGPipeShaderCsoCompositeSlotBase && slot < kMGPipeShaderCsoSlotLimit;
|
||||
}
|
||||
|
||||
static_assert(kMGPipeShaderCsoCompositeSlotBase > kMGPipeFirstAllocatableSlot,
|
||||
"the composite band must not swallow the ordinary program slots");
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -0,0 +1,57 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeHostSpan.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 ONE thing in MGPipe whose shape changes with the transport (plan B section 4.5.7).
|
||||
//
|
||||
// Monolith: Ptr addresses the frontend shadow or the application's own memory and the
|
||||
// accessor is one predictable branch. Split: Ptr is null and the bytes live in a staging
|
||||
// segment named by Seg/Offset, or - for the index bytes a server-side primitive-restart
|
||||
// rewrite or multi-draw flattening consumes - in the server's own index host mirror, which
|
||||
// costs no wire traffic at all (D-B7).
|
||||
namespace MobileGL::MG_Pipe {
|
||||
// Seg sentinels. Anything else is a real SEG_STAGE id assigned by the transport.
|
||||
inline constexpr Uint32 kMGHostSpanSegNone = 0;
|
||||
// "The bytes are already on your side": the server reads them out of the index host
|
||||
// mirror it maintains for every resource created with the ELEMENT_ARRAY bind bit while
|
||||
// kCapNeedsHostIndexBytes is set. When the mirror is over budget the tracker degrades
|
||||
// to per-draw staging and counts the bytes in index-bytes-shipped.
|
||||
inline constexpr Uint32 kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFFu;
|
||||
|
||||
struct MGHostSpan {
|
||||
// Field order is chosen so the struct is 32 bytes with natural alignment on both a
|
||||
// 64-bit and a 32-bit host: the pointer and the two 32-bit words fill the first
|
||||
// 16-byte block either way.
|
||||
const void* Ptr;
|
||||
Uint32 Seg;
|
||||
Uint32 Pad0;
|
||||
Uint64 Size;
|
||||
Uint64 Offset;
|
||||
};
|
||||
|
||||
static_assert(sizeof(MGHostSpan) == 32, "MGHostSpan is the 32-byte host-bytes descriptor");
|
||||
static_assert(std::is_trivially_copyable_v<MGHostSpan>);
|
||||
|
||||
// Split-mode resolution needs the transport's segment table, which does not exist in a
|
||||
// monolith build; the hook is a weak-ish indirection installed by MG_Remote when it is
|
||||
// compiled in. In P0 there is no transport, so a span that names a segment resolves to
|
||||
// null and every caller is still on the monolith branch.
|
||||
using MGPipeSegmentResolver = const void* (*)(Uint32 seg, Uint64 offset, Uint64 size);
|
||||
inline MGPipeSegmentResolver gMGPipeSegmentResolver = nullptr;
|
||||
|
||||
// One predictable branch on the hot path.
|
||||
inline const void* MGPipeHostBytes(const MGHostSpan& span) {
|
||||
if (span.Ptr != nullptr) {
|
||||
return static_cast<const Uint8*>(span.Ptr) + span.Offset;
|
||||
}
|
||||
if (gMGPipeSegmentResolver == nullptr) return nullptr;
|
||||
return gMGPipeSegmentResolver(span.Seg, span.Offset, span.Size);
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,630 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/MGPipeValueTypes.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
|
||||
#ifndef MOBILEGL_MG_PIPE_VALUE_TYPES_H // belt and braces: this file is reachable both as
|
||||
#define MOBILEGL_MG_PIPE_VALUE_TYPES_H // <MG_Pipe/...> and <...> (CMakeLists.txt:531,535)
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Math/VectorTypes.h> // includes only <Includes.h> + <cstring>
|
||||
#include <cstddef> // offsetof
|
||||
#include <type_traits>
|
||||
|
||||
// The value types MG_Pipe payloads embed (plan B section 6.3; ARCHITECTURE.md section on
|
||||
// the value header): the render-state, pixel-store, sampler and vertex-attribute value
|
||||
// structs and the enums they are made of. They lived in MG_State::GLState until P0.5;
|
||||
// the MG_State headers that used to define them now include this file, so every existing
|
||||
// spelling (namespace and name) compiles unchanged.
|
||||
//
|
||||
// PURITY: nothing from MG_State, MG_Impl, MG_Backend or MG_Remote -
|
||||
// scripts/check_include_closure.py probe "value-header" (ROADMAP P0.5; ARCHITECTURE.md
|
||||
// section 10.3 gate A). Adding one turns CI red. MG_Pipe never includes MG_State back.
|
||||
|
||||
namespace MobileGL {
|
||||
// GL_MAX_DRAW_BUFFERS as MobileGL advertises it. FramebufferObject::MAX_DRAW_BUFFERS is
|
||||
// defined from this constant, so the two cannot drift.
|
||||
inline constexpr Uint kMGMaxDrawBuffers = 8;
|
||||
|
||||
enum class BlendFactor {
|
||||
Zero,
|
||||
One,
|
||||
SrcColor,
|
||||
OneMinusSrcColor,
|
||||
DstColor,
|
||||
OneMinusDstColor,
|
||||
SrcAlpha,
|
||||
OneMinusSrcAlpha,
|
||||
DstAlpha,
|
||||
OneMinusDstAlpha,
|
||||
ConstantColor,
|
||||
OneMinusConstantColor,
|
||||
ConstantAlpha,
|
||||
OneMinusConstantAlpha,
|
||||
// Dual-source blend factors (GL_SRC1_*, glBindFragDataLocationIndexed); require the
|
||||
// dualSrcBlend device feature.
|
||||
Src1Color,
|
||||
OneMinusSrc1Color,
|
||||
Src1Alpha,
|
||||
OneMinusSrc1Alpha,
|
||||
BlendFactorCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class BlendEquation {
|
||||
Add,
|
||||
Subtract,
|
||||
ReverseSubtract,
|
||||
Min,
|
||||
Max,
|
||||
BlendEquationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class LogicOperation {
|
||||
Clear,
|
||||
And,
|
||||
AndReverse,
|
||||
Copy,
|
||||
AndInverted,
|
||||
Noop,
|
||||
Xor,
|
||||
Or,
|
||||
Nor,
|
||||
Equiv,
|
||||
Invert,
|
||||
OrReverse,
|
||||
CopyInverted,
|
||||
OrInverted,
|
||||
Nand,
|
||||
Set,
|
||||
LogicOperationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class DepthTestFunc {
|
||||
Never,
|
||||
Less,
|
||||
Equal,
|
||||
LessEqual,
|
||||
Greater,
|
||||
NotEqual,
|
||||
GreaterEqual,
|
||||
Always,
|
||||
DepthTestFuncCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class StencilOperation {
|
||||
Keep,
|
||||
Zero,
|
||||
Replace,
|
||||
IncrementClamp,
|
||||
DecrementClamp,
|
||||
Invert,
|
||||
IncrementWrap,
|
||||
DecrementWrap,
|
||||
StencilOperationCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class StencilFace {
|
||||
Front,
|
||||
Back,
|
||||
StencilFaceCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class PixelStoreParam {
|
||||
// Pack Parameters
|
||||
PackAlignment,
|
||||
PackRowLength,
|
||||
PackImageHeight,
|
||||
PackSkipRows,
|
||||
PackSkipPixels,
|
||||
PackSkipImages,
|
||||
PackSwapBytes,
|
||||
PackLSBFirst,
|
||||
|
||||
// Unpack Parameters
|
||||
UnpackAlignment,
|
||||
UnpackRowLength,
|
||||
UnpackImageHeight,
|
||||
UnpackSkipRows,
|
||||
UnpackSkipPixels,
|
||||
UnpackSkipImages,
|
||||
UnpackSwapBytes,
|
||||
UnpackLSBFirst,
|
||||
|
||||
PixelStoreParamCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class CullFaceMode {
|
||||
Front,
|
||||
Back,
|
||||
FrontAndBack,
|
||||
CullFaceModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class FrontFaceMode {
|
||||
CounterClockwise,
|
||||
Clockwise,
|
||||
FrontFaceModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class ProvokingVertexMode {
|
||||
FirstVertex,
|
||||
LastVertex,
|
||||
ProvokingVertexModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class CapabilityInput {
|
||||
Blend,
|
||||
ClipDistance0,
|
||||
ClipDistance1,
|
||||
ClipDistance2,
|
||||
ClipDistance3,
|
||||
ClipDistance4,
|
||||
ClipDistance5,
|
||||
ClipDistance6,
|
||||
ClipDistance7,
|
||||
ColorLogicOp,
|
||||
CullFace,
|
||||
DebugOutput,
|
||||
DebugOutputSynchronous,
|
||||
DepthClamp,
|
||||
DepthTest,
|
||||
Dither,
|
||||
FramebufferSrgb,
|
||||
LineSmooth,
|
||||
Multisample,
|
||||
PolygonOffsetFill,
|
||||
PolygonOffsetLine,
|
||||
PolygonOffsetPoint,
|
||||
PolygonSmooth,
|
||||
PrimitiveRestart,
|
||||
PrimitiveRestartFixedIndex,
|
||||
RasterizerDiscard,
|
||||
SampleAlphaToCoverage,
|
||||
SampleAlphaToOne,
|
||||
SampleCoverage,
|
||||
SampleShading,
|
||||
SampleMask,
|
||||
ScissorTest,
|
||||
StencilTest,
|
||||
TextureCubeMapSeamless,
|
||||
ProgramPointSize,
|
||||
CapabilityInputCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
struct PixelStoreParameters {
|
||||
Bool SwapBytes = false;
|
||||
Bool LSBFirst = false;
|
||||
Int RowLength = 0;
|
||||
Int ImageHeight = 0;
|
||||
Int SkipPixels = 0;
|
||||
Int SkipRows = 0;
|
||||
Int SkipImages = 0;
|
||||
Int Alignment = 4;
|
||||
};
|
||||
|
||||
struct PerBufferBlendState {
|
||||
Bool Enabled = false;
|
||||
BlendFactor SrcFactorRGB = BlendFactor::One;
|
||||
BlendFactor DstFactorRGB = BlendFactor::Zero;
|
||||
BlendFactor SrcFactorAlpha = BlendFactor::One;
|
||||
BlendFactor DstFactorAlpha = BlendFactor::Zero;
|
||||
BlendEquation ColorEquation = BlendEquation::Add;
|
||||
BlendEquation AlphaEquation = BlendEquation::Add;
|
||||
};
|
||||
|
||||
struct StencilFaceState {
|
||||
DepthTestFunc Func = DepthTestFunc::Always;
|
||||
Int Ref = 0;
|
||||
Uint32 ValueMask = 0xffffffffu;
|
||||
Uint32 WriteMask = 0xffffffffu;
|
||||
StencilOperation FailOp = StencilOperation::Keep;
|
||||
StencilOperation PassDepthFailOp = StencilOperation::Keep;
|
||||
StencilOperation PassDepthPassOp = StencilOperation::Keep;
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
Uint PatchVertices = 3;
|
||||
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL (glPatchParameterfv). The
|
||||
// tessellation levels used when a program has an evaluation stage and NO control stage -
|
||||
// GL's fixed-function pass-through (4.6 core 11.2.2). Both backends have to synthesize
|
||||
// that stage, and they bake these numbers into it, so a change here makes an already-built
|
||||
// one stale exactly as PATCH_VERTICES does. Default 1.0, per table 23.44.
|
||||
FloatVec4 PatchDefaultOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 PatchDefaultInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Float PolygonOffsetFactor = 0.0f;
|
||||
Float PolygonOffsetUnits = 0.0f;
|
||||
// GL_POLYGON_OFFSET_CLAMP (GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp): the maximum
|
||||
// magnitude of the offset glPolygonOffsetClamp's third argument allows. Zero - the default
|
||||
// - means "no clamp", which is exactly the behaviour glPolygonOffset leaves behind.
|
||||
Float PolygonOffsetClamp = 0.0f;
|
||||
|
||||
// glClipControl (GL 4.5 core 13.5). Defaults per table 23.7 are the pre-4.5 fixed
|
||||
// behaviour: origin at the lower left, depth mapped from -1..1.
|
||||
GLenum ClipOrigin = GL_LOWER_LEFT;
|
||||
GLenum ClipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
|
||||
|
||||
// Blending
|
||||
Array<PerBufferBlendState, kMGMaxDrawBuffers> BlendStates;
|
||||
LogicOperation LogicOp = LogicOperation::Copy;
|
||||
|
||||
// Depth
|
||||
Bool DepthTestEnabled = false;
|
||||
DepthTestFunc DepthFunc = DepthTestFunc::Less;
|
||||
Bool DepthMask = true;
|
||||
|
||||
// Color Mask. Per-draw-buffer state (glColorMaski); glColorMask broadcasts to all buffers.
|
||||
// Every entry is initialized to all-true in RenderState's constructor.
|
||||
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
|
||||
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
// glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples
|
||||
// that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial
|
||||
// value is 0, and the value is clamped to [0, 1] on the way in.
|
||||
Float MinSampleShadingValue = 0.0f;
|
||||
Array<StencilFaceState, 2> StencilStates{};
|
||||
|
||||
// Cull Face
|
||||
Bool CullFaceEnabled = false;
|
||||
CullFaceMode CullFaceModeSetting = CullFaceMode::Back;
|
||||
FrontFaceMode FrontFaceModeSetting = FrontFaceMode::CounterClockwise;
|
||||
ProvokingVertexMode ProvokingVertexModeSetting = ProvokingVertexMode::LastVertex;
|
||||
|
||||
// Hints (glHint). All GL 3.3 core hint targets default to GL_DONT_CARE.
|
||||
GLenum LineSmoothHint = GL_DONT_CARE;
|
||||
GLenum PolygonSmoothHint = GL_DONT_CARE;
|
||||
GLenum TextureCompressionHint = GL_DONT_CARE;
|
||||
GLenum FragmentShaderDerivativeHint = GL_DONT_CARE;
|
||||
|
||||
// Point parameters (glPointParameter). Only the two GL 3.3 core pnames.
|
||||
Float PointFadeThresholdSize = 1.0f;
|
||||
GLenum PointSpriteCoordOrigin = GL_UPPER_LEFT;
|
||||
|
||||
// Color clamping (glClampColor). Core profile exposes only GL_CLAMP_READ_COLOR.
|
||||
GLenum ClampReadColor = GL_FIXED_ONLY;
|
||||
|
||||
// Polygon rasterization mode (glPolygonMode). Core profile sets front and back together,
|
||||
// but GL_POLYGON_MODE still reports both slots, so keep them separate for a faithful query.
|
||||
GLenum PolygonModeFront = GL_FILL;
|
||||
GLenum PolygonModeBack = GL_FILL;
|
||||
|
||||
// Primitive restart index (glPrimitiveRestartIndex); consumed when GL_PRIMITIVE_RESTART is
|
||||
// enabled during an indexed draw. Default 0.
|
||||
Uint32 PrimitiveRestartIndex = 0;
|
||||
|
||||
// Scissor
|
||||
Bool ColorLogicOpEnabled = false;
|
||||
Bool DebugOutputEnabled = false;
|
||||
Bool DebugOutputSynchronousEnabled = false;
|
||||
Bool DitherEnabled = true;
|
||||
Bool LineSmoothEnabled = false;
|
||||
Bool MultisampleEnabled = true;
|
||||
Bool PolygonOffsetFillEnabled = false;
|
||||
Bool PolygonOffsetLineEnabled = false;
|
||||
Bool PolygonOffsetPointEnabled = false;
|
||||
Bool PolygonSmoothEnabled = false;
|
||||
Bool PrimitiveRestartEnabled = false;
|
||||
Bool PrimitiveRestartFixedIndexEnabled = false;
|
||||
Bool RasterizerDiscardEnabled = false;
|
||||
Bool SampleAlphaToCoverageEnabled = false;
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool SampleShadingEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// One bit per viewport, set the first time the application writes that index's scissor
|
||||
// rectangle - glScissor broadcasts and sets all 16, glScissorIndexed/glScissorArrayv set
|
||||
// the indices they name. It exists because the RECTANGLE cannot answer "has the
|
||||
// application spoken?": ScissorBoxes starts all-zero (its spec initial value is the size
|
||||
// of a window the frontend does not know yet, see the RenderState constructor), and
|
||||
// glScissor(0, 0, 0, 0) is a legal GL state meaning "the scissor test rejects every
|
||||
// fragment". A backend that reads an empty rectangle as the never-written sentinel
|
||||
// therefore INVERTS that request into "accept every fragment"; DirectGLES did exactly
|
||||
// that and KHR-GL43.viewport_array.scissor_zero_dimension caught it. Deliberately beside
|
||||
// ScissorBoxes so it shares their tail span (after LogicOp) and DirectGLES' span memcmp
|
||||
// picks a transition up like any other state.
|
||||
Uint32 ScissorBoxWrittenMask = 0;
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
// eight numbered capabilities cannot share. Lives in the tail span (after LogicOp), so
|
||||
// DirectGLES' span memcmp picks a change up like any other capability.
|
||||
Uint32 ClipDistanceEnabledMask = 0;
|
||||
};
|
||||
|
||||
enum class SamplerFilterMode {
|
||||
Nearest,
|
||||
Linear,
|
||||
SamplerFilterCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerMipmapMode {
|
||||
None,
|
||||
Nearest,
|
||||
Linear,
|
||||
SamplerMipmapModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerWrapMode {
|
||||
ClampToEdge,
|
||||
MirroredRepeat,
|
||||
Repeat,
|
||||
ClampToBorder,
|
||||
MirrorClampToEdge,
|
||||
SamplerWrapModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerCompareMode {
|
||||
None,
|
||||
CompareToTexture,
|
||||
SamplerCompareModeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
enum class SamplerCompareFunc {
|
||||
Never,
|
||||
Less,
|
||||
Equal,
|
||||
LessEqual,
|
||||
Greater,
|
||||
NotEqual,
|
||||
GreaterEqual,
|
||||
Always,
|
||||
SamplerCompareFuncCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
// Which of the three GL_TEXTURE_BORDER_COLOR entry-point families last wrote the border colour,
|
||||
// and therefore which of the three stored representations is AUTHORITATIVE. GL 4.6 core 8.10:
|
||||
// TexParameterIiv/Iuiv store an integer border colour "unmodified, with an internal data type of
|
||||
// integer", TexParameterfv stores a floating-point one, and the derived forms are only a
|
||||
// convenience for a getter of the other spelling. A backend cannot pick the right driver entry
|
||||
// point (glSamplerParameterIiv vs fv) or the right VkBorderColor family without this: numerically
|
||||
// the three representations are always populated, so the value alone says nothing about the form.
|
||||
enum class BorderColorForm : Uint8 {
|
||||
Float,
|
||||
Int,
|
||||
Uint
|
||||
};
|
||||
|
||||
struct SamplerParameters {
|
||||
SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
|
||||
SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
|
||||
SamplerWrapMode wrapR = SamplerWrapMode::Repeat;
|
||||
SamplerFilterMode minFilter = SamplerFilterMode::Nearest;
|
||||
SamplerFilterMode magFilter = SamplerFilterMode::Linear;
|
||||
SamplerMipmapMode mipmapMode = SamplerMipmapMode::Linear;
|
||||
Float minLod = -1000.0f;
|
||||
Float maxLod = 1000.0f;
|
||||
Float lodBias = 0.0f;
|
||||
Float maxAnisotropy = 1.0f;
|
||||
// GL 4.6 core table 23.18 / GLES 3.2 table 21.16: TEXTURE_COMPARE_FUNC starts at LEQUAL,
|
||||
// for both sampler objects and the sampler state a texture object carries.
|
||||
SamplerCompareFunc compareFunc = SamplerCompareFunc::LessEqual;
|
||||
SamplerCompareMode compareMode = SamplerCompareMode::None;
|
||||
// TEXTURE_BORDER_COLOR is sampler state (GL 4.6 core table 23.18), so it belongs here and
|
||||
// not on the texture - a texture object reaches it through the sampler object it owns. The
|
||||
// three representations are the float, integer and unsigned-integer forms glSamplerParameterfv,
|
||||
// glSamplerParameterIiv and glSamplerParameterIuiv set; whichever is written last defines
|
||||
// the colour and the other two follow it, so a getter always has an answer.
|
||||
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 borderColorI = {0, 0, 0, 0};
|
||||
UintVec4 borderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm borderColorForm = BorderColorForm::Float;
|
||||
};
|
||||
|
||||
namespace MG_State::GLState {
|
||||
class BufferObject;
|
||||
|
||||
struct VertexAttribute {
|
||||
Bool Enabled = false;
|
||||
int Size = 4;
|
||||
DataType Type = DataType::Float32;
|
||||
Bool Normalized = false;
|
||||
// The RESOLVED byte distance between consecutive elements, never the raw
|
||||
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
|
||||
// packed" and is resolved to the element size here, so a zero that survives
|
||||
// into this field can only have come from the binding model, where a zero
|
||||
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
|
||||
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
|
||||
// consume this verbatim; collapsing 0 back into the element size is what made
|
||||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
||||
int Stride = 0;
|
||||
SizeT Offset = 0;
|
||||
Bool IsInteger = false;
|
||||
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
|
||||
// Set only by the long (L) format entry points. It is NOT implied by
|
||||
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
|
||||
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
|
||||
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
|
||||
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
|
||||
Bool IsLong = false;
|
||||
Bool IsBgra = false;
|
||||
Uint Divisor = 0;
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
|
||||
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
|
||||
// arguments of the last glVertexAttrib*Pointer call on this attribute,
|
||||
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
|
||||
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
|
||||
// and the binding model does overwrite those, so the two views have to be
|
||||
// stored apart or the binding-model sequence reports a legacy state it never
|
||||
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
|
||||
int LegacyStride = 0;
|
||||
SizeT LegacyPointer = 0;
|
||||
};
|
||||
|
||||
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
||||
// binding-point API are resolved eagerly into the flat VertexAttribute view above, so
|
||||
// backends keep consuming resolved attributes and never see binding points.
|
||||
struct VertexBufferBindingPoint {
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
SizeT Offset = 0;
|
||||
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
||||
int Stride = 16;
|
||||
Uint Divisor = 0;
|
||||
};
|
||||
|
||||
struct VertexAttributeVersion {
|
||||
Uint16 FormatVersion = 0;
|
||||
Uint16 BufferVersion = 0;
|
||||
Uint16 SwitchVersion = 0;
|
||||
};
|
||||
} // 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
|
||||
// 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
|
||||
// therefore not trivially copyable (MGPipeTypes.h carries them as a blob).
|
||||
static_assert(std::is_trivially_copyable_v<PixelStoreParameters> && sizeof(PixelStoreParameters) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<PerBufferBlendState> && sizeof(PerBufferBlendState) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<StencilFaceState> && sizeof(StencilFaceState) == 28);
|
||||
static_assert(std::is_trivially_copyable_v<RenderStateParameters>);
|
||||
static_assert(std::is_standard_layout_v<RenderStateParameters>); // offsetof legality
|
||||
static_assert(sizeof(RenderStateParameters) == 1168,
|
||||
"RenderStateParameters changed size; MGL_RESIDUAL_BLOCK_SIZE and the Espryt spans depend on it");
|
||||
static_assert(offsetof(RenderStateParameters, BlendStates) < offsetof(RenderStateParameters, LogicOp));
|
||||
static_assert(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)> == kMGMaxDrawBuffers);
|
||||
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
|
||||
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
|
||||
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
|
||||
#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
@@ -0,0 +1,183 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeCalls.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 single source of truth for the MGPipe call catalogue (plan B section 4.1 / 4.4 /
|
||||
// appendix A). One line per call; seven generators consume this file
|
||||
// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test
|
||||
// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic.
|
||||
//
|
||||
// X(Name, PayloadStruct, Class, Flags)
|
||||
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
|
||||
// kScreen lands in struct MGPipeScreen, every other class in struct
|
||||
// MGPipeContext (plan section 4.3).
|
||||
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
|
||||
// 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.
|
||||
//
|
||||
// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their
|
||||
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
|
||||
// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire
|
||||
// opcode is the 1-based position in this list, so reordering is a protocol break.
|
||||
//
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// COUNTS. MGP_CALL_LIST_DOCUMENTED_COUNT below is the authority; PipeCatalogueTest asserts
|
||||
// that the expansion, the two generated tables and this number agree.
|
||||
//
|
||||
// class entries group (as the plan tabulates it)
|
||||
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||
// appended server-side fence wait 1
|
||||
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
||||
// kCtxCso 13 CSO create/bind/delete
|
||||
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
|
||||
// kCtxObject 9 set_texture_params (the 17th set_*) + 8 object-scoped transfers
|
||||
// kCtxVerb 13 3 context-reading transfer calls + the 10 commands
|
||||
// total 71
|
||||
//
|
||||
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
||||
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
||||
// - "screen 14" tabulates the fence and query families together with the screen block.
|
||||
// Section 4.3 assigns the query NAMESPACE to the context ("VAO / FBO / XFB object /
|
||||
// query namespaces, the command stream, present"), so the six query calls carry
|
||||
// kCtxQuery and live in MGPipeContext. Screen keeps 10 of the plan's (11 with the appended
|
||||
// FenceWaitServer, below). The eight EGL lifecycle entry points stay virtual functions on
|
||||
// pActiveBackendObject and are deliberately NOT calls here (section 4.4.1, last row).
|
||||
// - "CSO 15" is create/bind/delete x 5 kinds. Two of those binds are ALSO named in the
|
||||
// set_* catalogue as their array forms - bind_sampler_states and set_sampler_views
|
||||
// (section 4.4.3) - and a call may only exist once, so they are emitted under
|
||||
// kCtxState and the CSO group holds 13: create/delete x 5 plus the three remaining
|
||||
// binds (render state, vertex elements, shader).
|
||||
// - "transfer 12" enumerates 11 calls in section 4.4.4 plus appendix A
|
||||
// (resource_subdata, buffer_subdata_resident, resource_flush_range, resource_readback,
|
||||
// resource_copy_region, blit, clear, generate_mipmap, read_pixels, get_texture_image,
|
||||
// resource_subdata_complete). Eleven is what is emitted; the twelfth is not named
|
||||
// anywhere in the plan.
|
||||
// - "about 74 items" in section 4.1 is the sum of those headline numbers, so it inherits
|
||||
// the same double counting. 68 unique records was the honest total of the plan's own
|
||||
// catalogue.
|
||||
// - Three LIVE GLFunctionsTable entries had no carrier in it at all: GetGpuTimestampNs
|
||||
// (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp
|
||||
// (glQueryCounter, a one-shot stamp rather than a begin/end pair) and WaitSync (the
|
||||
// GPU-side wait, which FenceWait's client-side wait does not express). They are
|
||||
// QueryTimestamp, QueryCounter and FenceWaitServer, APPENDED at the end of the list -
|
||||
// not slotted into their groups - because the wire opcode is the position, so a record
|
||||
// that arrives late goes last. 71 unique records.
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
#define MGP_CALL_LIST_DOCUMENTED_COUNT 71
|
||||
|
||||
// clang-format off
|
||||
#define MGP_CALL_LIST(X) \
|
||||
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
|
||||
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
|
||||
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
|
||||
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck) \
|
||||
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
|
||||
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
|
||||
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
|
||||
X(FenceCreate, MGPHandleOnly, kScreen, kNone) \
|
||||
X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot) \
|
||||
X(FenceWait, MGPFenceWait, kScreen, kReplySlot) \
|
||||
X(FenceDestroy, MGPHandleOnly, kScreen, kNone) \
|
||||
/* ---- context: query objects (plan 4.3 gives the namespace to the context) ---- */ \
|
||||
X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot) \
|
||||
X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot) \
|
||||
X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone) \
|
||||
/* ---- context: CSO create/bind/delete (plan 4.4.2, 4.5.2-4.5.5) ---- */ \
|
||||
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \
|
||||
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \
|
||||
X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
|
||||
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kNone) \
|
||||
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
|
||||
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob) \
|
||||
X(BindShaderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone) \
|
||||
/* ---- context: set_* (plan 4.4.3) ---- */ \
|
||||
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \
|
||||
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \
|
||||
X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail) \
|
||||
X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone) \
|
||||
X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone) \
|
||||
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \
|
||||
X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail) \
|
||||
X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail) \
|
||||
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \
|
||||
X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail) \
|
||||
X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob) \
|
||||
X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail) \
|
||||
X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone) \
|
||||
X(SetPatchState, MGPPatchState, kCtxState, kNone) \
|
||||
X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone) \
|
||||
X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone) \
|
||||
/* Migration-only carrier for Track V, retired field by field across P2..P13. Its */ \
|
||||
/* retirement is a compile error: MGL_RESIDUAL_BLOCK_SIZE only ever goes DOWN and the */ \
|
||||
/* final step asserts sizeof(ResidualValueBlock) == 0 (plan 6.3). */ \
|
||||
X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) \
|
||||
/* ---- context: per-object state and transfer (plan 4.4.3 set_texture_params, 4.4.4) ---- */ \
|
||||
X(SetTextureParams, MGPTextureParams, kCtxObject, kNone) \
|
||||
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
|
||||
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
|
||||
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
|
||||
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
|
||||
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
|
||||
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
|
||||
X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone) \
|
||||
X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot) \
|
||||
/* ---- context: transfer calls that read whole-context state, and the commands ---- */ \
|
||||
X(Blit, MGPBlit, kCtxVerb, kNone) \
|
||||
X(Clear, MGPClear, kCtxVerb, kNone) \
|
||||
X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot) \
|
||||
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \
|
||||
X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone) \
|
||||
X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone) \
|
||||
X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone) \
|
||||
X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone) \
|
||||
X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
|
||||
X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
|
||||
X(Flush, MGPFlush, kCtxVerb, kNone) \
|
||||
X(Present, MGPPresent, kCtxVerb, kNone) \
|
||||
X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) \
|
||||
/* ---- APPENDED. Opcodes are positional, so a late arrival goes at the END, never into ---- */ \
|
||||
/* ---- its group: three live GLFunctionsTable entries the catalogue had no carrier for. ---- */ \
|
||||
/* glGetInteger64v(GL_TIMESTAMP) - GetGpuTimestampNs, a synchronous server answer, which */ \
|
||||
/* the reply slot carries. The query namespace is the context's (plan 4.3). */ \
|
||||
X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot) \
|
||||
/* glQueryCounter(GL_TIMESTAMP) - QueryCounterTimestamp, a one-shot stamp into a query */ \
|
||||
/* object, NOT a begin/end pair. Kind carries GL_TIMESTAMP. */ \
|
||||
X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone) \
|
||||
/* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \
|
||||
/* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \
|
||||
X(FenceWaitServer, MGPFenceWait, kScreen, kNone)
|
||||
// clang-format on
|
||||
|
||||
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
||||
// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry -
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each,
|
||||
// the only indexed pnames the device rather than the frontend answers - live in MGPCaps
|
||||
// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled
|
||||
// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from
|
||||
// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names
|
||||
// frontend state and is answered before any table is consulted.
|
||||
// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact
|
||||
// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not
|
||||
// a backend answer at all; nothing a backend knows about a program crosses this way.
|
||||
// - ShaderStorageBlockBinding (folded into MGPProgramDesc's reflection archive),
|
||||
// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a
|
||||
// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views
|
||||
// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3).
|
||||
@@ -0,0 +1,356 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeFields.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
|
||||
|
||||
// Field lists for the G4 shadow comparator (plan B section 10.3-2). One macro per payload
|
||||
// in MGPipeTypes.h, listing the fields that carry MEANING - padding is deliberately absent,
|
||||
// because MOBILEGL_PIPE_VERIFY has to have ZERO false positives and a padding byte is
|
||||
// exactly what makes a memcmp of RenderStateParameters false-DIFFER
|
||||
// (DirectGLES.cpp documents that behaviour where it does the same comparison itself).
|
||||
//
|
||||
// Hand maintained alongside MGPipeTypes.h, MGPipeValueTypes.h, MGPipeHostSpan.h and
|
||||
// MG_Backend/BackendObject.h. Adding a member to one of these structs without adding it here
|
||||
// would make the comparator blind to it, so gen_pipe.py asserts - in both modes, hence in
|
||||
// pipe-gates - that every list below names exactly the direct data members of its struct
|
||||
// (P1 brief D8; a member named Pad<n> is padding and is not listed).
|
||||
//
|
||||
// clang-format off
|
||||
|
||||
#define MGP_FIELDS_MGPBlobRef(F) \
|
||||
F(Offset) F(Size) F(Seg)
|
||||
|
||||
#define MGP_FIELDS_MGPRange(F) \
|
||||
F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPBox(F) \
|
||||
F(X) F(Y) F(Z) F(W) F(H) F(D)
|
||||
|
||||
#define MGP_FIELDS_MGPReplySlot(F) \
|
||||
F(Id)
|
||||
|
||||
#define MGP_FIELDS_MGPStateChunk(F) \
|
||||
F(Offset) F(Length)
|
||||
|
||||
#define MGP_FIELDS_MGPHandleOnly(F) \
|
||||
F(Handle) F(Kind)
|
||||
|
||||
#define MGP_FIELDS_MGPCaps(F) \
|
||||
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
|
||||
|
||||
#define MGP_FIELDS_MGPResourceDesc(F) \
|
||||
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(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
|
||||
F(BufOffset) F(BufSize)
|
||||
|
||||
#define MGP_FIELDS_MGPFenceWait(F) \
|
||||
F(Fence) F(TimeoutNs)
|
||||
|
||||
#define MGP_FIELDS_MGPQueryDesc(F) \
|
||||
F(Query) F(Kind) F(Stream)
|
||||
|
||||
#define MGP_FIELDS_MGPQueryResultRequest(F) \
|
||||
F(Query) F(Wait)
|
||||
|
||||
#define MGP_FIELDS_MGPTimestampRequest(F) \
|
||||
F(Reserved)
|
||||
|
||||
#define MGP_FIELDS_MGPRenderStateDesc(F) \
|
||||
F(Cso) F(BaseCso) F(ChunkMask) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPBindRenderState(F) \
|
||||
F(Cso) F(Version) F(PipelineVersion)
|
||||
|
||||
#define MGP_FIELDS_MGPDynamicState(F) \
|
||||
F(ChunkMask) F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexElements(F) \
|
||||
F(Cso) F(AttributeCount) F(BindingPointCount) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerDesc(F) \
|
||||
F(Cso) F(Parameters)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerView(F) \
|
||||
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
||||
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) \
|
||||
F(Res) F(BuiltinSampler) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) \
|
||||
F(SamplerResync) F(MinLod) F(MaxLod) F(LodBias)
|
||||
|
||||
#define MGP_FIELDS_MGPProgramDesc(F) \
|
||||
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
||||
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) \
|
||||
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) \
|
||||
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(Target) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
||||
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexBuffers(F) \
|
||||
F(Start) F(Count) F(BaseInstance) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPIndexBuffer(F) \
|
||||
F(Res) F(Offset) F(IndexSize)
|
||||
|
||||
#define MGP_FIELDS_MGPIndirectBuffers(F) \
|
||||
F(DrawIndirect) F(Parameter)
|
||||
|
||||
#define MGP_FIELDS_MGPBoundView(F) \
|
||||
F(View) F(Texture) F(Unit)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerViews(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPSamplerStates(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPImageView(F) \
|
||||
F(Res) F(Unit) F(InternalFormat) F(Layer) F(Level) F(Layered) F(Access)
|
||||
|
||||
#define MGP_FIELDS_MGPShaderImages(F) \
|
||||
F(Start) F(Count) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPBufferRange(F) \
|
||||
F(Res) F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPShaderBuffers(F) \
|
||||
F(Class) F(Start) F(Count) F(WritableMask) F(HostSpanCount) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputTargets(F) \
|
||||
F(Count) F(Generation) F(ContentHash)
|
||||
|
||||
#define MGP_FIELDS_MGPGlobalConstants(F) \
|
||||
F(ShaderCso) F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPAttribValue(F) \
|
||||
F(Location) F(ValueClass) F(Data)
|
||||
|
||||
#define MGP_FIELDS_MGPVertexAttribDefaults(F) \
|
||||
F(Mask) F(Count)
|
||||
|
||||
#define MGP_FIELDS_MGPPixelPackState(F) \
|
||||
F(Pack)
|
||||
|
||||
#define MGP_FIELDS_MGPPatchState(F) \
|
||||
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) \
|
||||
F(CapabilityBits)
|
||||
|
||||
#define MGP_FIELDS_MGPResidualValueState(F) \
|
||||
F(Version) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSubRegion(F) \
|
||||
F(X) F(Y) F(Z) F(W) F(H) F(D) F(SrcOffset) F(SrcRowStride) F(SrcSliceStride)
|
||||
|
||||
#define MGP_FIELDS_MGPSubData(F) \
|
||||
F(Res) F(Target) F(Level) F(SourceIsVerbatimLevelShadow) F(UnionBox) F(RegionCount) F(Blob)
|
||||
|
||||
#define MGP_FIELDS_MGPSubDataComplete(F) \
|
||||
F(Res) F(Target) F(FirstLevel) F(LevelCount) F(PullSerial)
|
||||
|
||||
#define MGP_FIELDS_MGPFlushRange(F) \
|
||||
F(Res) F(Offset) F(Size) F(AccessFlags)
|
||||
|
||||
#define MGP_FIELDS_MGPReadback(F) \
|
||||
F(Res) F(Offset) F(Size)
|
||||
|
||||
#define MGP_FIELDS_MGPCopyRegion(F) \
|
||||
F(Src) F(Dst) F(SrcBox) F(DstX) F(DstY) F(DstZ) F(SrcTarget) F(DstTarget) F(SrcLevel) F(DstLevel)
|
||||
|
||||
#define MGP_FIELDS_MGPBlit(F) \
|
||||
F(ReadFbo) F(DrawFbo) F(SrcX0) F(SrcY0) F(SrcX1) F(SrcY1) F(DstX0) F(DstY0) F(DstX1) F(DstY1) \
|
||||
F(Mask) F(Filter)
|
||||
|
||||
#define MGP_FIELDS_MGPClear(F) \
|
||||
F(Fbo) F(Kind) F(DrawBufferIndex) F(BufferMask) F(ValueClass) F(ColorValue) F(DepthValue) \
|
||||
F(StencilValue)
|
||||
|
||||
#define MGP_FIELDS_MGPMipPlan(F) \
|
||||
F(Res) F(Target) F(BaseLevel) F(LevelCount)
|
||||
|
||||
#define MGP_FIELDS_MGPReadbackInfo(F) \
|
||||
F(Res) F(Box) F(Format) F(Type) F(Target) F(Level) F(DstOffset) F(DstSize)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawInfo(F) \
|
||||
F(Mode) F(IndexSize) F(Flags) F(InstanceCount) F(StartInstance) F(RestartIndex) F(DrawIdOffset) \
|
||||
F(IndexResource) F(MinIndex) F(MaxIndex) F(XfbCpuCapturedVertices) F(NumDraws)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawRange(F) \
|
||||
F(Start) F(Count) F(IndexBias)
|
||||
|
||||
#define MGP_FIELDS_MGPDrawIndirect(F) \
|
||||
F(Buffer) F(ParameterBuffer) F(Offset) F(ParameterOffset) F(Stride) F(DrawCount)
|
||||
|
||||
#define MGP_FIELDS_MGPGridInfo(F) \
|
||||
F(GridX) F(GridY) F(GridZ) F(BlockX) F(BlockY) F(BlockZ) F(IndirectBuffer) F(IndirectOffset) \
|
||||
F(IsIndirect)
|
||||
|
||||
#define MGP_FIELDS_MGPMemoryBarrier(F) \
|
||||
F(Bits) F(ByRegion)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputBegin(F) \
|
||||
F(PrimitiveMode)
|
||||
|
||||
#define MGP_FIELDS_MGPXfbAccounting(F) \
|
||||
F(CapturedVertices) F(PrimitivesWritten) F(PrimitiveMode)
|
||||
|
||||
#define MGP_FIELDS_MGPStreamOutputControl(F) \
|
||||
F(Reserved)
|
||||
|
||||
#define MGP_FIELDS_MGPFlush(F) \
|
||||
F(Flags)
|
||||
|
||||
#define MGP_FIELDS_MGPPresent(F) \
|
||||
F(FrameSerial)
|
||||
|
||||
#define MGP_FIELDS_MGPSwapInterval(F) \
|
||||
F(Interval)
|
||||
|
||||
#define MGP_FIELDS_MGPSurfaceInfo(F) \
|
||||
F(Width) F(Height) F(InternalFormat) F(Samples) F(Layers) F(IsDefault)
|
||||
|
||||
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
|
||||
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
|
||||
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
|
||||
// gone (a struct without a list is a compile error), and gen_pipe.py asserts every list names
|
||||
// every direct data member of its struct - Pad-named members are padding and excluded - so a
|
||||
// member added to RenderStateParameters without a row here fails pipe-gates.
|
||||
|
||||
#define MGP_FIELDS_RenderStateParameters(F) \
|
||||
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
|
||||
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
|
||||
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
|
||||
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(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
|
||||
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
|
||||
F(TextureCompressionHint) F(FragmentShaderDerivativeHint) F(PointFadeThresholdSize) \
|
||||
F(PointSpriteCoordOrigin) F(ClampReadColor) F(PolygonModeFront) F(PolygonModeBack) \
|
||||
F(PrimitiveRestartIndex) F(ColorLogicOpEnabled) F(DebugOutputEnabled) \
|
||||
F(DebugOutputSynchronousEnabled) F(DitherEnabled) F(LineSmoothEnabled) F(MultisampleEnabled) \
|
||||
F(PolygonOffsetFillEnabled) F(PolygonOffsetLineEnabled) F(PolygonOffsetPointEnabled) \
|
||||
F(PolygonSmoothEnabled) F(PrimitiveRestartEnabled) F(PrimitiveRestartFixedIndexEnabled) \
|
||||
F(RasterizerDiscardEnabled) F(SampleAlphaToCoverageEnabled) F(SampleAlphaToOneEnabled) \
|
||||
F(SampleCoverageEnabled) F(SampleMaskEnabled) F(SampleShadingEnabled) F(StencilTestEnabled) \
|
||||
F(ProgramPointSizeEnabled) F(ScissorTestEnabledMask) F(ScissorBoxes) F(ScissorBoxWrittenMask) \
|
||||
F(ClipDistanceEnabledMask)
|
||||
|
||||
#define MGP_FIELDS_PixelStoreParameters(F) \
|
||||
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
||||
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) \
|
||||
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
||||
F(AlphaEquation)
|
||||
|
||||
#define MGP_FIELDS_StencilFaceState(F) \
|
||||
F(Func) F(Ref) F(ValueMask) F(WriteMask) F(FailOp) F(PassDepthFailOp) F(PassDepthPassOp)
|
||||
|
||||
#define MGP_FIELDS_DynamicBackendParameters(F) \
|
||||
F(UniformBufferOffsetAlignment) F(ShaderStorageBufferOffsetAlignment) F(MaxTextureMaxAnisotropy) \
|
||||
F(AliasedLineWidthRangeMin) F(AliasedLineWidthRangeMax) F(SmoothLineWidthRangeMin) \
|
||||
F(SmoothLineWidthRangeMax) F(SmoothLineWidthGranularity) F(PointSizeRangeMin) \
|
||||
F(PointSizeRangeMax) F(PointSizeGranularity) F(Max3DTextureSize) F(MaxArrayTextureLayers) \
|
||||
F(MaxCubeMapTextureSize) F(MaxFramebufferWidth) F(MaxFramebufferHeight) F(MaxFramebufferLayers) \
|
||||
F(MaxRenderbufferSize) F(MaxTextureSize) F(MaxColorTextureSamples) F(MaxDepthTextureSamples) \
|
||||
F(MaxFramebufferSamples) F(MaxIntegerSamples) F(MaxSamples) F(MaxSampleMaskWords) \
|
||||
F(MaxPatchVertices) F(MaxTessGenLevel) F(MinProgramTextureGatherOffset) \
|
||||
F(MaxProgramTextureGatherOffset) F(MaxTextureImageUnits) F(MaxVertexTextureImageUnits) \
|
||||
F(MaxComputeTextureImageUnits) F(MaxCombinedTextureImageUnits) F(MaxVertexAttribs) \
|
||||
F(MaxComputeShaderStorageBlocks) F(MaxCombinedShaderStorageBlocks) \
|
||||
F(MaxVertexShaderStorageBlocks) F(MaxTessControlShaderStorageBlocks) \
|
||||
F(MaxTessEvaluationShaderStorageBlocks) F(MaxGeometryShaderStorageBlocks) \
|
||||
F(MaxFragmentShaderStorageBlocks) F(MaxComputeUniformBlocks) F(MaxComputeWorkGroupInvocations) \
|
||||
F(MaxComputeWorkGroupCount) F(MaxComputeWorkGroupSize) F(MaxShaderStorageBufferBindings) \
|
||||
F(MaxTextureBufferSize) F(TextureBufferOffsetAlignment) F(MaxUniformBufferBindings) \
|
||||
F(MaxUniformBlockSize) F(MaxImageUnits) F(MaxCombinedImageUniforms) F(MaxVertexImageUniforms) \
|
||||
F(MaxGeometryImageUniforms) F(MaxFragmentImageUniforms) F(MaxComputeImageUniforms) \
|
||||
F(MaxDrawBuffers) F(MaxColorAttachments) F(MaxClipDistances) F(MaxCullDistances) \
|
||||
F(MaxCombinedClipAndCullDistances) F(MaxViewports) F(LayerProvokingVertex) \
|
||||
F(ViewportIndexProvokingVertex) F(MaxViewportWidth) F(MaxViewportHeight) \
|
||||
F(ViewportBoundsRangeMin) F(ViewportBoundsRangeMax) F(ViewportSubpixelBits) \
|
||||
F(MinFragmentInterpolationOffset) F(MaxFragmentInterpolationOffset) \
|
||||
F(FragmentInterpolationOffsetBits) F(SupportsWideLines) \
|
||||
F(SupportsDistinctDepthStencilAttachments) F(PerLayerFramebufferAttachmentTargets) \
|
||||
F(SupportsShaderFloat64) F(SupportsFloat64VertexAttributes) F(SupportsTessellationPointSize) \
|
||||
F(SupportsGeometryPointSize) F(MaxShaderStorageBlockSize) F(SubgroupSize) \
|
||||
F(SubgroupSupportedStages) F(SubgroupSupportedFeatures) F(SubgroupQuadOperationsInAllStages) \
|
||||
F(GpuVendor)
|
||||
|
||||
#define MGP_FIELDS_MGHostSpan(F) \
|
||||
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
|
||||
// macros; gen_pipe.py reads THIS list to know what to emit.
|
||||
#define MGP_VERIFY_PAYLOAD_LIST(P) \
|
||||
P(MGPBlobRef) P(MGPRange) P(MGPBox) P(MGPReplySlot) P(MGPStateChunk) P(MGPHandleOnly) P(MGPCaps) \
|
||||
P(MGPResourceDesc) P(MGPFenceWait) P(MGPQueryDesc) P(MGPQueryResultRequest) P(MGPTimestampRequest) P(MGPRenderStateDesc) \
|
||||
P(MGPBindRenderState) P(MGPDynamicState) P(MGPVertexElements) P(MGPSamplerDesc) P(MGPSamplerView) \
|
||||
P(MGPTextureParams) P(MGPProgramDesc) P(MGPSurface) P(MGPFramebufferState) P(MGPVertexBuffer) \
|
||||
P(MGPVertexBuffers) P(MGPIndexBuffer) P(MGPIndirectBuffers) P(MGPBoundView) P(MGPSamplerViews) \
|
||||
P(MGPSamplerStates) P(MGPImageView) P(MGPShaderImages) P(MGPBufferRange) P(MGPShaderBuffers) \
|
||||
P(MGPStreamOutputTargets) P(MGPGlobalConstants) P(MGPAttribValue) P(MGPVertexAttribDefaults) \
|
||||
P(MGPPixelPackState) P(MGPPatchState) P(ResidualValueBlock) P(MGPResidualValueState) \
|
||||
P(MGPSubRegion) P(MGPSubData) P(MGPSubDataComplete) P(MGPFlushRange) P(MGPReadback) \
|
||||
P(MGPCopyRegion) P(MGPBlit) P(MGPClear) P(MGPMipPlan) P(MGPReadbackInfo) P(MGPDrawInfo) \
|
||||
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
||||
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
||||
P(MGPSurfaceInfo) \
|
||||
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||
P(StencilFaceState) \
|
||||
P(DynamicBackendParameters) P(MGHostSpan) \
|
||||
P(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
|
||||
|
||||
// clang-format on
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user