Compare commits

...
32 Commits
Author SHA1 Message Date
swung0x48 6672778b80 [Merge] (dev): merge dev@50fb1343 into feat/disaggregated
- brings the write-map landing into GPU-resident stores, the open-ended fp64 storage block flattening, the idle-based CTS chunk timeout and the MSVC /WHOLEARCHIVE test link
- verified on the merged tree: unit 1458, integration 868, Wire 54, retrace 79/79
2026-09-05 22:13:21 -04:00
swung0x48 6e0e3df372 [Docs] (Disaggregated): point the history note at commit hashes only
- the retired drafts stay in git history; the README no longer names or characterises them
2026-09-05 21:58:28 -04:00
swung0x48 bee22f9d26 [Docs] (Disaggregated): separate the dirty-surface totals from the immediate-publish subset in MEASUREMENTS
- the sentence read as if 836 of the 92 immediate calls were RecordError; 836 is RecordError's share of all 926 calls, and most of the 92 immediate ones are RecordError
2026-09-05 21:57:11 -04:00
swung0x48 8952b14024 [Docs] (Disaggregated): rewrite the MGPipe plan into a design and architecture set - README, ARCHITECTURE, ROADMAP, MEASUREMENTS - and retire PLAN.md and REVIEW.md to git history
- README.md: what MGPipe is, the one-paragraph architecture, the P0 status line, the file and code map, and the commit range where the design competition and the three adversarial review rounds live
- ARCHITECTURE.md: the design as decided, one reason per decision - handles and generations, the 71-call catalogue by class with flags and cap bits, record and payload conventions, the tracker, texture subdata and dirty ownership, shader state and the P0.5 header extraction, the reverse channel, the backend strangler, the server side and the index host mirror, the transport as landed in MG_Remote, the persistent-map tiers as measured, roundtrips, present and threads, process and platform delivery, build shapes and purity gates, the five-part verification gate, and the knob tables marked landed versus planned
- ROADMAP.md: P0..P13 as one table of what lands, the gate and the dependency, the two tracks and milestones, the day-43 GO/NO-GO checklist with both exits, the re-baseline checkpoints, and the questions still open after P0
- MEASUREMENTS.md: spike A on both devices, the spike B tier matrix, the four-trace boundary-counter baselines on both backends, the desktop and corpus facts, and the harness traps with the exact commands
- every file:line kept is verified at 458ccde1 and the citation lint is clean; everything else cites a symbol plus a file
- dropped on purpose: the v1/v2 revision archaeology, rejected alternatives, per-subsystem day estimates, the Feat/CS-Delta-IPC reuse audit and the reviewer back-and-forth
2026-09-05 21:56:31 -04:00
swung0x48 458ccde176 [Feat] (Metrics, Config): make the boundary-counter summary cadence a knob, because the device harness never reaches the teardown dump
- MOBILEGL_PIPE_STATS_PERIOD (default 120, clamped to [1, 1000000]) sets the frames per
  summary line; Init() latches it and a zero falls back to the default
- the trace APK's replay never tears MobileGL down, so MOBILEGL_PIPE_STATS_FILE never
  fires on device and a fixture shorter than the period (create-indirect) reported nothing
- PipeStatsTest pins the latch and the zero fallback
2026-09-05 21:21:54 -04:00
swung0x48 901d48a678 [Fix] (MGPipe, Metrics, Config, CI): exchange the per-frame stats instead of racing a store, carry the three uncarried table entries, drop the inline host span from a buffer range, spell the buffer subdata range, and close the small gate holes
- S-1 PipeStats::OnPresent read each frame accumulator and then store(0)'d it; a Bump from a staging thread landing in between was lost from the Tracy plot and from every frame. Each accumulator is now exchange(0, relaxed) and the exchanged value is what is plotted, so every add lands in exactly one frame.
- T-3 FdPassing without MSG_CMSG_CLOEXEC (macOS, BSD) handed back descriptors that survived exec; every received fd now gets FD_CLOEXEC by hand under !MSG_CMSG_CLOEXEC. MSG_NOSIGNAL is defined to 0 where the platform lacks it (FdPassing.cpp, Doorbell.cpp) and SO_NOSIGPIPE is set on the socketpair and on a SocketDoorbell's descriptor where it exists, so a write to a hung-up peer is EPIPE rather than a fatal signal.
- T-4 the missing-flatbuffers fallback wrote OFF into the cache with FORCE, so a plain re-configure after `git submodule update` stayed OFF silently. It is a normal-variable set now, shadowing the cache for that configure only; verified by hiding flatbuffers.h, configuring with ON (warning, transport off, cache still ON) and re-configuring plainly with the header back (transport ON).
- P-2 three LIVE GLFunctionsTable entries had no carrier: GetGpuTimestampNs (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp (glQueryCounter, a one-shot stamp, not a begin/end pair) and WaitSync (the GPU-side wait FenceWait's client wait does not express). QueryTimestamp (MGPTimestampRequest, kCtxQuery, kReplySlot), QueryCounter (MGPQueryDesc with Kind = GL_TIMESTAMP, kCtxQuery) and FenceWaitServer (MGPFenceWait, kScreen) are APPENDED at the end of PipeCalls.def because the opcode is the position: SetSwapInterval stays 68, the three take 69-71, and PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering pins that. Header counts 71 (screen 11, query 8); the seven generators regenerated.
- P-3 MGPBufferRange inlined a 32-byte MGHostSpan into every range of every class - dead space on every SSBO, atomic-counter and XFB range, and D-B8 says not to freeze the named-UBO payload before the stage-ubo-named numbers exist. The range is 24 bytes now; the host spans are an optional second var-tail behind the ranges, announced by MGPShaderBuffers::HostSpanCount (0 or Count), with set_shader_buffers keeping its kVarTail|kHostSpan flags. PipeCatalogue.BufferRangeCarriesNoInlineHostSpan pins the sizes, the flags and the comparator's view of the count.
- P-4 QueryEnvUint64 parsed with base 0 (a leading zero meant octal: MOBILEGL_PIPE_PUSH=010 read as 8) and accepted -1 as every bit set; it is decimal or explicit 0x now and a '-' anywhere is refused with the warning (smoke through the integration binary: -1 and 12abc warn, 010 and 0x10 parse). The CI stdio gate's alternation now also catches fprintf(stdout, puts( and std::cout/cerr; it is green over MG_Backend and MG_State. MGPSubData states how the buffer half expresses [offset, size): UnionBox.X / UnionBox.W with Target == Buffer, Y = Z = 0, H = D = 1, one record bounded at a 2^31-1 offset and 2^32-1 size beyond which the emitter splits (the same rule the ring's half-capacity bound already imposes); MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size are the only spelling and PipeCatalogue.SubDataBufferRangeRidesInTheUnionBox pins the encoding and its bounds. gen_pipe.py now refuses, in both modes, a call payload named in PipeCalls.def with no field list in PipeFields.def (the four memcmp-fallback member types are the documented exception); shown by dropping P(MGPSwapInterval), which exits 1 naming the payload.
- The MGPPixelPackState size assertion compared sizeof against itself; it asserts the literal 28 PixelStoreParameters measures.
- Verified: ctest -L unit green in both the default and the split configuration, gen_pipe.py --check clean with the generated files committed, nm --defined-only of the default libMobileGL.so has no MG_Remote symbol, and the full integration-gpu suite passes (the *IsActuallyArmedWhenTheEnvironmentPinsItOn family trips under -j 8 as documented and passes serially).
2026-09-05 21:19:00 -04:00
swung0x48 e8ee7b1a88 [Feat] (Backend, MGPipe): carry the six per-axis compute limits in DynamicBackendParameters so MGPCaps has every backend-owned indexed answer, and pin them against glGetIntegeri_v on both backends
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself.
- The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs.
- PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct.
- AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers.
- Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green.
2026-09-05 21:19:00 -04:00
swung0x48 1154f9a00d [Fix] (MG_Remote, Transport): give the inproc doorbell a death state so Shutdown can join a parked waiter, and bound a ring record at half the capacity so a refusal can never look like backpressure
- T-1: InProcessChannel::Close rang each CondVarDoorbell once and claimed that unparks a peer mid-frame. It does not. Doorbell::Wait consumes the one ring, re-tests a condition nothing published, finds the bell alive (CondVarDoorbell never overrode Dead(); Doorbell.cpp had no death state at all) and with kWaitForever parks again for good - so Shutdown could never join a server thread sitting in the design's own steady state (spun, set consumerParked, blocked; plan section 8.1 inheriting the earlier plan's 6.2a). CondVarDoorbell now carries an atomic death latch: Kill() sets it under the mutex and notify_all's, Dead() reports it, Park returns false at once on a dead bell (and the wait predicate includes it, so a Kill cannot slip between the test and the wait), and Close kills both bells instead of ringing them. Same shape as SocketDoorbell's EOF latch; Notify stays the ordinary wakeup.
- T-2: RingProducer::Reserve refused only total > capacity, but a record with capacity/2 < total <= capacity is unplaceable at every head offset where neither the space to the wrap boundary nor the space before it holds it - even in an EMPTY ring, because a wrap pad costs spaceToEnd bytes on top of the record. Concretely: head offset 16 of an empty 256-byte ring, a 248-byte record; FreeBytes() says 256, Reserve says nullptr, forever, and a producer waiting for FreeBytes() >= 248 stalls with nothing logged. The bound is now capacity/2, which is exact rather than conservative (worst case 2*total-8 <= capacity-8), exposed as MaxRecordBytes() for the emitter to chunk against; the minimum ring is two headers so the smallest record still fits the bound. Ring.h states Capacity()/2 as the chunking bound and the G3 header comment in gen_pipe.py now states the chunking rule plan section 8.2 asks G3 to define (PipeWire.inc regenerated).
- Tests, each shown red with only the fix site reverted and green with it: InProcessTransportTest.ShutdownUnparksAWaiterWithNoDeadline (bounded join through a shared_ptr-owned waiter: 5 s red instead of a hung job; reverted it hangs and fails at 5051 ms), RingTest.RecordLargerThanHalfTheRingIsRefused (reverted, the 248-byte record is accepted), RingTest.RecordPlaceabilityDoesNotDependOnTheHeadOffset (the offset-0 vs offset-16 negative control), RingTest.HalfCapacityRecordFitsAtEveryHeadOffset (the positive half: the maximal record at all 32 head offsets of a 256-byte ring) and RingTest.RejectsARingTooSmallForTheSmallestRecord.
2026-09-05 21:19:00 -04:00
swung0x48 7ef7c7e543 [Fix] (Spikes): answer the tier question for DirectGLES too, make an OK mean bytes round-tripped through a real GPU access, and exercise T3 in the direction that makes it a tier
- plan-B §8.3 asks which tier `AcquirePersistentMap` lands in, but the probe only
  asked Vulkan. DirectGLES ("Espryt") reaches a persistent map through
  `glBufferStorageEXT` + `glMapBufferRange(PERSISTENT|COHERENT)`, not a
  `VkDeviceMemory` map, so a Vulkan-only answer decides nothing for that backend.
  Add a GLES leg to T1: the exported fd imported with `glCreateMemoryObjectsEXT`
  + `glImportMemoryFdEXT` + `glBufferStorageMemEXT`, then mapped
  PERSISTENT|COHERENT -- in-process first (isolates "GL can import this fd" from
  "the fd survives a process boundary"), then cross-process (new `t1gl` child).
  Drivers disagree about how the import must be phrased, so each attempt walks a
  ladder over {dedicated flag} x {import size = memory requirement or the fd's
  own size} x {buffer size} and reports the rung the driver accepted plus every
  rejected rung with its GL error -- a driver *preference* must never be reported
  as a missing capability. A driver that backs the storage but refuses
  PERSISTENT|COHERENT is reported separately from one that refuses the storage:
  that distinction is exactly T1 vs T2 for DirectGLES. The T0 GLES leg
  (`EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT` +
  persistent map, verified by `AHardwareBuffer_lock` on the client side) now
  reports every step's GL enum and requires the persistent flags for OK.
- the verdict was unfalsifiable: T1 reported PARTIAL when neither leg had moved a
  byte. Replace it with an explicit decisive-leg model -- OK only when every
  decisive leg round-tripped in both directions, PARTIAL when at least one did,
  FAIL otherwise with the failing step and its driver error named in `why:`.
  Every row now opens with a per-leg trace (`vkimport[D]=rt gpu[D]=rt`). The raw
  `mmap` leg is informational for opaque-fd (Vulkan forbids interpreting that
  payload outside the driver, so a refusal is conformant) and decisive for
  dma-buf, where a CPU mapping is the point of the handle type.
- T3 never ran the direction that would make it a tier: both ends were the
  importing process. Add `T3-client-memfd-server-import` (new `t3c` child) -- the
  client creates and writes the memfd, the server mmaps the received fd, imports
  the client's host pointer into a `VkDeviceMemory`, reads what the client wrote,
  writes back, and takes a GPU access on the client's memory, which the client
  then verifies through its own mapping.
- no route touched the GPU, so an OK proved only that a map call returned a
  pointer. Every tier row now takes a real GPU access before it can be OK:
  `vkCmdCopyBuffer` out of the shared allocation into private staging (mismatch =
  the GPU could not read what the peer wrote) plus `vkCmdFillBuffer` into it,
  queue-idle and an explicit host-read barrier, with the peer checking the filled
  region through its own mapping. VkCtx grows a queue and command pool for it.
- the device run executes in the `shell` SELinux domain, not the `untrusted_app`
  domain MobileGL runs in, and the two do not share dmabuf/gralloc rules. Print
  uid/pid/`/proc/self/attr/current` in a run-context header, repeat the caveat in
  the summary, and document in README.md how to answer it for the real domain
  later (exec the same binary from the trace app's spike hook, spike-A package)
  without implementing that here.
- `vkStr()` returned a pointer into one static buffer while several results
  routinely appear in one format call, so all of them showed the last one; it
  returns std::string now, `memFlagStr` likewise, and `fmt`/`pr` carry
  `format(printf)` so a missed `.c_str()` is a compile error rather than UB.
- `advertisedExportable` decided the status at the allocate site but not at the
  `vkGetMemoryFdKHR` site. One rule at every export failure now
  (`exportFailStatus`): advertised EXPORTABLE and then declining is FAIL, never
  advertised is UNSUPPORTED. Export + map + fd is factored into `exportHostVisible`.
- `T0-ahb-blob-transfer` was recorded OK on the socket handoff alone. The handoff
  keeps its own informational row; the tier row is now composed at the end from
  the full import+map+compare+writeback chain over the Vulkan, GL and GPU legs.
- `mmapErrno` kept the first attempt's errno after the second-chance mmap
  succeeded, so a working mapping carried a failure code; it is cleared on
  success and the first errno moves into the note.
- a failed `glImportMemoryFdEXT` no longer closes the fd: EXT_memory_object_fd
  does not say whether ownership still transfers on failure and Mesa closes it
  either way, so closing risks a double close landing on the socket. Leaking a
  handful of dups in a short-lived probe is the safe side of that trade.
- validated end to end on the host harness (lavapipe + llvmpipe,
  `VK_DRIVER_FILES=lvp_icd.json EGL_PLATFORM=surfaceless`): T1-opaque-fd OK,
  T3-external-memory-host OK, T3-memfd-cross-process OK,
  T3-client-memfd-server-import OK, T1-dma-buf UNSUPPORTED (not advertised
  exportable). The two T1-gles rows FAIL there with GL_OUT_OF_MEMORY on every
  ladder rung although GL_DEVICE_UUID_EXT matches the Vulkan deviceUUID --
  llvmpipe's GL does not implement importing a lavapipe opaque-fd allocation,
  a Mesa interop gap recorded in README.md so a device FAIL stays attributable.
  Rebuilt for arm64-v8a with NDK r27d (PIE, android-30); the device run is
  pending, both device locks are held by another campaign.
2026-09-05 20:50:05 -04:00
swung0x48 6c7ad0a1bf [Feat] (Spikes): add the standalone external-memory probe that decides the persistent-map tier
- Plan B §11 P0 requires spike B ("external memory 导出,两台设备") to run before the
  persistent-map decision of §8.3 can be taken: T0 (server imports a client
  allocation), T1 (server exports its own HOST_VISIBLE|HOST_COHERENT allocation)
  or T2 (AcquirePersistentMap returns nullptr, making the §5.10 client-side block
  push mandatory). §8.3 says the answer must be measured on the two campaign
  devices, and that a platform unknown must not block the interface work.
- tools/spikes/extmem_probe/ is a self-contained NDK command-line program: it
  links vulkan/EGL/GLESv3/android/log and nothing from MobileGL, is configured by
  its own CMakeLists with the android toolchain file, and is deliberately absent
  from the project's build graph (the root CMakeLists only pulls in
  tools/trace_replay), so the default ALL target is untouched.
- Phase A enumerates VK_KHR_external_memory_fd, VK_EXT_external_memory_dma_buf,
  VK_EXT_external_memory_host, VK_ANDROID_external_memory_android_hardware_buffer
  and, through a headless EGL pbuffer context, GL_EXT_memory_object{,_fd},
  GL_EXT_external_buffer, GL_EXT_buffer_storage, GL_OES_EGL_image_external{,_essl3}
  and EGL_ANDROID_get_native_client_buffer, plus the memory-type table and the
  vkGetPhysicalDeviceExternalBufferProperties verdict per handle type for the exact
  buffer usage set MobileGL needs.
- Route T1 allocates a HOST_VISIBLE|HOST_COHERENT buffer memory with
  VkExportMemoryAllocateInfo, writes a pattern through vkMapMemory, exports an fd
  with vkGetMemoryFdKHR (opaque-fd and, where advertised, dma-buf), hands it to a
  second process over SCM_RIGHTS, and has that process both mmap() the fd and
  import it into its own VkDeviceMemory + vkMapMemory. Both sides write and both
  sides compare, so a copy-on-import or one-directional mapping is reported as
  PARTIAL rather than as success.
- Route T0 has the second process allocate an AHardwareBuffer BLOB
  (CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER), send it with
  AHardwareBuffer_sendHandleToUnixSocket, and the first process import it three
  ways -- AHardwareBuffer_lock, VkDeviceMemory via
  VK_ANDROID_external_memory_android_hardware_buffer, and a GL buffer via
  eglGetNativeClientBufferANDROID + glBufferStorageExternalEXT mapped
  persistent/coherent -- with a write-back leg the allocating process verifies.
- Route T3 covers VK_EXT_external_memory_host: a memfd-backed mmap region aligned
  to minImportedHostPointerAlignment, imported through
  VkImportMemoryHostPointerInfoEXT, plus the same memfd handed to another process.
- The second process is /proc/self/exe re-exec'd with --child=<route> and one end
  of a socketpair on fd 3. A bare fork() is not usable: neither side's Vulkan
  driver survives fork, and both sides need live Vulkan. It is also the topology
  the transport actually has (§8.1, inheriting PLAN.md §11.1-§11.6: the client
  spawns the server), so the probe measures the arrangement that would ship.
- The probe also builds for the host with T0 compiled out. That is not scope
  creep: a negative device result is only worth something if the harness is known
  to report a working route as working. Running it on lavapipe did that, and paid
  for itself immediately by exposing two harness bugs that would have produced
  false negatives on the devices -- (a) the child wrote through its plain mmap
  before reading through the Vulkan import, so on a driver whose exported fd maps
  at an offset the probe overwrote the very payload the second read compares
  (lavapipe reports payloadAt=4096); reads through both mappings now precede
  writes through either, and the offset is searched for and reported; (b) an
  export failure on a handle type the driver never advertised as EXPORTABLE was
  classified FAIL instead of UNSUPPORTED (lavapipe's dma-buf answer).
- Output is a RESULT/summary table carrying the raw driver verdicts (VkResult
  names, errno, GL enums) because those codes -- not a pass/fail bit -- are what
  §8.3 needs in order to pick the tier.
- Built with NDK 27.3.13750724 for arm64-v8a / android-30, RelWithDebInfo, PIE,
  warning-clean; host build clang/RelWithDebInfo, warning-clean.
2026-09-05 20:50:04 -04:00
swung0x48 38d4c2372c [Feat] (TraceApp, CI): pass arbitrary env vars through the retrace lane
- PLAN-B.md §8.2 and appendix B add a batch of new runtime switches
  (MOBILEGL_PIPE_PUSH / _VERIFY / _STATS / _LEGACY_MEMOS / _TEXEL_RETAIN_MB /
  _INDEX_MIRROR_MB, plus MOBILEGL_IPC_* later), and §11 P0 wants them parsed beside
  the existing ones. Today every knob that has to reach an Android replay costs an
  edit in five files - run_android_retrace_local.py, trace-replay-ci.sh,
  TraceReplayActivity's request record, the JNI marshalling, and the setenv block in
  trace_replay_core.cpp. That per-knob tax is what this replaces: one extra,
  `--es mobilegl_env "K=V;K=V"`, carries all of them.
- Applied last, immediately before dlopen(libMobileGL.so), so it can also override
  the dedicated fields above it - MobileGL's config is read during the load, and an
  escape hatch that cannot beat the defaults is not one. An entry with no '=' unsets
  the variable, which is the only way to clear a default the marshalling sets.
- The existing per-knob flags stay: they carry semantics beyond a setenv (use_angle
  also selects a variant, the dump lists are joined, DirectVulkan forces the
  R11G11B10F fallback), and rewriting them as env strings would move that logic into
  the callers.
- Surface: --env / MOBILEGL_TRACE_ENV in trace-replay-ci.sh, repeatable --env
  KEY=VALUE in run_android_retrace_local.py, `mobilegl_env` intent extra,
  Request::envOverrides.
- The two-level parse now lives in trace_env_overrides.hpp, beside the semicolon
  splitter it shares with the texture and FBO dump lists, and
  tools/trace_replay/trace_env_overrides_test.cpp pins it: the empty entries a
  trailing ';' leaves behind must not become unsetenv(""), `K=` must stay a Set of
  the empty string rather than an Unset (a knob read with getenv() != nullptr sees
  those as opposite answers), and only the FIRST '=' may separate, or a value
  carrying '=' is truncated without a word of warning. The whole MOBILEGL_PIPE_*
  batch rides on this parse, and the only lane that exercised it end to end was an
  on-device retrace, which would have reported a splitting bug as "the knob had no
  effect".
- The check is built and RUN at build time and mobilegl_trace_replay depends on it,
  so `cmake --build ... --target mobilegl_trace_replay` - the exact command of
  test.yml's "Build trace replay" job, which never invokes ctest - runs it. It is
  assert-free on purpose: that lane configures Release, and <cassert> under NDEBUG
  would compile every check into a green run that checked nothing. Negative control:
  swapping find('=') for rfind('=') fails 2 checks, and keeping the splitter's empty
  entries fails 2 more.
2026-09-05 20:49:57 -04:00
swung0x48 8a239177ac [Feat] (Build, TraceApp): ship and exec a second native binary on android
- PLAN-B.md §11 P0 lists spike A (the Android delivery chain) as a P0 deliverable,
  inherited verbatim from PLAN.md §15 P0; §8.1 inherits PLAN.md §11.1-§11.6, whose
  Android path needs a second process. Android gives an application no writable
  exec-able directory, so the only supported route is to name the binary lib*.so, let
  the packager put it in lib/<abi>/, and exec it out of
  getApplicationInfo().nativeLibraryDir. This builds that route end to end so the
  spike can be answered with evidence instead of folklore.
- New root option MOBILEGL_BUILD_SERVER_SPIKE (OFF, ANDROID-only) adds the
  MobileGLServer target from tools/spikes/server_stub/main.cpp with PREFIX "lib" /
  SUFFIX ".so" and -fPIE/-pie: an .so name does not exempt the file from Android's
  PIE requirement. Its RUNTIME_OUTPUT_DIRECTORY is pointed at
  CMAKE_LIBRARY_OUTPUT_DIRECTORY, because AGP packages what lands in the per-ABI
  library output directory and CMake would otherwise put an executable elsewhere.
- The option is opt-in on both sides. The plugin flavour cannot turn it on at all,
  and the trace flavour builds it only when asked, with
  `-Pmobilegl.buildServerSpike=ON` or MOBILEGL_BUILD_SERVER_SPIKE=ON in the
  environment; a flavour that silently carries an executable nothing loads is the
  kind of thing nobody notices until it ships. Verified both ways:
  assembleTraceDebug -Pmobilegl.buildServerSpike=ON packages
  lib/arm64-v8a/libMobileGLServer.so and `file` reports "ELF 64-bit LSB pie
  executable, ARM aarch64 ... interpreter /system/bin/linker64, for Android 26";
  the same task with no property packages only libMobileGL.so and
  libtrace_replay_runner.so.
- The stub prints one line to stdout and writes the same line to the file named by
  argv[1], then exits 0. The line carries pid/ppid/uid/gid and, decisively, the
  child's own /proc/self/attr/current: only `u:r:untrusted_app:...` proves an
  ordinary app process did the exec. An `adb run-as` shell runs in a different
  SELinux domain, so a success there would prove nothing.
- RunSpawnSpike() starts the stub with argv [serverPath, markerPath], redirects the
  child's stdout/stderr into a captured file (an app process has stdout on
  /dev/null, so a printed line would otherwise vanish), waits for it, and reports
  exit status, signal, the exec errno, the parent's own SELinux context, the marker
  content and the captured stdout - to logcat, to the returned string, and to a
  <marker>.report file, because the Activity finishes immediately afterwards.
- The child reports the errno of a REFUSED execve through a close-on-exec pipe.
  Without it the one datum the spike exists to produce is lost: the parent only ever
  sees a wait status, in which every reason has already been flattened into one exit
  code, and EACCES (SELinux, or a noexec mount) versus ENOEXEC (a packager that
  mangled the file) are opposite verdicts for the design. A successful exec closes
  the write end for free, so the parent reads EOF and reports execErrno=0.
- fork/execve only. The earlier draft also carried a posix_spawn arm behind
  `__ANDROID_API__ >= 28`, which was dead code in every configuration this repo can
  build - bionic declares posix_spawn from API 28 and the root CMakeLists.txt pins
  MOBILEGL_ANDROID_API_LEVEL to 26 and refuses to configure lower - and would have
  silently become the production path, untested, on a minSdk bump. Keeping the arm
  that actually ships means the spike measures the code the server would really use.
  Nothing happens between fork and execve except open/dup2/execve/write/_exit, all
  async-signal-safe, because the parent is a multi-threaded JVM process.
- The spike lives in its own TU, spawn_spike.cpp/.hpp, listed only by the trace
  APK's CMakeLists. Its sibling trace_replay_core.cpp is compiled verbatim by the
  DESKTOP mobilegl_trace_replay runner (tools/trace_replay/CMakeLists.txt names the
  same file), where <android/log.h> does not exist, so nothing Android-only may live
  there; spawn_spike.cpp carries an #error for anyone who adds it to that list.
- The Activity runs the spike, and nothing else, when launched with the
  `mobilegl_spike_spawn` intent extra; that mode needs no trace, no golden and no
  render surface. It is a separate JNI entry point rather than another parameter on
  the 30-argument replay call, which it shares nothing with.
- Not yet run on a device: both device locks are held by another campaign. The
  on-device verdict is the coordinator's step.
2026-09-05 20:49:57 -04:00
swung0x48 87ee17c68c [Docs] (Disaggregated): fold the P0 measurements and corrections into the plan
- GL_COMPUTE_WORK_GROUP_SIZE is answered by MG_Impl from ProgramObject::GetComputeLocalSize, not by a backend; only the compute limits are caps, and the two dead table entries (GetInteger64i_v, GetProgramiv) were retired in P0
- the glRenderbufferStorage OOM-probe idiom appears in 0 of 41 fixtures, so kNeedsAck is carried by glBufferStorage only
- FramebufferSrgb/DepthClamp: six readers of a constant false and zero readers respectively, no fixture enables either; recorded as a decision to take before the render-state chunk table freezes
- the call catalogue is 68 unique records (screen 10, ctx-query 6, CSO 13, kCtxState 17, kCtxObject 9, kCtxVerb 13); PipeCalls.def is the single source of truth and the wire opcode is a line's position
- measured layouts (MGPDrawInfo head 56 B, RenderStateParameters 1168 B, ResidualValueBlock 1248 B, MGPipeContext 464 B ...), the 926/73 MG_Impl mutator surface, the first per-draw accessor numbers on lavapipe (Espryt 20.65, Magma 15.54), the EndTransformFeedback null-as-capability trap, and the host-side spike results
2026-09-05 20:29:30 -04:00
swung0x48 aa005720d0 [Test] (MG_Remote, Wire): pin the hung-up doorbell, the wakeup that must not be eaten, the ring's capacity ceiling and the publish-then-ring handoff
- FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp builds a
  SOCK_STREAM socketpair - deliberately not FdPassing::CreateSocketPair's
  datagram pair, because only a stream end reports the hangup at all - closes
  the notifier, and asserts Park returns false, latches Dead(), stays latched,
  and that a Wait with kWaitForever gives up in under a second instead of
  spinning.

- FdPassingTest.SocketDoorbellStillDeliversTheLastRingBeforeAHangup rings and
  then closes: detecting death must not swallow the wakeup already sitting in
  the socket buffer, since the peer's last publish is the one a waiter is most
  likely to be blocked on.

- InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors blocks
  two readers on one endpoint with two different predicates and requires the
  frame to arrive within 2s rather than "eventually, when a receive timed out".

- RingTest.RejectsACapacityTheRecordHeaderCannotDescribe refuses 4 GiB from
  both roles without mapping anything (the constructor rejects before it
  touches the base pointer) and keeps 2 GiB accepted as the positive control.

- RingTest.DoorbellHandoffWakesBothSidesOnEveryPublish runs 2000 records
  through the ring with real parking in both directions, in the publish-then-
  NotifyIfParked order the fences assume. It cannot prove the fence pairing -
  no test can, since x86 has to actually hold the release store in the store
  buffer across the flag read - but it exercises the exact call order, and a
  lost wakeup surfaces as a Wait that times out with work available (a red
  test) rather than as a hung CI job.

- Result: 1429 unit tests pass with MOBILEGL_BUILD_DISAGGREGATED=ON (1424
  before this commit; the wire subset is 47), 1382 pass with it OFF, and the
  same three pre-existing skips appear in both. Every new case was verified to
  fail with its fix reverted and to pass again with it restored.
2026-09-05 20:16:50 -04:00
swung0x48 c1a7ffac94 [Fix] (MG_Remote, Transport): give descriptor offers their own condition variable, cap the ring at what a 32-bit size field can describe, and keep the frontend umbrella out of Framing.h
- InProcessChannel::Direction served two different predicates from one
  condition_variable signalled with notify_one, so a SendFrame wakeup could be
  delivered to a thread blocked in ReceiveFd, which re-tests its own predicate
  and goes back to sleep - leaving a queued message undelivered until some
  unrelated later event. ITransport narrows the contract to one dedicated
  reader thread, but that is a comment, not a mechanism, and the first caller
  that splits its reader should not have to discover this. Offers now have
  their own fdCv, and Close() notifies both.

- RingProducer/RingConsumer accepted any power-of-two capacity while
  RingRecordHeader::size is 32-bit by wire contract (plan section 8.1 ->
  PLAN.md section 6.3: 8-byte RecHeader). At 4 GiB or more a record's size -
  or a wrap filler's, which is sized by the distance to the boundary - would be
  truncated on the way in, and the consumer would then bounds-check the
  truncated value against the real one. kMaxRingCapacity rejects that at
  construction, the same class of guard as the power-of-two and
  smaller-than-a-header checks beside it. Unreachable today (SEG_CMD 8 MiB,
  SEG_STAGE 32 MiB), which is the point of catching it now.

- Framing.h included MG_Util/Debug/Log.h, which includes Includes.h, the GL
  frontend's umbrella header: 661 headers by `clang++ -H`. It is the one header
  under Transport/ that broke the rule ITransport.h states for this layer
  ("nothing about a byte pipe needs the GL frontend's umbrella header"), which
  matters when the server-side binary links this and when the include-graph
  purity gate of plan section 10.3 (gate A, asserted on -H output rather than
  on symbols) lands. Its three error paths now call WireLogError, declared in a
  new dependency-free WireLog.h whose .cpp owns the umbrella. Framing.h is down
  to 134 headers, none of them MG_State, Includes.h or Log.h.

- Two documentation corrections. ITransport::Shutdown documented a one-sided
  "releases the endpoint" while InProcessTransport::Shutdown closes both
  directions - which is what closing a socket does, so the spawn transport will
  behave the same way; the interface now says whole-connection teardown, and
  keeps the promise that queued messages stay readable until drained.
  InProcessTransport.h cited a CMake option
  MOBILEGL_BUILD_DISAGGREGATED_INPROC that grep finds nowhere: plan appendix B
  reserves it for the role-isolation shim, this skeleton does not add it, and
  the delivery mode is a runtime choice (MOBILEGL_TRANSPORT), not a build one.

- Evidence: both configurations reconfigured and rebuilt; nm --defined-only on
  the OFF build still reports 0 MG_Remote symbols and the ldd dependency set is
  byte-identical to the OFF link (plan section 10.3, the two surviving
  byte-level equalities). Negative controls: removing the capacity ceiling
  makes RingTest.RejectsACapacityTheRecordHeaderCannotDescribe fail on both
  roles; collapsing fdCv back into cv makes
  InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors fail at
  3950ms against its 2000ms bound.
2026-09-05 20:16:50 -04:00
swung0x48 bdd4bed431 [Fix] (MG_Remote, Transport): close the doorbell's lost-wakeup window with two seq_cst fences and stop a hung-up peer turning every park into a spin
- The header claimed the park flag's own seq_cst store and load closed the
  lost-wakeup window. They cannot: that Dekker argument needs all FOUR accesses
  in the seq_cst total order, and the other two are not in it - the watermark
  publish is a release store (RingProducer::Publish) and the condition re-test
  is an acquire load. There was no atomic_thread_fence anywhere under
  MG_Remote. On x86 a release store and a seq_cst load are both plain MOVs, so
  the notifier can read parked==0 while its head store still sits in the store
  buffer, and the waiter then parks on a stale watermark forever; ARMv8
  survived only because STLR->LDAR is RCsc, which is luck, not the design.
  Doorbell::Wait now fences after announcing and NotifyIfParked fences before
  reading the flag - the pairing the standard actually guarantees
  ([atomics.order]) - and the header says so, including the other half of the
  contract: publish the watermark BEFORE ringing, because a fence only orders
  what precedes it. This is the claim the whole P5/P6 wait discipline (present
  credit, kNeedsAck blocking requests, full-ring escalation) will be built on,
  and its failure mode is a silent cross-process hang.
  Inherited design, plan section 8.1 (PLAN.md section 6.2/6.2a: bidirectional
  doorbell, MOBILEGL_IPC_SPIN_US default 50us, condvar for inproc).

- SocketDoorbell::Park treated any poll() return > 0 as a wakeup and never
  looked at revents. Measured on this machine: an AF_UNIX SOCK_STREAM
  socketpair whose peer has closed returns revents=POLLIN|POLLHUP with
  recv()==0 immediately and forever. Park therefore returned true, Wait stored
  parked=0, found its condition still false and re-parked - so a waiter with
  kWaitForever burned a big core at full clock with no bound. That is exactly
  the pathology the bidirectional doorbell exists to prevent (a whole 16.6ms
  frame of a big core on a phone, competing with the GPU and the game's JVM),
  reached from the other side. Park now branches on revents, a new Drain()
  latches death on EOF (and on ECONNRESET/EPIPE from Notify), and the new
  Doorbell::Dead() lets Wait give up instead of re-parking on a descriptor that
  can never deliver another wakeup.

- Same commit, same defect class: `fd` is documented as one end of an AF_UNIX
  socket pair, not "a socket or pipe end". Notify uses send(MSG_DONTWAIT|
  MSG_NOSIGNAL) and Park uses poll()+recv(), which a pipe end refuses with
  ENOTSOCK, and a SOCK_DGRAM pair reports no readiness at all when the peer
  closes (measured), so the spawn transport wants SOCK_STREAM.

- Evidence: build-linux-split rebuilt clean; the wire suite is green (47/47).
  Negative control - reinstating the revents-blind Park makes
  FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp fail on both Park
  assertions, and restoring this code turns it green again.
2026-09-05 20:16:49 -04:00
swung0x48 10315e71f3 [Test] (MG_Remote, CI): cover the wire layer with five suites and gate the generated header with flatc-check
- MobileGL/MG_Test/Wire, 42 gtest cases in five binaries, ctest label `unit`, registered only when MOBILEGL_BUILD_DISAGGREGATED is ON so the default configuration is untouched.
- FdPassingTest is the one the earlier branch could not have had: it forks a child that creates a shared segment, fills 64 KiB with a pattern and hands the descriptor over SCM_RIGHTS; the parent adopts it, maps it read-only and compares every byte. Everything in Feat/CS-Delta-IPC ran in one process, which is why `out->fd = -1` survived unnoticed. It also pins that a too-small sideband buffer is refused before the datagram is consumed (so no descriptor is dropped), that an empty sideband still carries its fd, and that a receive with no offer times out. The `spawn` SocketDoorbell is covered in the same file because it rides the same kind of socket: a parked waiter woken through NotifyIfParked, a clean timeout, and a wakeup that arrives before anyone parks being remembered and then consumed exactly once.
- RingTest: control-page size/alignment/cache-line layout, a non-power-of-two capacity refused, payload alignment, 200 records driven through a 256-byte ring so the wrap filler path runs repeatedly and no record ever straddles the boundary, backpressure (full ring refuses, applied alone frees nothing, retired frees), a record larger than the ring refused, the generation bump refused while records are in flight and accepted once quiesced, a corrupt header refused instead of dispatched, and a 20000-record two-thread producer/consumer run checking order, content and the final cursor equality.
- FramingTest: byte-at-a-time reassembly of two frames, the magic reading "MGLF" on the wire, empty payloads, a bad magic and an oversized length each latching the reader dead (the old code hung silently instead), the send-side cap, and the buffer-too-small contract keeping the message so the retry still finds it.
- InProcessTransportTest: both directions, ordering, buffer-too-small, poll and timeout, shutdown draining queued messages before it closes, a blocked receiver woken by a send and by a shutdown, the frame cap, descriptor hand-off with its sideband, and three condvar doorbell cases - a parked waiter woken through NotifyIfParked, an already-true condition that must never park, and a clean timeout.
- ProtocolSmokeTest: a Hello built and read back through the committed generated header, a Welcome carrying the four segment announcements, the same buffer travelling across the transport unchanged, a truncated buffer failing verification rather than being read, and the CtrlMsg / SegmentKind / LogLevel / FatalCode tag values frozen - they are wire numbers, so if that test has to be edited the schema change was a wire break.
- CI: a `flatc-check` job in test.yml that checks out only the flatbuffers submodule, builds the pinned flatc through scripts/gen_protocol.py into the runner temp directory, regenerates and runs `git diff --exit-code` on protocol_generated.h. It needs no MobileGL build, so it does not depend on build-linux (plan B section 8.1 / earlier section 7.1: the committed header plus a CI regeneration check, and no flatc in the default build graph).
- Verified: `ctest -L unit` is green in both configurations - 1382 cases with the option OFF and 1424 with it ON (the same 1382 plus these 42); the nm gate is 0 matches OFF and 93 ON; regeneration of protocol_generated.h is byte-identical, and the flatc-check gate goes red on a hand-edited header and green again after a clean regeneration.
2026-09-05 20:16:49 -04:00
swung0x48 bfa087d0f7 [Feat] (MG_Remote, Transport): land the transport skeleton behind MOBILEGL_BUILD_DISAGGREGATED - framing, the SPSC ring, doorbells, shm segments and SCM_RIGHTS
- New CMake option MOBILEGL_BUILD_DISAGGREGATED (default OFF, plan B appendix B). ON appends the MG_Remote sources to SOURCE_FILES, puts 3rdparty/flatbuffers/include on the include path and defines MOBILEGL_BUILD_DISAGGREGATED=1. OFF compiles nothing from MG_Remote and adds no include path and no library, which is one of the two byte-level equalities plan B section 10.3 keeps: measured `nm --defined-only build-linux/libMobileGL.so | grep -ic MG_Remote` = 0 with the option OFF and 93 with it ON, with an identical ldd set in both configurations.
- The option forces itself OFF with message(WARNING) when 3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h is missing, so a checkout without the submodule still configures and builds rather than failing a hundred lines later on a missing header.
- ITransport.h: SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown. ReceiveFrame's contract is the fix for a defect of the earlier branch: a destination buffer smaller than the pending message returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the required size and KEEPS the message queued. The earlier transport failed the call and popped the message anyway, which wedges the stream permanently the first time a reader guesses a size wrong.
- Framing.h: [u32 'MGLF'][u32 len][payload], 64 MiB cap, validated on read. A bad magic or an oversized length latches the reader dead, is logged at ERROR and makes every later call return MOBILEGL_ERR_PROTOCOL_MISMATCH. This is the second inherited defect: Feat/CS-Delta-IPC's Framing.h:41-45 Feed() always returned OK and its header peek merely returned false, so a desynchronized stream became a silent permanent hang; its LocalSocketTransport.cpp:232-236 then allocated on the peer-supplied length with no cap. The magic is also byte-ordered so it reads "MGLF" on the wire, where the earlier constant spelled "FLGM".
- Ring.h/.cpp: RingControl exactly as the inherited design (plan B section 8.1 -> earlier section 6.2): two independent cursor triples (cmd and stage, each {head, appliedTail, retiredTail}), appliedSeq / submittedSeq / retiredSeq / completedFrameSerial / presentAckSerial, serverEpoch, ringGeneration, consumerParked, producerParked, eventRingFull, eventDropped. One 4096-byte page with each contended group on its own cache line, pinned by static_assert on size and alignment and by an offset test.
- The SPSC pair uses monotonic byte cursors and a power-of-two mask, so a torn read can never look like a valid earlier position. A record never straddles the wrap: the producer emits a kPad filler to the boundary, which is always a multiple of 8 and therefore always has room for a header. The producer reclaims against retiredTail rather than appliedTail, so the day the server borrows a ring slot into the GPU timeline (kRecBorrowSlot) it does not silently degrade to early recycling. HardDrainRing bumps ringGeneration only when the ring is quiesced, and leaves the cursors monotonic so cached offsets are recognisably stale.
- The consumer bounds-checks every record header before dispatch (8-aligned, at least a header, no larger than what the producer published, contiguous inside the mapping) and reports corruption to the caller instead of dispatching into undefined behaviour. That is the runtime half of the earlier section 6.3 discipline: SEG_CMD is written by another process, so a compile-time static_assert on record sizes proves nothing about what is in the mapping.
- Doorbell.h/.cpp: spin then park, both directions (earlier section 6.2a). CondVarDoorbell for inproc, SocketDoorbell for spawn (one byte, codes 0x01 ring-advanced and 0x02 watermark-advanced) - no futex, eventfd or named event anywhere. The lost-wakeup window is closed by ordering: the waiter stores its park flag seq_cst and then re-tests the condition, NotifyIfParked loads the same flag seq_cst after the watermark is published, so one of the two always sees the other. kDefaultSpinUs is 50, the MOBILEGL_IPC_SPIN_US default.
- ShmSegment.{h,cpp} + ShmSegmentPosix.cpp: memfd_create by raw syscall on desktop Linux (the glibc wrapper is too recent to rely on), ASharedMemory_create on Android (API 26; libc's memfd_create wrapper is API 30, above MobileGL's floor), shm_open + immediate shm_unlink as the fallback. Adopt() refuses a descriptor whose fstat size is smaller than the size the peer announced, so a short segment cannot turn every later offset into an out-of-bounds map. ShmSegmentWin32.cpp (CreateFileMappingW in Local\) is compile-guarded and untested - this project's Windows machine is not a correctness gate.
- The Android path is compile-verified, not just written: an arm64-v8a NDK build with the option ON links, ShmSegmentPosix.cpp.o carries an undefined ASharedMemory_create, and ShmSegmentWin32.cpp.o is empty there. That build is also what caught the missing <cstddef> in ShmSegment.h and Framing.h, where std::size_t / std::ptrdiff_t only resolved through a transitive include on the host sysroot.
- FdPassing.{h,cpp}: SCM_RIGHTS in the FIRST transport commit, as plan B section 8.1 demands, over a dedicated AF_UNIX SOCK_DGRAM socketpair rather than the control byte stream (message boundaries survive on every POSIX - SOCK_SEQPACKET does not exist on macOS - and ancillary data can never be split from its payload). The third inherited defect this replaces: Feat/CS-Delta-IPC deferred fd passing and hardcoded `out->fd = -1` in LocalSocketTransport.cpp:296, so on the only platform that matters its data plane could not move one byte between processes. MSG_CTRUNC, an unexpected descriptor count and a malformed sideband header all close every descriptor received before failing, and a too-small sideband buffer is refused before the recvmsg so a datagram is never half-consumed.
- InProcessTransport.{h,cpp}: two in-memory queues plus the condvar doorbell pair. It keeps the frame size cap so nothing that passes in inproc becomes illegal after the switch to spawn, hands descriptors over with dup() under the same ownership rule as SCM_RIGHTS, and lets a peer's queued messages be drained after Shutdown - usually the last one says why it is going away.
- Not done here on purpose: the MOBILEGL_IPC_* environment variables are parsed in ConfigLoader.cpp, which belongs to another P0 work package running in parallel; this commit only exposes the constants (kDefaultSpinUs, kMaxFramePayloadSize, FdPassing::kMaxSidebandBytes) so that plumbing has something to set.
2026-09-05 20:16:49 -04:00
swung0x48 a1e22c26ab [Build] (MG_Remote, Protocol): pin the flatbuffers submodule, add the control-plane schema and commit its generated header
- 3rdparty/flatbuffers submodule pinned to the latest release tag v25.12.19 (7e163021). The runtime is header-only, so only 3rdparty/flatbuffers/include is ever used and no library is linked; CMake never calls add_subdirectory on it and flatc is not in the build graph (plan B section 8.1, inheriting the earlier plan's section 7.1).
- MobileGL/MG_Remote/Protocol/protocol.fbs carries the CONTROL PLANE only: SegmentRef, Hello, Welcome, CapsSnapshot, DefaultFramebufferInfo, SurfaceOp, SurfaceReply, ResyncRequest, ResyncDone, AuxRequest, Fatal, LogLine, union CtrlMsg and the CtrlEnvelope root with a file_identifier. Hot-path records are FlatBuffers structs generated from MG_Pipe/PipeCalls.def in a later package and are deliberately absent here, so record numbering never churns.
- Two deviations from the earlier plan's section 7.1 sketch, both deliberate: (a) ProgramReflection is not a union member - plan B ships program artifacts inside the create_shader_state CSO blob (section 8.2), and union tags are wire values that may only ever be appended, so reserving a tag for a message that may never exist is worse than appending one later; (b) maxComputeWorkGroupCount/Size are vectors, not [int:3] - fixed-size arrays are legal only in FlatBuffers structs, never in tables.
- scripts/gen_protocol.py resolves flatc as MOBILEGL_FLATC_EXECUTABLE, otherwise builds the pinned flatc ONCE into <repo>/../flatc-build (override with MOBILEGL_FLATC_BUILD_DIR), outside the project build graph. A flatc found on PATH is deliberately refused and a version mismatch against the pinned runtime is a hard error: the generated header static_asserts FLATBUFFERS_VERSION, so a stray flatc either fails to compile or churns the committed file on every machine. The earlier branch did the opposite - Protocol/CMakeLists.txt:22-38 add_subdirectory'd the FlatBuffers tree with FLATBUFFERS_BUILD_FLATC=ON whenever MOBILEGL_FLATC_EXECUTABLE was unset, which is exactly the NDK trap it claimed to avoid (cross-compile an arm64 flatc, then run it on the host).
- protocol_generated.h is committed with the project source header prepended by the generator, so regeneration is byte-identical: verified by running gen_protocol.py twice and by perturbing the file and regenerating it back.
- Reuse from Feat/CS-Delta-IPC: MobileGL/Protocol/mg_protocol_base.h, kept as the shared C vocabulary (result codes, byte spans, shm region, id typedefs) and keeping the structSize-first versioning discipline that section 14.2 calls the answer to risk B-R10. Dropped from it: MobileGLObjectKind / MobileGLObjectScope / MobileGLObjectHandle - plan B never puts GL object identity on the wire (the frontend allocates {slot, generation} handles in MG_Pipe, section 4.2.1), so a second identity vocabulary would be a drift surface with no reader. Added MOBILEGL_ERR_BUFFER_TOO_SMALL as an append-only code for the receive contract.
2026-09-05 20:16:49 -04:00
swung0x48 bd2b4158e0 [Fix] (DirectVulkan, State): key the transform-feedback counter slots on the object's identity, not on its recycled GL name
- VulkanRenderer::CurrentXfbCounterSlot keyed m_xfbCounterSlotByObject on
  GetBoundTransformFeedbackName(). glGenTransformFeedbacks hands a deleted name
  straight back (IndexGenerator is LIFO) and nothing ever removed a map entry, so
  a transform feedback object created on a recycled name was served the DEAD
  object's counter group - and with it that group's m_xfbCountersValid and
  m_xfbLastSeenGeneration entries, which are the resume/fresh decision for
  vkCmdBeginTransformFeedbackEXT. This is D21 in plan B v2 4.7.3, the one entry
  in that table whose today-key "guards nothing", and 10.4-5 asks for it to land
  on dev on its own - hence this separate commit, kept in files no other commit
  on this branch touches so the cherry-pick applies unaided.
- Frontend: TransformFeedbackObjectState gains a never-reused `lifetimeId`
  through a default member initialiser, so every route into existence
  (operator[] materialisation, `= {}` in GenTransformFeedbackNames and
  CreateTransformFeedbackObject) mints a fresh one and a recycled name cannot
  carry the dead object's id back. The allocator is the same shape as
  BufferObject::AllocateLifetimeId (atomic, starts at 1 so a zeroed backend slot
  is never a live object).
- The bound object's id is mirrored in m_boundTransformFeedbackLifetimeId,
  refreshed by RestoreBoundTransformFeedbackState - which every bind, and the
  revert that deleting the bound object performs, goes through - and seeded for
  the default object by the GLContext constructor. GetBoundTransformFeedback
  LifetimeId is therefore a const load. Reading it through operator[] instead
  would have been an INSERT on the per-draw path, and UnorderedMap is
  ska::flat_hash_map, whose rehash invalidates every reference into the
  container, not just its iterators.
- Backend: the UnorderedMap is replaced by a fixed 16-entry owner table, which
  fixes the second half of the same defect - the map was keyed on a value that
  recycles yet was never pruned, so it grew for the life of the context. With
  lifetime ids as keys a map would have grown without bound instead, so the
  bounded table is required, not cosmetic.
- Slot exhaustion: past sixteen owners a group has to be taken over, and the
  victim is chosen among owners with NO OPEN SPAN, which
  GLContext::HasOpenTransformFeedbackSpan answers; an identity no live object
  carries any more answers false, and that is what lets a dead owner's group
  come back. Least-recently-used ALONE would have been exactly the wrong rule:
  GL only permits another object to capture while this one is PAUSED, so the
  paused span these groups exist to protect is by construction the least
  recently used entry, and an LRU takeover would reset the one resume offset
  that still matters. LRU is now only the tie-break among reclaimable groups.
  Sixteen genuinely open spans at once is reported (MGLOG_E_ONCE) rather than
  resolved silently, because whatever is taken then restarts at offset 0.
- Not done, and why: the natural place to hand a group back is
  glEndTransformFeedback, but registering DirectVulkan's EndTransformFeedback
  table entry would flip the test FixupGsStripCaptureOrder makes of that same
  pointer (GL_Drawing.cpp:1255) to decide whether the backend already captured
  in GL's vertex order, silently disabling the geometry-stage strip fixup for
  DirectVulkan. Giving that discriminator a name of its own is a separate
  change; until then the no-open-span rule is what keeps the table honest.
- CurrentXfbCounterSlot asserts the identity is never 0. Zero is the free-slot
  sentinel, so an identity of 0 would match every free slot as "mine" without
  ever claiming one - this bug reintroduced, with no symptom at the call site.
- TransformFeedbackLifetimeIdTest, in its own translation unit, pins the
  frontend halves: an object created on a recycled name must not report the dead
  object's id, the default object has an identity before anything binds it, and
  a PAUSED span still reads as open while another object is bound and capturing
  - which is the whole correctness argument for the eviction rule. The name
  reuse is not simulated: the test asks the real generator and skips (loudly) if
  it never recycled. Still untested: the >16-owners path itself, which needs a
  backend scenario with seventeen capturing objects and there is none.
- Negative controls, each applied then reverted: making a non-bound object's
  span read as closed reddens APausedSpanStaysOpenWhileAnotherObjectCaptures;
  making a vanished identity read as open reddens the same case on its delete
  assertion; dropping the constructor's seeding reddens
  AnObjectAtARecycledNameCarriesAFreshLifetimeId.
- Tested: cmake --build build-linux -j 24 (clean, 166 targets); ctest -L unit
  -j 12 -> 1386/1386 passed; ctest -L integration-gpu -> 866/866 passed
  serially, and 866/866 on one of two -j 8 runs. The other -j 8 run failed
  DirectGLES.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnviron
  mentPinsItOn, a member of the pre-existing parallel-ctest flake family: it
  passes in isolation here, and the unmodified parent tree
  (~/w7/p0-noop-wins-base) reproduces the same family under -j 8.
2026-09-05 20:16:49 -04:00
swung0x48 9c7339b214 [Feat] (State): give RenderbufferObject the never-reused lifetime id every other cache-keyable state object already has
- RenderbufferObject was the last state object a backend twin registry keys on
  that could only be identified by its heap address or its GL name - both of
  which recycle. BufferObject, VertexArrayObject and ProgramObject all carry a
  process-wide, never-reused id for exactly this; the renderbuffer's absence is
  named in plan B §10.4-5 as one of the two latent problems P0 closes.
- Mirrors BufferObject.h:202-208 / BufferObject.cpp:19-24 verbatim in shape: a
  private static AllocateLifetimeId() over a namespace-scope
  std::atomic<Uint64> starting at 1 (so a zero-initialised memo slot can never
  carry a live object's id), a const member initialised from it at construction,
  and an inline const getter. The doc comment is the buffer one restated for the
  renderbuffer's own recycling sources.
- Deliberately NO GetVersion(): plan B §11 P0 says the id only. A mutation
  counter would be a second, independent invalidation surface to keep correct,
  and nothing needs one yet - the renderbuffer's mutable content already reaches
  the backends through AllocateStorage / SetInternalFormat / SetSamples.
- No caller yet, by design: the id exists so §4.7.3's D1 rekey (and the
  DirectGLES renderbuffer twin registry at Managers.h:1858) has something to key
  on. It is a pure addition - no existing field, signature or answer changes.
- ObjectLifetimeIdTest gains the two cases the other two object types already
  have, so the renderbuffer is covered by the same allocator-reuse probe: an
  object rebuilt at a freed address must not answer to the dead one's id, and
  two live ones must differ.
- Tested: cmake --build build-linux -j 24 (clean); ctest -R ObjectLifetimeId ->
  6/6 passed (4 pre-existing + 2 new).
2026-09-05 20:16:49 -04:00
swung0x48 50815a232e [Fix] (Backend, Getter): retire the two frontend queries that were never asked and strip the unreachable frontend arms from the third
- GLFunctionsTable had three entries that are frontend queries wearing a
  backend interface (plan B v2 §2.1(a)): GetIntegeri_v, GetInteger64i_v and
  GetProgramiv. Two of them have NO caller at all - `grep -o
  'gBackendFunctionsTable\.GL\.[A-Za-z_0-9]*'` outside MG_Backend/ lists 69
  distinct entries and neither GetInteger64i_v nor GetProgramiv is among them -
  so both table slots, both backends' implementations and both registrations are
  deleted here. This is plan B §11 P0's first "strictly no-op free win".
- Nothing was moved into MG_Impl, because MG_Impl already answers all of it.
  glGetInteger64i_v is served by GL_Getter.cpp:1240-1314, which handles the
  indexed buffer queries itself and derives every other pname from its own
  GetIntegeri_v ("Handing the leftovers straight to the backend instead made
  glGetInteger64i_v disagree with glGetIntegeri_v on the very same pname").
  glGetProgramiv is served by GL_Program.cpp, whose GL_COMPUTE_WORK_GROUP_SIZE
  arm (:928-946) reads ProgramObject::GetComputeLocalSize - a link artifact of
  the program the APPLICATION wrote, which is the only program in the
  application's namespace. §4.7.1 class B.
- GetIntegeri_v stays, but only for what a backend genuinely owns. Its pure
  frontend arms were unreachable: GL_Getter::GetIntegeri_v answers
  GL_SHADER_STORAGE_BUFFER_{BINDING,START,SIZE} through
  TryDecodeIndexedBufferQuery (:991-1029) and the six GL_IMAGE_BINDING_* pnames
  at :1115-1153, and returns before touching the table. That left 9 dead cases
  in DirectGLES.cpp and 9 in DirectVulkan.cpp - the plan's "15" undercounts the
  two files separately. What still arrives is GL_MAX_COMPUTE_WORK_GROUP_COUNT /
  _SIZE (GL_Getter.cpp:1161-1177, and MG_Util/ShaderTranspiler/CompileEnv.cpp
  :134-138 asks the table directly), so DirectVulkan keeps exactly those two and
  DirectGLES becomes a plain driver passthrough.
- The dead arms were also WRONG, which is why deleting rather than reconciling
  them is the strict no-op: they clamped a bound range's size to the buffer's
  current storage, while the frontend reports the size glBindBufferRange was
  asked for verbatim (GL 4.6 core tables 23.4/23.5 - the clamp answered 0 for
  KHR-GL43.shader_storage_buffer_object.basic-binding's shape). Had a later
  refactor made the table the answer, the regression would have been silent.
- ProgramResourceCache::computeWorkGroupSize and the spirv-reflect entry-point
  loop that filled it go with DirectVulkan's GetProgramiv; nothing else read it.
- Three cases added to AdvertisedLimitsScenario pin what the frontend answers,
  on both lanes: the indexed SSBO binding/start/size on the 32- and 64-bit
  widths INCLUDING a shrink of the store underneath the binding (the arm that
  actually separates verbatim from clamped), the six image-unit pnames on both
  widths, and the compute local size plus the INVALID_OPERATION a program with
  no compute stage must give.
- Both new gates were shown to go red for their reason: making
  GL_COMPUTE_WORK_GROUP_SIZE answer a defaulted (1,1,1) fails
  ComputeLocalSizeComesFromTheLinkedProgram on both lanes, and re-introducing
  the deleted store clamp in GL_Getter fails
  IndexedBufferBindingsAreReportedVerbatimOnBothWidths on both lanes.
- Tested: cmake --build build-linux -j 24 (clean); ctest -L unit -j 12 ->
  1382/1382 passed; ctest -R AdvertisedLimits -> 18/18 passed (6 pre-existing +
  3 new, x DirectGLES and DirectVulkan).
2026-09-05 20:16:49 -04:00
swung0x48 d380a01f32 [Fix] (Metrics, DirectGLES, DirectVulkan): stop the summary line printing window totals under a per-frame label, and wire the six staging paths the site inventory claimed were covered or absent
- A window with no Present divided by a faked 1 and printed the window TOTALS under
  "bytes/f[...]": the *MultiDraw* slice (47 draws, no present) reported 1,404,550 bytes as a
  PER-FRAME figure, a 47x overstatement of exactly the SEG_STAGE sizing input plan B section
  8.2 / section 11 P0 asks this package to produce. FormatWindowLine now relabels the bracket
  to "bytes[...]" and prints draws/f=n/a when the window holds no frame; acc/draw=n/a follows
  the same rule, because "0.00" beside a non-zero acc= is the same lie. Pinned by
  PipeStatsTest.SummaryLineSurvivesZeroFrames and the reworked SummaryLineSurvivesZeroDraws.
- Every per-frame and per-draw field now goes through one FormatFixed2 helper. draws/f read 1
  for 26 draws over 14 frames (1.86) and buf read 97 for 1360 bytes (97.14): a systematic
  downward truncation of up to a whole unit on the figures the package exists to produce.
  Pinned by PipeStatsTest.PerFrameFieldsKeepTwoDecimals.
- FormatSummaryLine rewrote the window bases as a side effect of formatting, so any second
  reader silently zeroed the next window. Split into a pure FormatWindowLine() and an explicit
  AdvanceSummaryWindow(); EmitSummaryLine calls both. Pinned by
  PipeStatsTest.FormattingTwiceDoesNotConsumeTheWindow.
- Init() called ResetForTesting(), against the header's own "not used by any shipping path".
  Both now forward to an internal ResetCounters().
- TracyPlot published only the MISS half of each gate under the gate's unqualified name, so
  the headline output channel of section 11 P0 carried no denominator. Two series per gate now
  ("...-hit" / "...-miss") from static literal arrays. The payload histogram stays unplotted
  and says why: it is a run-total distribution over draws (section 4.5.7), not a per-frame
  scalar, and it reaches the operator through the JSON dump.
- GetOrCreatePipeline's 15-call tally sat ABOVE the list-topology primitive-restart refusal,
  so a declined draw added ten reads it never made - an OVER-count, which breaks the lower
  bound contract every other tally keeps. Moved to immediately before the payload build, and
  the enumeration reconciled with the constant: the excluded read is the sample-shading
  capability, short-circuited by m_sampleRateShadingFeatureEnabled.
- Six real staging paths were uncounted while the inventory claimed coverage. The inventory
  claim "DirectVulkan's own buffer staging ... has no second copy to count" was simply false.
  Now wired: MultiDraw.cpp's UploadScratch/UploadScratchRing (indirect commands, the compute
  tier's draw-info array, the rebased index stream - the class is passed in, so a new tier
  cannot forget it); Managers.cpp's pool-recycle reseed and the VBO-backed Float64 narrowing;
  VkBufferManager's eight host->device copies of buffer contents plus UploadTransient, the
  single chokepoint for Magma's per-draw vertex/index/indirect staging; VkTextureManager's
  packed staging slice, with the same box/rect SHAPE split Espryt already reported.
- New byte class stage-indirect-cmd, for draw PARAMETER bytes a backend synthesises and
  stages. Kept out of stage-index-client because these are the population that becomes MGPipe
  command-record payload (section 4.5.7), not resource bytes. A name addition, not a rename:
  no recorded baseline is invalidated.
- stage-ubo-named moved past the zero-copy direct-bind decision in ResolveUniformBufferPayload
  (a direct bind repacks nothing, so counting it there reported a copy that never happened),
  and Magma's default-uniform-block image now feeds stage-ubo-global the way Espryt's does,
  counted after the per-frame slice memo.
- The site inventory in PipeStats.cpp is rewritten to name every unwired path by file and
  function. An inventory that overstates coverage is worse than a missing counter, because
  the zero is then read as an answer.
- Evidence, lavapipe/llvmpipe, MOBILEGL_PIPE_STATS=1: the MultiDraw slice now prints
  "window=0 draws/f=n/a bytes[buf=1404550 ...]"; the indirect tiers
  (MOBILEGL_ESPRYT_MULTIDRAW_MODE=indirect|multiindirect) move icmd 0 -> 660; Magma's GuiBatch
  line moves from buf=0 tex=0 ubog=0 to buf=685.71 tex=41.14 ubog=157.71 with tex[emit=9
  box=9 rect=0 jobs=9]. Off-path A/B against the base tree with the env unset, 9 runs each of
  a 40-scenario draw slice, sorted totals in ms: Espryt base 389..794 (median 399) vs branch
  384..403 (median 394); Magma base 406..472 (median 410) vs branch 408..761 (median 414) -
  the always-compiled guard is below this harness's noise on both backends.
- 1410 unit tests green (15 PipeStatsTest). integration-gpu 860/860, 860/860, 859/860; the one
  failure is DirectVulkan.PointSizeDemotion...TheDemotionIsActuallyArmedWhenTheEnvironmentPins-
  ItOn, a member of the load-dependent *IsActuallyArmed* flake family already present in the
  untouched base tree, and it passes 8/8 standalone here. stdio gate and gen_pipe check green.
2026-09-05 20:16:49 -04:00
swung0x48 7566a0b002 [Feat] (DirectGLES, DirectVulkan): instrument the six memo gates, the staging byte paths and both Present hooks with the MGPipe counters
- The sites of plan B section 2.3.1, verified against dev@81b17c0b (the plan's own line
  numbers for DirectGLES drift by 4-9 lines; the DirectVulkan ones are exact):
  SyncRenderState is DirectGLES.cpp:1994 with the version read at :1998 and the
  early-out at :2007-2010 (plan says 2003 / 2007 / 2016-2018); SyncNeccessaryTextures
  at :1511 (plan :1520); CurrentUnitBindingsEpoch at :1412-1435 (plan :1418-1436);
  PrepareForDraw at :2907-2968 (plan :2916-2976); the global-UBO upload at :3355-3397
  (plan :3369-3392); TrySetupDrawFastPath :5994, GetOrCreatePipeline :4948-4993,
  ApplyDynamicDrawStateTail :5871-5893 and UniformManager ResolveUniformBufferPayload
  :2022/:2052 all as cited.
- Accessor counting is STATIC TALLIES at ten hot entry points, not a wrapper around the
  293 pGLContext-> sites: each instrumented function adds the number of accessor calls
  its own body made on the path taken. Reads inside callees, and every conditional read
  (the sRGB capability in SyncRenderState, the XFB probe and the version-gated parameter
  fetch in TrySetupDrawFastPath, the cull-mode/logic-op/tessellation reads in the
  pipeline payload builder) are excluded, so the number is a consistent LOWER bound. The
  full inventory of what is and is not counted is the header comment of PipeStats.cpp.
- The Magma fast-path gate is counted from SetupDraw, not from inside
  TrySetupDrawFastPath: that function has 27 decline returns and one success return, and
  counting at the caller is the only shape that cannot miss one.
- The texture upload counts the SHAPE (union box vs N-rect list) separately from the
  bytes, because SSIM is blind to the shape and the +6 ms/frame Mali regression of
  section 7.3 was a shape regression, not a byte one.
- Frame boundary: DirectGLES::Present after the ring upkeep, and DirectVulkan's backend
  Present rather than VulkanRenderer::Present - the latter has an early return for the
  no-usable-swapchain case, and a suspended frame is still a frame the counters close.
- Measured on lavapipe/llvmpipe with MOBILEGL_PIPE_STATS=1, GuiBatchScenario, 14 frames
  and 26 draws: Espryt 20.65 accessor calls per draw (gates ers 22/18, etl 71/9, eub
  71/9), Magma 15.54 (mfp 12/14, mpm 0/14, mdt 12/14). Both land inside the 10-25 band
  section 2.3.1 predicted and far below the 124/169 static counts, which is the
  correction that section was written to force.
2026-09-05 20:16:49 -04:00
swung0x48 42e0f47ebb [Feat] (Metrics): land the MGPipe boundary counters - bytes, dynamic accessor calls and the six memo gates, off unless MOBILEGL_PIPE_STATS is set
- Plan B section 11 P0 asks for TracyPlot per-frame counters on both sides of the
  boundary, and section 2.3.1 adds the deliverable v1 did not have: DYNAMIC call
  counters. The static call-site counts everyone quotes (Espryt 124 / Magma 169) are
  not the per-draw cost, because every one of those paths is memo-gated; without a
  dynamic counter P2's verdict stays a guess. The tree had no per-frame byte or call
  measurement at all - MG_Util/Metrics is format arithmetic, and Tracy has zones but
  no plots.
- Cost when off: g_pipeStatsEnabled is a plain global Bool latched once in
  Initialize() right after MG_ConfigLoader::Init(), and every counting site is
  `if (PipeStats::Enabled()) ...` - one load of a hot global and one never-taken
  branch. The counters are relaxed atomics because buffer and texture staging are
  reachable from more than one thread; the off path never touches them.
- Eight byte classes (stage-buffer, stage-texture, stage-ubo-global, stage-ubo-named,
  stage-vertex-client, stage-index-client, persistent-map-push and the
  residual-value-block placeholder of section 6.3), six call classes and the six memo
  gates of section 2.3.1, each as a hit/miss pair. Names are minted now, including the
  two that stay 0 in P0, so no recorded baseline is invalidated by a later rename -
  which is why the name set is pinned by a test.
- Reporting: TracyPlot per counter per frame when TRACY_ENABLE; with
  MOBILEGL_PIPE_STATS=1 one MGLOG_I summary line every 120 frames and at teardown, plus
  a JSON dump to MOBILEGL_PIPE_STATS_FILE when that is set. MGLOG_I against the usual
  "MGLOG_D for non-critical" rule on purpose: the line has to survive an INFO build -
  the only build a device runs - it is at most one line per 120 frames, and it exists
  only when the operator asked for it.
- Summary lines report disjoint WINDOWS, not run totals: a run total over a workload
  that changes shape (load, then steady state) averages away the very number section
  2.3.1 wants an absolute value for.
- MOBILEGL_PIPE_STATS_FILE joins the six switches parsed in e48a3582; like them it
  needs no allow-list entry because InitializeAcceptedEnvVariables accepts every
  MOBILEGL_-prefixed variable by construction.
2026-09-05 20:16:49 -04:00
swung0x48 9bbf71990c [Build] (CI): add the pipe-gen check, the stdio-instrumentation gate and the citation lint
- Three P0 gates from plan B section 11, in one job that needs no build: a broken build
  must not be able to hide a drifted interface.
- pipe-gen-check regenerates G1-G7 and runs git diff --exit-code over
  MobileGL/MG_Pipe/generated. Since all seven generators read the same .def files, this is
  what makes drift between them impossible rather than merely unlikely.
- The stdio gate refuses fprintf(stderr and printf( under MG_Backend and MG_State. Nothing
  there matches today, so it lands with NO whitelist - verified with a negative control
  that fprintf(stderr and std::printf both trip it while snprintf does not. Per-draw
  instrumentation has been committed by accident before, once inside a mutex critical
  section, and MGLOG_D is the channel these trees are allowed to use because it compiles
  out in INFO builds.
- scripts/gen_pipe_dirty_surface.py reports the frontend mutation surface corollary 4
  needs covered: 926 mutator calls under MG_Impl/GLImpl over 73 distinct mutators, of
  which only 92 sit in a function that also reaches the backend. The other 834 are
  published by the NEXT verb, which is precisely the population that needs an aggregate
  generation. Informational in P0; it becomes a gate in P1 when there is a mapping file to
  diff against.
- scripts/check_doc_citations.py resolves every `path:line` citation against a git
  revision. It reproduces the failure that motivated it - the plan's first draft cited
  SamplerObject.h:468-492 in a 160-line file - and reports 84 unresolved citations out of
  1028 in docs/Disaggregated today, which is why CI runs it warning-only until those
  documents settle. --strict exits 1, verified in both directions.
2026-09-05 20:16:49 -04:00
swung0x48 2f8d0f0d51 [Feat] (Config): parse the six MOBILEGL_PIPE_* switches
- Appendix B of plan B. All six default to today's behaviour: PipePush 0 is "pull
  everything", verify and stats off, legacy memos ON so the first handle waves keep a real
  old-versus-new arm (B-R16), texel retain 0 because MipmapStorage already holds a complete
  CPU shadow so that cache buys latency and never correctness (section 7.5c), index mirror
  64 MiB.
- No allow-list edit is needed and the header says why: InitializeAcceptedEnvVariables
  accepts every MOBILEGL_ / LIBGL_ prefixed variable found in the environment, so a name
  with that prefix is visible to these queries by construction - the failure mode of a
  hand-maintained accepted list does not exist here.
- PipeLegacyMemos defaults ON, so it is read as a tri-state (QueryEnvQuirkOverride !=
  ForceOff) rather than as a plain truthy flag: unset must keep the memos and only an
  explicitly falsy value may drop them. Reading it with QueryEnvFlag would have inverted
  the default silently.
- QueryEnvUint64 is added alongside QueryEnvUint32 for the subsystem bitmask, accepting a
  0x prefix - a bitmask written in decimal is unreadable - and warning and falling back to
  the default on anything unparseable, exactly like its 32-bit sibling.
2026-09-05 20:16:49 -04:00
swung0x48 3363258908 [Test] (MGPipe): pin the catalogue arithmetic, the wire opcodes and the comparator
- Thirteen cases that need no GL context and no driver, so the fact that PipeCalls.def and
  the seven generated files agree is checked on every unit run rather than at review time.
- The catalogue count is one fact stated three times - the macro expansion,
  MGP_CALL_LIST_DOCUMENTED_COUNT and the two generated tables - and the per-class counts
  documented in the .def header are asserted individually, which is what caught kCtxState
  being 17 rather than 18 (set_texture_params carries kCtxObject, per the plan's own
  example in section 4.1).
- An uninstalled pipe must be all-null: that is what "this subsystem has not been migrated,
  keep pulling" means (section 4.1), so it is asserted rather than assumed.
- The comparator test pins the two properties the verify harness depends on: the FIRST
  differing field is named, and padding is not a field - two payloads that differ only in
  padding compare equal, while a nested array element does not.
- The poison test pins the per-verb behaviour a bitmap cannot express: a field filled for
  verb N is stale at verb N+1.
2026-09-05 20:16:49 -04:00
swung0x48 9c773182bb [Feat] (MGPipe): land the interface skeleton - the complete call catalogue, the payload PODs and the seven generators
- Plan B section 4 makes the frontend/backend boundary explicit and gives the backend its
  own state machine. This is the P0 deliverable of section 11: the whole catalogue exists
  from day one, placeholders included, because the wire opcode is a call's position in
  PipeCalls.def and record numbering must never churn.
- MG_Pipe/PipeCalls.def is the single source of truth: 68 unique calls as
  X(Name, Payload, Class, Flags). Its header reconciles that number with the plan's
  headline counts (section 4.4, appendix A), which double count - "CSO 15" names
  bind_sampler_states and set_sampler_views that the "set_* 17" list also names, "screen
  14" tabulates the query family that section 4.3 assigns to the context, and the
  "transfer 12" row enumerates 11 calls. Each reconciliation is written down next to the
  count rather than resolved silently.
- MGPipeHandles.h: the 8-byte {slot, gen} pair, dense per-kind slots, the reserved null
  and default-framebuffer handles, and the ShaderCso composite band (sections 4.2, 5.6.3).
  The two generations are documented as strictly separate, with the interface rule that no
  call may require the client to know MGGen.
- MGPipeTypes.h: every payload of section 4.5 as a flat POD with explicit padding, a
  trivial-copyability assertion and an exact sizeof assertion, because the wire records are
  memcpy'd and a field silently changing width is a protocol break no test would see.
  MGPCaps embeds DynamicBackendParameters by inclusion so a caps field added there needs no
  second edit here; its assertion is stated as a composition because that struct still
  carries SizeT. ResidualValueBlock is pinned at MGL_RESIDUAL_BLOCK_SIZE 1248, the ratchet
  that only ever goes down and reaches static_assert(... == 0) in P13 (section 6.3).
- MGPipeHostSpan.h keeps the one shape that changes with the transport isolated behind one
  predictable branch, with the kFromServerIndexMirror sentinel D-B7 needs.
- MGPipeCallbacks.h names the reverse channel as ten callbacks plus the forward terminator
  in the context table, replacing 95 poke sites across 17 methods (section 7.1).
- scripts/gen_pipe.py runs G1-G7 off those .def files. G1 asserts each table is EXACTLY its
  call count of function pointers; G3 pads every wire record to the stream's 8-byte
  granularity and checks size >= sizeof && size <= remaining && size % 8 == 0 before
  dispatch, fatally; G4 compares field by field (padding excluded, floats by bits) because
  a comparator with false positives is one nobody reads - DirectGLES.cpp says the same
  thing about its own memcmp of RenderStateParameters; G5 turns the accessor list into
  per-verb poison generations rather than a written-once bitmap, which is the only version
  that can see a field left over from the previous draw (section 6.2.2); G6 joins the 477
  read points of the vendored backend_read_inventory.md against Coverage.def and reports
  0 UNMAPPED (299 to a call, 167 signatures that become handle parameters, 6 reverse
  channel, 5 client-resolved); G7 pins the pipeline subset BY MEMBER NAME from what
  VulkanRenderer::ComputePipelineStateHash hashes today, computing no offsets in python.
- The generated files are committed so the build never depends on python; CI regenerates
  and diffs them.
2026-09-05 20:16:49 -04:00
swung0x48 8349babe90 [Docs] (Disaggregated): consolidate into a single MGPipe plan and drop the replica plan
- docs/Disaggregated/PLAN.md is now the one plan: the MGPipe design with the transport, control-plane, present, threading, EGL/process, monolith/build chapters inlined as real chapters (7-13) instead of references, sections renumbered 0-17 + appendices, every internal citation updated
- the replica-GLContext plan and its review record are removed at the user's request; REVIEW.md is the MGPipe design-competition and adversarial-review record only, with the comparison verdicts dropped and the remaining finding text reworded to the new section numbers
- day-43 GO/NO-GO now names its two outcomes (continue / shrink to headless tooling or re-evaluate) without any rollback path
2026-09-05 14:06:24 -04:00
swung0x48 1794ac94b1 [Docs] (Disaggregated): land plan B - MGPipe, a gallium-style explicit frontend/backend interface with a backend-owned state machine (client-allocated {slot,gen} handles, CSOs keyed on the pipeline subset with dynamic state pushed separately, set_* catalogue derived from the twins the two backends already keep, per-verb validate with aggregate generations, reverse channel as ten named callbacks plus a pull terminator, strangler migration behind PipeInputs with per-verb poison and a field-wise verify harness, three purity gates replacing byte identity), the item-by-item comparison against the replica plan A (memory, copies, drift surface, monolith benefit vs 4x effort and 7x later first frame) with a day-43 GO/NO-GO hedge, and the round-2 design-competition and adversarial-review record; mark plan A as superseded except for its inherited transport sections 2026-09-05 13:05:43 -04:00
swung0x48 8b31de2f8d [Docs] (Disaggregated): land the two-process split plan - server runs the unchanged backends against a replica GLContext driven by mutator replay, the client is the sole originator of every implicit-publish semantic (persistent-map push, MarkGpuWritten, texture dirty clear, XFB accounting, generated-mip allocation), SPSC shm ring with FlatBuffers structs on the hot path and tables on the control socket, two-way doorbells, present credit 1, spawn-only shipping build with an inproc CI variant, and a P0-P9 schedule gated on the existing unit/integration/retrace/CTS suites; plus the design-competition and adversarial-review record 2026-09-05 09:21:58 -04:00
111 changed files with 20187 additions and 303 deletions
+81
View File
@@ -295,6 +295,33 @@ jobs:
path: /tmp/core.*
if-no-files-found: ignore
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
# flatc is deliberately absent from the default build graph (a codegen step in
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc
# and trying to run it on the host). This job is what keeps the committed
# header honest: build the pinned flatc, regenerate, and fail on any diff.
# It needs no MobileGL build, so it does not depend on build-linux.
flatc-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Check out the FlatBuffers submodule only
# Just this one: the schema check has nothing to do with glslang,
# SPIRV-Cross or the trace fixtures.
run: git submodule update --init 3rdparty/flatbuffers
- name: Regenerate protocol_generated.h
run: python3 scripts/gen_protocol.py --build-dir "${{ runner.temp }}/flatc-build"
- name: Fail if the committed header is stale
run: git diff --exit-code -- MobileGL/MG_Remote/Protocol/generated/protocol_generated.h
benchmark:
runs-on: ubuntu-latest
needs: build-linux
@@ -778,3 +805,57 @@ jobs:
)
echo "Deleted ${deleted} intermediate Linux artifact(s); retained ${retained} failed-retrace fixture(s)."
pipe-gates:
name: MGPipe generators and hygiene gates
runs-on: ubuntu-latest
# Deliberately independent of build-linux: these are source-level gates, they take
# seconds, and a broken build must not hide a drifted interface.
steps:
- name: Checkout repo
uses: actions/checkout@v6
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
# keeps the two interface tables, the wire records, the verify comparators, the
# PipeInputs field ids, the read-inventory coverage and the render-state member list
# from drifting apart from the catalogue. The generated files are committed
# deliberately: the build must not depend on python.
- name: Regenerate the MGPipe interface (G1-G7)
run: |
python3 scripts/gen_pipe.py
git diff --exit-code -- MobileGL/MG_Pipe/generated
# Per-draw fprintf/printf instrumentation has repeatedly been committed by accident,
# once inside a mutex critical section. Nothing under these two trees prints to a
# stdio stream today - MGLOG_D compiles out in INFO builds and is the only channel
# they are allowed to use - so this gate starts with no exceptions, and any addition
# to it needs a reason in the pull request rather than a quiet whitelist entry. The
# alternation names every stdio spelling, not just the two that were committed:
# fprintf to either stream, printf, puts, and the iostream pair.
- name: No stdio instrumentation in MG_Backend or MG_State
run: |
if grep -rnE 'fprintf[[:space:]]*\((stderr|stdout)|(^|[^[:alnum:]_>.])printf[[:space:]]*\(|(^|[^[:alnum:]_>.:])puts[[:space:]]*\(|std::(cout|cerr)' \
MobileGL/MG_Backend MobileGL/MG_State; then
echo "::error::stdio instrumentation found; use MGLOG_D (compiled out in INFO builds)"
exit 1
fi
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
# Informational: the frontend mutation surface an MGPipe aggregate generation has to
# cover. It becomes a gate in P1, when the mapping file exists to diff against.
- name: MGPipe dirty-surface report
run: python3 scripts/gen_pipe_dirty_surface.py --summary
# Warning only for now: the disaggregation documents are still being written, and a
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
# --strict when the documents settle.
- name: Documentation citation lint
run: |
shopt -s nullglob
documents=(docs/Disaggregated/*.md)
if [ ${#documents[@]} -eq 0 ]; then
echo "no disaggregation documents to check"
exit 0
fi
python3 scripts/check_doc_citations.py "${documents[@]}" || true
+3
View File
@@ -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
Vendored Submodule
+1
Submodule 3rdparty/flatbuffers added at 7e163021e5
+108
View File
@@ -14,6 +14,14 @@ 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)
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 +246,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
@@ -417,6 +427,49 @@ 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()
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
@@ -467,11 +520,19 @@ set(MOBILEGL_COMPILE_DEF
-DASIO_NO_DEPRECATED
)
if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=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}
@@ -482,6 +543,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}
)
@@ -698,3 +766,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()
+40
View File
@@ -316,6 +316,46 @@ namespace MobileGL::MG_Config {
// 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 default and the only shipped value until the migration lands - is "pull
// everything", i.e. exactly today's behaviour. One bit of it also turns OFF
// client-side content addressing of CSOs, which is the negative control the CSO
// design is measured against. Accepts decimal or 0x-prefixed hex.
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;
// 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
+47
View File
@@ -159,6 +159,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");
@@ -207,6 +239,21 @@ namespace MobileGL::MG_ConfigLoader {
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.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
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() {
+10
View File
@@ -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();
+29 -2
View File
@@ -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.
@@ -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
+84 -129
View File
@@ -28,6 +28,7 @@
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Metrics/BufferMetrics.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <Config.h>
#include <atomic>
@@ -1412,10 +1413,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
if (MG_Util::PipeStats::Enabled()) {
// Two accessor calls whichever way the shutter goes; only the unit WALK is
// gated, and that walk reads no GLContext accessor of its own.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit &&
g_observedUnitBindingsGeneration == bindGeneration) {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/true);
}
return g_unitBindingsEpoch;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/false);
}
if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit ||
!UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) {
CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings);
@@ -1505,6 +1517,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
if (MG_Util::PipeStats::Enabled()) {
// The three reads above; CurrentUnitBindingsEpoch counts its own two.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
return keys;
}
@@ -1534,6 +1550,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
PairingsIntact(g_unitTextureSyncList)) {
if (MG_Util::PipeStats::Enabled()) {
// Gate 2 of section 2.3.1. The served path walks the memoised entries
// and reads no GLContext accessor at all.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true);
}
for (const auto& entry : g_unitTextureSyncList) {
// Aggregate gate == the conjunction of the three callees' own
// early-outs (see IsDrawSyncClean); skipping on true is
@@ -1546,6 +1567,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
entry.backend->SyncMipmapsToBackend(*entry.slot);
}
} else {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false);
// One GetTextureUnitObject per touched unit in the rebuild walk below.
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::AccessorCalls,
maxTouchedUnit >= 0 ? static_cast<Uint64>(maxTouchedUnit) + 1u : 0u);
}
g_unitTextureSyncListValid = false;
g_unitTextureSyncList.clear();
for (Int index = 0; index <= maxTouchedUnit; ++index) {
@@ -2006,8 +2034,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask;
if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState &&
currentRenderStateVersion == g_syncedRenderStateVersion) {
// Gate 1 of section 2.3.1: the steady-state cost of this whole function is
// the one Uint16 read above plus this compare.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false);
// The version read above, the parameter-block fetch and the viewport fetch
// below - the three accessor calls this function makes unconditionally on a
// miss. The conditional sRGB capability read further down is deliberately
// NOT counted (see the inventory in PipeStats.cpp).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
@@ -2924,6 +2966,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// cross-TU call with a guarded static inside - repeating it per stage showed
// up in draw-loop profiles.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
if (MG_Util::PipeStats::Enabled()) {
// THE per-draw denominator for Espryt, plus this function's own two accessor
// calls (the VAO and the draw program). Everything the callees below read is
// counted by the callees that are instrumented; the rest is not counted (see
// the inventory in PipeStats.cpp).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion,
@@ -3370,6 +3420,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (BufferImpl::UboRingAllocate(bindSize, offset)) {
std::memcpy(static_cast<Uint8*>(BufferImpl::UboRingMappedPtr()) + offset,
currentProgram->MapUBO(), uboSize);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(uboSize));
}
ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial,
offset};
slotValid = true;
@@ -3389,6 +3443,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
currentProgram->MapUBO());
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(
MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(currentProgram->GetUBOSize()));
}
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
}
@@ -4306,6 +4365,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_restartIndices.capacity = capacity;
if (data != nullptr && bytes != 0) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
// Rewritten index list staged on the draw path: in a split build these
// bytes are the index-mirror-versus-ship decision of section 8.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
static_cast<Uint64>(bytes));
}
}
return true;
}
@@ -7252,138 +7317,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncImageTextureBinding(unit);
}
// Only the pnames MG_Impl/GLImpl/Getter/GL_Getter.cpp has no case for reach here. Every
// indexed pname naming FRONTEND state - the indexed buffer bindings, the per-unit
// texture/sampler bindings, the image-unit bindings, the viewport rectangles, the indexed
// capabilities - is answered there and returns 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 of the frontend's, and they did not even agree with it (the
// frontend reports the range glBindBufferRange was ASKED for, verbatim and unclamped; these
// clamped it to the buffer's current storage). In practice what arrives is
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE, which the driver owns.
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
if (!data) return;
switch (target) {
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;
if (g_GLESFuncs.glGetIntegeri_v) {
g_GLESFuncs.glGetIntegeri_v(target, index, data);
} else {
*data = 0;
}
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: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
return;
}
case GL_IMAGE_BINDING_LEVEL: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Level;
return;
}
case GL_IMAGE_BINDING_LAYERED: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Layered;
return;
}
case GL_IMAGE_BINDING_LAYER: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = imageBinding.Layer;
return;
}
case GL_IMAGE_BINDING_ACCESS: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
*data = static_cast<GLint>(imageBinding.Access);
return;
}
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));
*data = static_cast<GLint>(imageBinding.Format);
return;
}
default:
if (g_GLESFuncs.glGetIntegeri_v) {
g_GLESFuncs.glGetIntegeri_v(target, index, data);
} else {
*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:
if (g_GLESFuncs.glGetInteger64i_v) {
g_GLESFuncs.glGetInteger64i_v(target, index, data);
} else {
*data = 0;
}
return;
}
}
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
if (!params) return;
GLuint backendProgramId = GetBackendProgramId(program);
if (!backendProgramId) {
params[0] = 0;
return;
}
g_GLESFuncs.glGetProgramiv(backendProgramId, pname, params);
}
// NOTE the shape here, and do not "simplify" it back to GetBackendProgramId(): this entry
@@ -10638,6 +10587,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::UnpackRingOnPresent();
BufferImpl::UploadRingOnPresent();
BufferImpl::TrimBufferPool();
// THE frame boundary for the MGPipe counters: publish this frame's plots, fold the
// frame into the run totals and, every 120th frame, emit the summary line.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
}
void DestroyEGLContext() {
@@ -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);
@@ -15,6 +15,7 @@
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
@@ -779,6 +780,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
if (MG_Util::PipeStats::Enabled() && initialData != nullptr) {
// An ORPHANING respecify passes NULL and moves nothing, which is exactly
// why the test is on initialData rather than on size.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
@@ -886,6 +893,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT start = std::min(range.start, end);
const SizeT size = end - start;
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
// Counted once per queued range, before the three delivery shapes
// below diverge: all three move exactly these bytes, and it is the
// byte count - not the shape - that sizes SEG_STAGE.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
// The invalidating map's fast path is SHAPE-dependent on this Mali
// driver: a whole-buffer invalidation renames the store outright,
// and a large range gets fresh pages - but a small unaligned range
@@ -953,6 +967,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (write.offset >= limit) continue;
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
@@ -1520,6 +1538,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBufferId(TempBufferTarget, reused);
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize,
bufferObject->MappedData());
if (MG_Util::PipeStats::Enabled()) {
// The pool-recycle reseed is a whole-buffer upload on the hot path,
// not a bookkeeping detail: it moves the same bytes a fresh
// glBufferData would.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(poolSize));
}
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->pendingRanges.clear();
@@ -2545,6 +2570,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
// GL ignores `normalized` for floating-point array types, so it is not
// forwarded here either.
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
@@ -2575,6 +2604,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(uploadSize), clientData,
GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(uploadSize));
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
@@ -2669,6 +2702,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
// The VBO-backed half of the 64-bit narrowing. Same population as the
// client-array half above: a stream the backend synthesises per draw.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
stream.valid = true;
stream.sourceLifetimeId = sourceLifetimeId;
stream.sourceChangeSerial = sourceChangeSerial;
@@ -4391,6 +4430,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (ringStaged) {
BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId());
}
if (MG_Util::PipeStats::Enabled()) {
// One emission per (upload target, level) that ships texels;
// the switch below turns it into either one union-box job or
// dirtyRectCount rect jobs. The box/rect split is counted
// separately from the bytes on purpose: SSIM is blind to it
// and the +6 ms/frame Mali cliff was a shape regression, not
// a byte regression (plan section 7.3).
const Bool rectShape = subRectEligible && dirtyRectCount >= 2;
Uint64 shippedBytes = 0;
if (rectShape) {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
shippedBytes += static_cast<Uint64>(rect.hi.x() - rect.lo.x()) *
static_cast<Uint64>(rect.hi.y() - rect.lo.y()) *
static_cast<Uint64>(std::max(rect.hi.z() - rect.lo.z(), 1)) *
static_cast<Uint64>(bpp);
}
} else if (subRectEligible) {
shippedBytes = static_cast<Uint64>(regionSize.x()) *
static_cast<Uint64>(regionSize.y()) *
static_cast<Uint64>(std::max(regionSize.z(), 1)) *
static_cast<Uint64>(bpp);
} else {
shippedBytes = static_cast<Uint64>(byteSize);
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
shippedBytes);
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1);
MG_Util::PipeStats::AddCalls(
rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions
: MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions,
1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
+25 -6
View File
@@ -9,6 +9,7 @@
#include "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring>
#include <limits>
@@ -156,7 +157,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 +173,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 +190,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 +215,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 +428,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 +544,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 +750,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);
@@ -740,8 +740,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;
@@ -939,6 +937,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);
+24 -117
View File
@@ -13,6 +13,7 @@
#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 +78,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 +208,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) {
@@ -682,130 +672,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;
@@ -1430,5 +1330,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);
@@ -20,6 +20,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>
@@ -2076,6 +2077,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 +2294,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,
@@ -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 "MG_State/GLState/Core.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>
@@ -3150,6 +3151,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;
@@ -27,6 +27,7 @@
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
#include "MG_Util/Texture/PixelStoreProcessor.h"
@@ -3266,8 +3267,9 @@ void main() {
}
m_vertexInputStateFactory.reset();
m_xfbCounterBuffer.Destroy();
m_xfbCounterSlotByObject.clear();
m_xfbNextCounterSlot = 0;
m_xfbCounterSlotOwner.fill(0);
m_xfbCounterSlotLastUse.fill(0);
m_xfbCounterSlotUseSerial = 0;
m_xfbCountersValid.fill(false);
m_xfbLastSeenGeneration.fill(0);
if (m_occlusionQueryPool != VK_NULL_HANDLE) {
@@ -4999,9 +5001,19 @@ void main() {
entry.pipelineStateHash == pipelineStateHash &&
entry.primitiveRestartEnable == primitiveRestartEnable &&
entry.transformFlags == transformFlags) {
if (MG_Util::PipeStats::Enabled()) {
// Gate 5 of section 2.3.1. On a hit this whole function cost the one
// GetPipelineStateVersion read above plus this value compare.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return entry.pipeline;
}
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/false);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
// Shape gate. Behind the memo probe deliberately: only a pipeline that was created
// successfully is ever memoized, so a program refused here can never be sitting in the
@@ -5242,6 +5254,24 @@ void main() {
return VK_NULL_HANDLE;
}
if (MG_Util::PipeStats::Enabled()) {
// THE payload-builder walk section 2.3.1 says only runs on a pipeline memo miss.
// Counted as a constant, and counted HERE rather than at the top of the walk:
// the list-topology primitive-restart refusal above returns VK_NULL_HANDLE after
// only ten of these reads have run, and a tally that fires before an early return
// is an OVER-count, which breaks the lower-bound contract every other tally keeps.
//
// The 15 are: the six capability reads (cull face, depth test, polygon offset
// fill, rasterizer discard, colour logic op, stencil test), the draw-FBO slot
// read that gates depth/stencil, the two stencil face states, the polygon mode,
// the min sample shading value, the patch vertex count, the depth mask, the depth
// func, and the second draw-FBO slot read below. The sample-shading CAPABILITY
// read is the one excluded: it sits behind && on m_sampleRateShadingFeatureEnabled
// and does not run on a device without the feature. The other conditional reads -
// the cull-mode ternary, the logic-op fetch, the two tessellation default-level
// reads - are excluded for the same reason, so this stays a LOWER bound.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15);
}
PipelineFactory::PipelineCreatePayload payload {
.programHash = programObj.hash,
.vertexInputHash = vertexLayoutHash,
@@ -5890,8 +5920,18 @@ void main() {
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
shadow.dynamicTailIsDefaultFbo == isDefaultFbo) {
// Gate 6 of section 2.3.1: one version read plus a four-integer compare.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return;
}
if (MG_Util::PipeStats::Enabled()) {
// The version read above and the bulk parameter fetch that builds the value key.
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/false);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2);
}
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
// Second-level VALUE gate: the version moved, but RenderState's version counts
// every parameter, most of which this tail never reads. Build the key over
@@ -6365,6 +6405,15 @@ void main() {
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
}
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount);
if (MG_Util::PipeStats::Enabled()) {
// The six accessor reads this function makes unconditionally on the path that
// reaches here: the draw program, the VAO, the draw-FBO slot, the pipeline
// state version, the texture bind generation and the sampling-resolution
// generation. The XFB-active probe is elided on a device without the feature
// and the parameter-block fetch only runs when the pipeline state version
// moved, so neither is counted (lower bound, as everywhere else).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 6);
}
return true;
}
@@ -6390,9 +6439,27 @@ void main() {
return false;
}
}
if (MG_Util::PipeStats::Enabled()) {
// THE per-draw denominator for Magma, plus the draw-program read above. Placed
// here rather than inside TrySetupDrawFastPath because the fast path has 27
// decline returns and one success return: counting the gate from the caller is
// the only shape that cannot miss one.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/true);
}
return true;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/false);
// The three reads the full path makes immediately below (draw FBO, VAO,
// program). The accessor reads the declined fast path had already made before
// it turned back are NOT counted.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {
@@ -11133,16 +11200,66 @@ void main() {
}
}
// Keyed on the frontend's never-reused lifetime id, NOT on the GL name. The name is
// recycled the moment glDeleteTransformFeedbacks gives it back, so a name-keyed slot
// handed a brand-new object the counter group - and the m_xfbCountersValid /
// m_xfbLastSeenGeneration entries - of the object that died under that name.
//
// Slots are never handed back (there is no backend entry telling this renderer that a span
// closed - registering the EndTransformFeedback one would flip the "captures through its own
// driver" test FixupGsStripCaptureOrder makes of it), so once all sixteen are owned a new
// object has to take one over. The victim is chosen among owners with NO OPEN SPAN: an object
// whose span is closed, or which no longer exists at all, can never resume, so its counter
// bytes are dead. Least-recently-used ALONE would be exactly the wrong rule - GL only permits
// another object to capture while this one is PAUSED, so the paused span whose counters the
// slots exist to protect is by construction the least recently used entry. Taking a group over
// resets its counter state, because those bytes describe the previous owner's span.
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();
const auto it = m_xfbCounterSlotByObject.find(name);
if (it != m_xfbCounterSlotByObject.end()) {
return it->second;
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
const Uint64 identity = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
MOBILEGL_ASSERT(identity != 0,
"transform feedback object reported the free-slot sentinel (0) as its identity - "
"every slot would then read as 'mine' without ever being claimed");
Uint32 freeSlot = kNoSlot;
for (Uint32 slot = 0; slot < kNoSlot; ++slot) {
if (m_xfbCounterSlotOwner[slot] == identity) {
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
return slot;
}
if (m_xfbCounterSlotOwner[slot] == 0 && freeSlot == kNoSlot) {
freeSlot = slot;
}
}
// Past the tracked set every object shares slot group 0. Only concurrently-paused
// spans need distinct groups, and applications do not keep sixteen of those open.
const Uint32 slot = m_xfbNextCounterSlot < kXfbCounterObjectSlots ? m_xfbNextCounterSlot++ : 0;
m_xfbCounterSlotByObject[name] = slot;
Uint32 slot = freeSlot;
if (slot == kNoSlot) {
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
if (MG_State::pGLContext->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
continue;
}
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
if (slot == kNoSlot) {
// Sixteen capture spans open at once. Whatever is taken loses its resume offset and
// restarts at byte 0 of its capture buffers, which is a wrong picture rather than a
// slow one - hence a report rather than a silent choice.
MGLOG_E_ONCE("CurrentXfbCounterSlot: all %zu counter groups belong to transform feedback objects "
"with an open capture span; the least recently used one is taken over and that span "
"will restart at offset 0 instead of appending",
kXfbCounterObjectSlots);
slot = 0;
for (Uint32 candidate = 1; candidate < kNoSlot; ++candidate) {
if (m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
m_xfbCounterSlotOwner[slot] = identity;
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
m_xfbCountersValid[slot] = false;
m_xfbLastSeenGeneration[slot] = 0;
return slot;
}
@@ -675,8 +675,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{};
@@ -51,6 +51,7 @@ endif()
add_executable(MobileGLIntegrationTest
Main.cpp
Harness/HeadlessGL.cpp
Harness/BackendCapsPeek.cpp
Scenarios/OrientationScenario.cpp
Scenarios/CrossFrameBufferScenario.cpp
Scenarios/ResidentIndexScenario.cpp
@@ -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
@@ -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
+106
View File
@@ -0,0 +1,106 @@
// 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) \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers once the */ \
/* inventory carries the target argument (P1). Named for the plan's explicit */ \
/* replacement of the DrawIndirect/Parameter pair. */ \
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) \
/* Not in ComputePipelineStateHash today even though Vulkan makes it pipeline */ \
/* state; recorded here so the G7 chunk table has to answer for it before it */ \
/* freezes (section 10.3-5). */ \
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)
// 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)
// clang-format on
+93
View File
@@ -0,0 +1,93 @@
// 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). Generated by G7 from the field list ComputePipelineStateHash already hashes;
// MGPipeRenderStateSpans.cpp and the setter-consistency test land with P2, which is
// when the chunk table can be filled with real offsets.
struct MGPipeRenderStateSpans;
// 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"
// 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
+61
View File
@@ -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
+98
View File
@@ -0,0 +1,98 @@
// 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.
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
+57
View File
@@ -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
+806
View File
@@ -0,0 +1,806 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeTypes.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 "MGPipeHostSpan.h"
// Every MGPipe payload (plan B section 4.5). Each one is a flat POD with explicit padding,
// carries a static_assert on trivial copyability and one on its exact size, and never
// contains a pointer: MGHostSpan, the one shape that changes with the transport, only ever
// rides in a variable tail (draw_vbo's user indices, set_shader_buffers' named-UBO bytes),
// never inline in a fixed payload.
//
// Sizes are asserted rather than merely documented because the wire records generated from
// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a
// protocol break that no test would otherwise see.
//
// P0.5 DEBT, recorded here so it is impossible to miss: two payloads reach into headers
// this directory is eventually forbidden to see - MGPCaps embeds MG_Backend's
// DynamicBackendParameters, and ResidualValueBlock embeds MG_State's RenderStateParameters
// and PixelStoreParameters. Both are deliberate: the caps block IS that struct (section
// 4.4.1) and the residual block is the migration carrier for Track V (section 6.3). P0.5
// extracts MGPipeValueTypes.h and both includes below go away; until then purity gate A
// (section 10.3) cannot be armed for this header.
#include <MG_Backend/BackendObject.h>
#include <MG_State/GLState/RenderState/RenderState.h>
namespace MobileGL::MG_Pipe {
using MG_Backend::DynamicBackendParameters;
// Both live directly in namespace MobileGL today; P0.5 moves them into
// MG_Pipe/MGPipeValueTypes.h.
using MobileGL::PixelStoreParameters;
using MobileGL::RenderStateParameters;
// A payload must be memcpy-able and its size must be an exact, stated number.
#define MGP_ASSERT_POD(T, Size) \
static_assert(std::is_trivially_copyable_v<T>, #T " must be trivially copyable"); \
static_assert(sizeof(T) == (Size), #T " changed size; update the wire format and this assertion")
// ---------------------------------------------------------------------------------
// Shared primitives
// ---------------------------------------------------------------------------------
// A run of bytes in the command stream's blob area. Monolith: Seg is
// kMGHostSpanSegNone and Offset is an address into the caller's staging arena. Split:
// Seg names a transport segment.
struct MGPBlobRef {
Uint64 Offset;
Uint64 Size;
Uint32 Seg;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPBlobRef, 24);
struct MGPRange {
Uint64 Offset;
Uint64 Size;
};
MGP_ASSERT_POD(MGPRange, 16);
// Destination box in the level's own coordinate system (section 4.5.6).
struct MGPBox {
Int32 X, Y, Z;
Uint32 W, H, D;
};
MGP_ASSERT_POD(MGPBox, 24);
// Where an asynchronous answer lands. Every server query in this catalogue is
// async-with-handle; none of them blocks (section 4.4.6, "the total rule").
struct MGPReplySlot {
Uint64 Id;
};
MGP_ASSERT_POD(MGPReplySlot, 8);
// One contiguous run of RenderStateParameters bytes. The pipeline/dynamic split is
// defined exactly once, in MGPipeRenderStateSpans, and generated by G7 from the field
// list VulkanRenderer::ComputePipelineStateHash already hashes (section 4.5.2).
struct MGPStateChunk {
Uint16 Offset;
Uint16 Length;
};
MGP_ASSERT_POD(MGPStateChunk, 4);
// The payload of every call that carries nothing but an object identity.
struct MGPHandleOnly {
MGPipeHandle Handle;
Uint32 Kind; // MGPipeKind, widened for a stable wire size
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPHandleOnly, 16);
// ---------------------------------------------------------------------------------
// Screen: caps, resources, fences
// ---------------------------------------------------------------------------------
// Capability bits that replace "is this table slot null" as an implicit feature probe
// (section 4.4.1). The five ownership-switch bits of v1 are deliberately absent: what
// they tried to express - who performs primitive-restart rewriting and multi-draw
// flattening - is not expressible as a capability (D-B7).
enum MGPCapBit : Uint64 {
kCapNone = 0,
kCapViewportArray = 1ull << 0,
kCapFloat64VertexAttrib = 1ull << 1,
kCapResidentSubData = 1ull << 2,
kCapCpuXfbPrimitiveAccounting = 1ull << 3,
kCapTimerQuery = 1ull << 4,
kCapOcclusionQuery = 1ull << 5,
kCapXfbPrimitivesQuery = 1ull << 6,
// The server rewrites restart indices / flattens multi-draws itself and therefore
// needs the index bytes on its side: under split this arms the index host mirror
// (D-B7).
kCapNeedsHostIndexBytes = 1ull << 7,
// The server packs named uniform blocks into its own ring and therefore needs the
// host bytes of a set_shader_buffers(Uniform) range (D-B8).
kCapNeedsHostUboBytes = 1ull << 8,
};
struct MGPCaps {
// The ~90 flat scalars the backends already publish, by inclusion rather than by
// restatement: a caps field added there must not need a second edit here. This is
// also where the six per-axis compute limits (MaxComputeWorkGroupCount/Size) ride -
// the only indexed answers the device owns, and therefore the only ones that outlive
// the GetIntegeri_v table entry (see the PipeCalls.def footer).
DynamicBackendParameters Dynamic;
Uint64 CallMask; // MGPCapBit
// The two halves that are not flat PODs travel as blobs: the format capability
// cache holds Vector<Int> sample-count lists, and the renderer strings are
// Strings. Their serializers land with the transport (P5).
MGPBlobRef FormatCapabilities;
MGPBlobRef RendererInfo;
};
static_assert(std::is_trivially_copyable_v<MGPCaps>, "MGPCaps must be trivially copyable");
// Stated as a COMPOSITION rather than a literal: DynamicBackendParameters still carries
// SizeT fields, so its literal size is ABI-dependent until P0.5 moves the caps block
// into MGPipeValueTypes.h with fixed-width members. The assertion still fires on any
// padding introduced between the members below.
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
"MGPCaps gained padding or a member; update the wire format");
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
// share one create/respecify shape (section 4.5.1).
struct MGPResourceDesc {
MGPipeHandle Resource;
Uint8 Target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
Uint16 BindMask;
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
Uint32 Width, Height, Depth;
Uint16 ArrayLayers, Levels, Samples;
Uint8 FixedSampleLocations, Immutable;
Uint32 Usage; // BufferUsage
Uint32 StorageFlags; // glBufferStorage flags
Uint8 HasDefinedContent; // false after a NULL-data respecify
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
Uint16 Pad0;
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
// cannot hold one.
Uint32 GlNameForDiag;
Uint32 Pad1;
MGPipeHandle ViewOf; // storage owner for a texture view
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
};
MGP_ASSERT_POD(MGPResourceDesc, 88);
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
struct MGPFenceWait {
MGPipeHandle Fence;
Uint64 TimeoutNs;
};
MGP_ASSERT_POD(MGPFenceWait, 16);
struct MGPQueryDesc {
MGPipeHandle Query;
Uint32 Kind; // GL query target
Uint32 Stream; // indexed query stream, 0 otherwise
};
MGP_ASSERT_POD(MGPQueryDesc, 16);
struct MGPQueryResultRequest {
MGPipeHandle Query;
Uint8 Wait; // the two-value contract of GetSyncStatus is preserved verbatim
Uint8 Pad0[3];
Uint32 Pad1;
};
MGP_ASSERT_POD(MGPQueryResultRequest, 16);
// query_timestamp: glGetInteger64v(GL_TIMESTAMP), the synchronous "what time is it on the
// GPU" GLFunctionsTable::GetGpuTimestampNs answers today. The request names nothing; the
// Int64 nanosecond stamp comes back through the reply slot.
struct MGPTimestampRequest {
Uint32 Reserved;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPTimestampRequest, 8);
// ---------------------------------------------------------------------------------
// CSOs
// ---------------------------------------------------------------------------------
// create_render_state carries ONLY the pipeline subset's chunk bytes. chunkMask lets an
// incremental create send just the chunks that moved, against baseCso (section 4.5.2).
struct MGPRenderStateDesc {
MGPipeHandle Cso;
MGPipeHandle BaseCso;
Uint32 ChunkMask; // all ones for a brand new CSO
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPRenderStateDesc, 48);
// Steady state: 12 bytes on the wire, no hashing, no blob.
struct MGPBindRenderState {
MGPipeHandle Cso;
Uint16 Version;
Uint16 PipelineVersion;
};
MGP_ASSERT_POD(MGPBindRenderState, 12);
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
// values, sample coverage, hints and the point-size family. This is what keeps
// glViewport from evicting Magma's pipeline memo (D-B1).
struct MGPDynamicState {
Uint32 ChunkMask;
Uint16 Version;
Uint16 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPDynamicState, 32);
// Both views travel, and neither is derivable from the other: the resolved
// VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means
// "element size" while a binding-model stride of 0 means "every vertex reads the same
// element" (section 4.5.3). IsLong and Type == Float64 are carried separately.
struct MGPVertexElements {
MGPipeHandle Cso;
Uint32 AttributeCount;
Uint32 BindingPointCount;
MGPBlobRef Blob; // VertexAttribute[] followed by VertexBufferBindingPoint[]
};
MGP_ASSERT_POD(MGPVertexElements, 40);
// SamplerParameters crosses byte for byte INCLUDING borderColorForm: without it the
// backend cannot choose between glSamplerParameterIiv and fv, or between the
// VkBorderColor families, because all three representations are always numerically
// populated (section 4.5.4). Carried as a blob until P0.5 gives it a value header.
struct MGPSamplerDesc {
MGPipeHandle Cso;
MGPBlobRef Parameters;
};
MGP_ASSERT_POD(MGPSamplerDesc, 32);
// = pipe_sampler_view, and ONLY the view restrictions. Everything a glTexParameter
// writes lives on set_texture_params instead, because a texture that is only an FBO
// attachment, only an image binding or only a glCopyImageSubData endpoint has no
// sampler view to hang it on (section 4.4.3).
struct MGPSamplerView {
MGPipeHandle Cso;
MGPipeHandle Texture;
Uint32 InternalFormat; // aliasing format for glTextureView
Uint8 Target;
Uint8 Pad0[3];
Uint16 MinLevel, NumLevels, MinLayer, NumLayers;
Uint16 Samples;
Uint8 FixedSampleLocations;
Uint8 Pad1;
};
MGP_ASSERT_POD(MGPSamplerView, 36);
// Per texture OBJECT, independent of any view.
struct MGPTextureParams {
MGPipeHandle Res;
Uint16 BaseLevel, MaxLevel;
Uint8 Swizzle[4];
Uint8 DepthStencilMode;
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
// override that the frontend params version does not move for.
Uint8 ForceResync;
Uint8 Pad0[2];
Float MinLod, MaxLod, LodBias;
};
MGP_ASSERT_POD(MGPTextureParams, 32);
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
// deserialize into them without dragging in glslang (section 4.5.5).
struct MGPProgramDesc {
MGPipeHandle Cso;
Uint32 StageMask; // == GetLinkedShaderStages()
Uint32 GlobalUboSize;
Uint32 ReservedNumSamplesOffset;
Uint8 SpirvStatus;
Uint8 NativeFloat64;
Uint8 PointSizeDemoted;
Uint8 EnableSpirvValidation;
MGPBlobRef Spirv[6]; // per stage
MGPBlobRef Reflection;
};
MGP_ASSERT_POD(MGPProgramDesc, 192);
// ---------------------------------------------------------------------------------
// set_*
// ---------------------------------------------------------------------------------
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
// push time with no lookup (section 4.5.6).
struct MGPSurface {
MGPipeHandle Res;
Uint32 InternalFormat;
Uint8 Kind; // Texture | Renderbuffer | None
Uint8 Layered;
Uint16 Level;
Uint32 Layer;
Uint16 UploadTarget;
Uint16 Pad0;
};
MGP_ASSERT_POD(MGPSurface, 24);
struct MGPFramebufferState {
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
MGPSurface Color[8];
MGPSurface Depth, Stencil;
// The RESOLVED read surface, not an index. This is what structurally closes the
// read-buffer-shared-FBO defect class.
MGPSurface ReadSurface;
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
Uint16 Width, Height, Layers, Samples;
Uint8 FixedSampleLocations, IsDefault, Complete, Pad0;
Uint32 Pad1;
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
// emission suppressor - an unchanged hash means this record is not sent at all.
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
// redundant glBindSampler traffic reappears as a variable-length record per batch.
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPFramebufferState, 304);
struct MGPVertexBuffer {
MGPipeHandle Res;
Uint64 Offset;
Uint32 Stride;
Uint32 Divisor;
Uint32 BindingIndex;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPVertexBuffer, 32);
// Var-tail header: MGPVertexBuffer[Count] follows.
struct MGPVertexBuffers {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPVertexBuffers, 16);
// An independent call, NOT a subset of the VAO configuration version (D5).
struct MGPIndexBuffer {
MGPipeHandle Res;
Uint64 Offset;
Uint32 IndexSize;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPIndexBuffer, 24);
struct MGPIndirectBuffers {
MGPipeHandle DrawIndirect;
MGPipeHandle Parameter;
};
MGP_ASSERT_POD(MGPIndirectBuffers, 16);
// One entry of set_sampler_views. No stage dimension: MobileGL's texture unit space is
// MERGED (TextureState::m_textureUnits is one Array of MAX_TEXTURE_IMAGE_UNITS = 192),
// and the same unit may be sampled from two stages (section 4.4.3).
struct MGPBoundView {
MGPipeHandle View;
MGPipeHandle Texture;
Uint32 Unit;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPBoundView, 24);
struct MGPSamplerViews {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPSamplerViews, 16);
// Var-tail header: MGPipeHandle[Count] of sampler CSOs follows.
struct MGPSamplerStates {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPSamplerStates, 16);
struct MGPImageView {
MGPipeHandle Res;
Uint32 Unit;
Uint32 InternalFormat;
Uint32 Layer;
Uint16 Level;
Uint8 Layered;
Uint8 Access;
};
MGP_ASSERT_POD(MGPImageView, 24);
struct MGPShaderImages {
Uint32 Start, Count;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPShaderImages, 16);
// One bound buffer range: 24 bytes, no inline host span. The named-UBO host bytes a
// backend needs under kCapNeedsHostUboBytes (D-B8) travel as an OPTIONAL second var-tail,
// MGHostSpan[HostSpanCount] behind the ranges, announced by MGPShaderBuffers below. An
// inline span would have cost every SSBO, atomic-counter and XFB range 32 dead bytes, and
// D-B8 says not to freeze that payload's shape before the stage-ubo-named counter has
// produced numbers.
struct MGPBufferRange {
MGPipeHandle Res;
Uint64 Offset;
Uint64 Size;
};
MGP_ASSERT_POD(MGPBufferRange, 24);
// Var-tail header: MGPBufferRange[Count], then MGHostSpan[HostSpanCount]. HostSpanCount is
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (a range with nothing to
// ship carries an empty span, so the two arrays stay index-aligned).
struct MGPShaderBuffers {
Uint32 Class; // Uniform | ShaderStorage | AtomicCounter
Uint32 Start;
Uint32 Count;
Uint32 WritableMask;
Uint32 HostSpanCount; // 0, or Count when the kHostSpan tail is present (D-B8)
Uint32 Pad0;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPShaderBuffers, 32);
// Var-tail header: MGPBufferRange[Count] then Uint32 offsets[Count].
struct MGPStreamOutputTargets {
Uint32 Count;
Uint32 Pad0;
Uint64 Generation;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPStreamOutputTargets, 24);
// Covers the DEFAULT UNIFORM BLOCK only (D6).
struct MGPGlobalConstants {
MGPipeHandle ShaderCso;
Uint32 Version;
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPGlobalConstants, 40);
// The float/int/uint view is resolved on the CLIENT by ClassifyVertexAttribType.
struct MGPAttribValue {
Uint32 Location;
Uint8 ValueClass; // Float | Int | Uint | Double
Uint8 Pad0[3];
Uint32 Data[4];
};
MGP_ASSERT_POD(MGPAttribValue, 24);
// Var-tail header: MGPAttribValue[popcount(Mask)] follows.
struct MGPVertexAttribDefaults {
Uint32 Mask;
Uint32 Count;
};
MGP_ASSERT_POD(MGPVertexAttribDefaults, 8);
// PACK only. There is deliberately no unpack counterpart: nothing on the far side of
// the boundary reads unpack state (section 4.6 D5), and the staged-repack upload path
// does not even issue glPixelStorei.
struct MGPPixelPackState {
PixelStoreParameters Pack;
};
static_assert(std::is_trivially_copyable_v<MGPPixelPackState>);
// 28 is what PixelStoreParameters measures: two Bools, two bytes of padding, six Ints.
// Asserting against sizeof(PixelStoreParameters) itself was a tautology that could not
// notice the value struct changing width under the wire format.
static_assert(sizeof(MGPPixelPackState) == 28,
"MGPPixelPackState changed size; update the wire format and this assertion");
// Also a shader-variant input: both backends bake these into the synthesized
// pass-through control stage.
struct MGPPatchState {
Uint32 Vertices;
Uint32 Pad0;
Float Outer[4];
Float Inner[2];
Uint32 Pad1[2];
};
MGP_ASSERT_POD(MGPPatchState, 40);
// Migration-only (section 6.3). Every stage removes fields and lowers
// MGL_RESIDUAL_BLOCK_SIZE; P13 asserts it is zero, which is the retirement trip wire.
//
// Layout must be asserted MEMBER BY MEMBER, not only by sizeof: a heterogeneous POD
// union is where padding differs across ABIs, and the monolith verify harness is blind
// to it because both sides are the same translation unit. G3 emits the offsetof
// assertions; under split the block is serialized field-wise rather than memcpy'd.
struct ResidualValueBlock {
RenderStateParameters RenderState; // until create/bind_render_state + set_dynamic_state land
PixelStoreParameters Pack; // until set_pixel_pack_state lands
Uint64 CapabilityBits;
Uint32 PatchVertices;
Uint32 Pad0;
Float PatchOuter[4];
Float PatchInner[2];
Uint32 Pad1[2];
};
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
// set_* call deletes fields here and lowers it, and P13 replaces it with
// static_assert(sizeof(ResidualValueBlock) == 0), which stays red until the last field is
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
// break - which is the point.
//
// Stable across the ABIs MobileGL ships on: every member of RenderStateParameters and
// PixelStoreParameters is a fixed-width scalar or an array of one, with no pointer and no
// SizeT.
#define MGL_RESIDUAL_BLOCK_SIZE 1248
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
"retired, and do not raise it");
struct MGPResidualValueState {
Uint32 Version;
Uint32 Pad0;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPResidualValueState, 32);
// ---------------------------------------------------------------------------------
// Transfer
// ---------------------------------------------------------------------------------
// Shape copied from the unpack ring's existing UnpackStagingBlock. The source strides
// are CARRIED, not inferred from a pointer comparison: the old
// `uploadData == mipData` test cannot survive a split, where the client neither ships
// the whole level nor keeps a server-side mirror of it (section 4.5.6).
struct MGPSubRegion {
Int32 X, Y, Z;
Uint32 W, H, D;
Uint64 SrcOffset; // into the blob
Uint32 SrcRowStride; // bytes; 0 = tightly packed (w * bpp)
Uint32 SrcSliceStride; // bytes; 0 = tightly packed
};
MGP_ASSERT_POD(MGPSubRegion, 40);
// Carries the union box AND the region list so the SERVER picks the upload shape - the
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
// JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame.
//
// THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination
// byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0, and
// Blob holds exactly Size source bytes. That caps ONE record at a 2^31-1 offset and a
// 2^32-1 size; a range beyond either is split by the emitter - the same rule, and at
// SEG_STAGE's 32 MiB the far tighter one, that the ring's half-capacity bound already
// imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are
// the only spelling of this convention; nothing else reads the box for a buffer.
struct MGPSubData {
MGPipeHandle Res;
Uint16 Target, Level;
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
// bytes an untransformed level shadow?
Uint8 SourceIsVerbatimLevelShadow;
Uint8 Pad0[3];
MGPBox UnionBox;
Uint32 RegionCount; // MGPSubRegion[] in the variable tail
Uint32 Pad1;
MGPBlobRef Blob;
};
MGP_ASSERT_POD(MGPSubData, 72);
// Encodes a buffer byte range into the record's box. False, with the record untouched,
// when the range does not fit one record: the emitter has to split it.
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
if (offset > 0x7FFFFFFFull || size > 0xFFFFFFFFull) {
return false;
}
record.UnionBox = MGPBox{static_cast<Int32>(offset), 0, 0, static_cast<Uint32>(size), 1, 1};
record.Level = 0;
record.RegionCount = 0;
return true;
}
inline Uint64 MGPipeSubDataBufferOffset(const MGPSubData& record) {
// A negative X is a corrupt record (the encoder never writes one); read as unsigned
// it lands above the encodable bound, which the applier's bounds gate refuses.
return static_cast<Uint64>(static_cast<Uint32>(record.UnionBox.X));
}
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
// zero regions - that is how a pull that needs nothing is answered.
struct MGPSubDataComplete {
MGPipeHandle Res;
Uint16 Target, FirstLevel, LevelCount, Pad0;
Uint64 PullSerial;
};
MGP_ASSERT_POD(MGPSubDataComplete, 24);
// Carries the application's REAL access flags, not a normalized subset.
struct MGPFlushRange {
MGPipeHandle Res;
Uint64 Offset, Size;
Uint32 AccessFlags; // Flags<BufferMappingAccessBit>
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPFlushRange, 32);
struct MGPReadback {
MGPipeHandle Res;
Uint64 Offset, Size;
};
MGP_ASSERT_POD(MGPReadback, 24);
struct MGPCopyRegion {
MGPipeHandle Src, Dst;
MGPBox SrcBox;
Int32 DstX, DstY, DstZ;
Uint16 SrcTarget, DstTarget;
Uint16 SrcLevel, DstLevel;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPCopyRegion, 64);
struct MGPBlit {
MGPipeHandle ReadFbo, DrawFbo;
Int32 SrcX0, SrcY0, SrcX1, SrcY1;
Int32 DstX0, DstY0, DstX1, DstY1;
Uint32 Mask;
Uint32 Filter;
};
MGP_ASSERT_POD(MGPBlit, 56);
// One discriminated record replacing glClear, the four glClearBuffer* and the four
// glClearNamedFramebuffer* entry points (section 4.4.4).
struct MGPClear {
MGPipeHandle Fbo;
Uint32 Kind; // Whole | Color | Depth | Stencil | DepthStencil
Int32 DrawBufferIndex;
Uint32 BufferMask; // GL_COLOR_BUFFER_BIT etc. for the whole-framebuffer form
Uint32 ValueClass; // Float | Int | Uint
Uint32 ColorValue[4];
Float DepthValue;
Int32 StencilValue;
};
MGP_ASSERT_POD(MGPClear, 48);
struct MGPMipPlan {
MGPipeHandle Res;
Uint16 Target, BaseLevel, LevelCount, Pad0;
};
MGP_ASSERT_POD(MGPMipPlan, 16);
// read_pixels and get_texture_image share one shape; both answer into a reply slot.
struct MGPReadbackInfo {
MGPipeHandle Res; // null for read_pixels: the bound read surface answers
MGPBox Box;
Uint32 Format, Type;
Uint16 Target, Level;
Uint32 Pad0;
Uint64 DstOffset, DstSize;
};
MGP_ASSERT_POD(MGPReadbackInfo, 64);
// ---------------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------------
enum MGPDrawFlagBit : Uint8 {
kDrawHasUserIndices = 1u << 0,
kDrawPrimitiveRestart = 1u << 1,
kDrawIndicesAreClient = 1u << 2,
kDrawHasIndexRange = 1u << 3,
kDrawHasXfbCount = 1u << 4,
};
// = pipe_draw_info. Today's twenty draw entry points collapse onto this one call, with
// MGPDrawRange[] holding exactly the shape the glMultiDraw* family already has.
//
// minIndex/maxIndex are computed only on the client-memory array path today, and
// xfbCpuCapturedVertices only on the XFB scatter path, so Flags gates the WORK. They
// stay in the fixed head; moving them into the variable tail is a wire-format decision
// that belongs with the transport (P5), where per-draw byte histograms exist to size
// it. userIndices is in the variable tail already, so the VBO path - every Minecraft
// and Sodium draw - never pays the 32 bytes of an MGHostSpan.
struct MGPDrawInfo {
Uint32 Mode;
Uint8 IndexSize; // 0 = arrays, else 1 / 2 / 4
Uint8 Flags; // MGPDrawFlagBit
Uint16 Pad0;
Uint32 InstanceCount, StartInstance;
Uint32 RestartIndex;
Uint32 DrawIdOffset;
MGPipeHandle IndexResource;
Uint32 MinIndex, MaxIndex; // ~0 = unknown
Uint64 XfbCpuCapturedVertices;
Uint32 NumDraws; // MGPDrawRange[] in the variable tail
Uint32 Pad1;
};
MGP_ASSERT_POD(MGPDrawInfo, 56);
// = pipe_draw_start_count_bias.
struct MGPDrawRange {
Uint32 Start, Count;
Int32 IndexBias;
};
MGP_ASSERT_POD(MGPDrawRange, 12);
// Present when the draw is indirect. The client resolves the COUNT itself, so the
// server never reads an indirect command block to learn how many draws there are.
struct MGPDrawIndirect {
MGPipeHandle Buffer;
MGPipeHandle ParameterBuffer;
Uint64 Offset, ParameterOffset;
Uint32 Stride, DrawCount;
};
MGP_ASSERT_POD(MGPDrawIndirect, 40);
struct MGPGridInfo {
Uint32 GridX, GridY, GridZ;
Uint32 BlockX, BlockY, BlockZ;
MGPipeHandle IndirectBuffer;
Uint64 IndirectOffset;
Uint8 IsIndirect;
Uint8 Pad0[7];
};
MGP_ASSERT_POD(MGPGridInfo, 48);
struct MGPMemoryBarrier {
Uint32 Bits; // GLbitfield
Uint8 ByRegion;
Uint8 Pad0[3];
};
MGP_ASSERT_POD(MGPMemoryBarrier, 8);
struct MGPStreamOutputBegin {
Uint32 PrimitiveMode;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPStreamOutputBegin, 8);
// end_stream_output carries the accounting the client owns; the scatter itself is a
// read-modify-write of the client's shadow and lives there (section 7.2.1).
struct MGPXfbAccounting {
Uint64 CapturedVertices;
Uint64 PrimitivesWritten;
Uint32 PrimitiveMode;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPXfbAccounting, 24);
struct MGPStreamOutputControl {
Uint32 Reserved;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPStreamOutputControl, 8);
struct MGPFlush {
Uint32 Flags;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPFlush, 8);
struct MGPPresent {
Uint64 FrameSerial;
};
MGP_ASSERT_POD(MGPPresent, 8);
struct MGPSwapInterval {
Int32 Interval;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPSwapInterval, 8);
// ---------------------------------------------------------------------------------
// Reverse channel payloads (section 7.1)
// ---------------------------------------------------------------------------------
struct MGPSurfaceInfo {
Uint32 Width, Height;
Uint32 InternalFormat;
Uint16 Samples, Layers;
Uint8 IsDefault;
Uint8 Pad0[7];
};
MGP_ASSERT_POD(MGPSurfaceInfo, 24);
#undef MGP_ASSERT_POD
} // namespace MobileGL::MG_Pipe
+177
View File
@@ -0,0 +1,177 @@
// 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
//
// 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, kNone) \
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).
+238
View File
@@ -0,0 +1,238 @@
// 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. Adding a field to a payload without adding it
// here makes the comparator blind to it; that gap closes in P1, when the verify harness
// goes live and the comparator's coverage is itself asserted.
//
// 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)
#define MGP_FIELDS_MGPTextureParams(F) \
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) 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)
#define MGP_FIELDS_MGPSurface(F) \
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
#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(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(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)
#define MGP_FIELDS_ResidualValueBlock(F) \
F(RenderState) F(Pack) F(CapabilityBits) F(PatchVertices) F(PatchOuter) F(PatchInner)
#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)
// 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)
// clang-format on
+108
View File
@@ -0,0 +1,108 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeCoverage.inc
// 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
// G6: backend read inventory -> MGPipe call coverage.
//
// GENERATED by scripts/gen_pipe.py from Coverage.def and scripts/data/backend_read_inventory.md - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// The acceptance rule (plan B section 10.3-5): regenerate, `git diff --exit-code`, and
// ZERO unmapped rows. P0 permits unmapped rows and only counts them; the count below is
// the number the later gate has to drive to zero.
//
// Three pseudo-calls stand for read points that never become a forward record:
// kClientResolved (the frontend answers it), kReverseChannel (it becomes one of the ten
// MGPipeCallbacks) and kStructuralHandle (the row is a signature carrying a
// SharedPtr<MG_State...> that becomes an MGPipeHandle parameter).
struct MGPipeCoverageEntry {
const char* Accessor;
const char* Call;
Uint32 ReadPoints;
};
inline constexpr MGPipeCoverageEntry kMGPipeCoverage[] = {
{"Buffer ops delta", "ResourceRespecify", 17},
{"GetActiveTextureUnit", "SetSamplerViews", 8},
{"GetBlendColor", "SetDynamicState", 1},
{"GetBlendEquationIndexed", "CreateRenderState", 1},
{"GetBlendFuncIndexed", "CreateRenderState", 1},
{"GetBoundTransformFeedbackName", "SetStreamOutputTargets", 1},
{"GetBoundVertexArray", "BindVertexElements", 12},
{"GetBufferBindingPoint", "SetShaderBuffers", 19},
{"GetBufferBindingPointCount", "SetShaderBuffers", 3},
{"GetBufferBindingSlot", "SetIndirectBuffers", 29},
{"GetClampReadColor", "SetDynamicState", 1},
{"GetClearColor", "SetDynamicState", 1},
{"GetClearDepth", "SetDynamicState", 1},
{"GetClearStencil", "SetDynamicState", 1},
{"GetColorMaskIndexed", "CreateRenderState", 6},
{"GetCullFaceMode", "CreateRenderState", 1},
{"GetCurrentVertexAttribute", "SetVertexAttribDefaults", 2},
{"GetDepthFunc", "CreateRenderState", 1},
{"GetDepthMask", "CreateRenderState", 5},
{"GetDepthRangeIndexed", "SetDynamicState", 1},
{"GetFramebufferBindingSlot", "SetFramebufferState", 19},
{"GetImageTextureBinding", "SetShaderImages", 14},
{"GetLineWidth", "SetDynamicState", 1},
{"GetLogicOp", "CreateRenderState", 1},
{"GetMaxTouchedTextureUnit", "SetSamplerViews", 1},
{"GetMinSampleShadingValue", "CreateRenderState", 1},
{"GetPatchDefaultInnerLevel", "SetPatchState", 3},
{"GetPatchDefaultOuterLevel", "SetPatchState", 3},
{"GetPatchVertices", "SetPatchState", 3},
{"GetPipelineStateVersion", "BindRenderState", 3},
{"GetPixelStoreParameters", "SetPixelPackState", 6},
{"GetPolygonModeFront", "CreateRenderState", 1},
{"GetPolygonOffsetFactor", "SetDynamicState", 1},
{"GetPolygonOffsetUnits", "SetDynamicState", 1},
{"GetPrimitiveRestartIndex", "DrawVbo", 3},
{"GetProgramForDispatch", "SetDispatchProgram", 3},
{"GetProgramForDraw", "SetDrawProgram", 7},
{"GetProgramObject", "CreateShaderState", 3},
{"GetProvokingVertexMode", "CreateRenderState", 1},
{"GetRenderStateParameters", "CreateRenderState", 11},
{"GetRenderStateParametersVersion", "BindRenderState", 2},
{"GetSamplingResolutionGeneration", "SetSamplerViews", 9},
{"GetScissorBox", "SetDynamicState", 3},
{"GetStencilState", "CreateRenderState", 8},
{"GetTextureBindGeneration", "SetSamplerViews", 5},
{"GetTextureContextId", "SetSamplerViews", 6},
{"GetTextureObject", "SetSamplerViews", 1},
{"GetTextureUnitObject", "SetSamplerViews", 19},
{"GetTouchedBufferBindingPointCount", "SetShaderBuffers", 2},
{"GetTransformFeedbackCapturedVertices", "DrawVbo", 1},
{"GetTransformFeedbackGeneration", "SetStreamOutputTargets", 1},
{"GetTransformFeedbackPausedPrimitiveCounter", "EndStreamOutput", 2},
{"GetTransformFeedbackProgram", "SetStreamOutputTargets", 3},
{"GetViewport", "SetDynamicState", 1},
{"GetViewportIndexed", "SetDynamicState", 1},
{"InvalidateCompileEnv", "kClientResolved", 2},
{"IsCapabilityEnabled", "CreateRenderState", 29},
{"IsCapabilityEnabledIndexed", "CreateRenderState", 1},
{"IsTransformFeedbackActive", "BeginStreamOutput", 5},
{"IsTransformFeedbackPaused", "PauseStreamOutput", 2},
{"RecordError", "kReverseChannel", 6},
{"ValidateProgramName", "kClientResolved", 3},
{"handle-ify (wire handle)", "kStructuralHandle", 167},
};
inline constexpr SizeT kMGPipeCoverageEntryCount = 63;
inline constexpr Uint32 kMGPipeInventoryReadPoints = 477;
inline constexpr Uint32 kMGPipeInventoryMappedToCall = 299;
inline constexpr Uint32 kMGPipeInventoryClientResolved = 5;
inline constexpr Uint32 kMGPipeInventoryReverseChannel = 6;
inline constexpr Uint32 kMGPipeInventoryStructuralHandle = 167;
inline constexpr Uint32 kMGPipeInventoryUnmapped = 0;
static_assert(kMGPipeCoverageEntryCount == sizeof(kMGPipeCoverage) / sizeof(kMGPipeCoverage[0]));
static_assert(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
kMGPipeInventoryUnmapped ==
kMGPipeInventoryReadPoints,
"every inventory row must land in exactly one bucket");
+308
View File
@@ -0,0 +1,308 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeFilled.inc
// 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
// G5: PipeInputs field ids and the per-verb poison generations.
//
// GENERATED by scripts/gen_pipe.py from Coverage.def and PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// One field id per GLContext accessor the backends actually read (plan B section 6.2:
// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is
// small and stable across the whole migration).
//
// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case:
// a field filled by the previous DRAW and then read by the glTexSubImage that follows is
// stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
enum class MGPipeInputField : Uint16 {
GetActiveTextureUnit,
GetBlendColor,
GetBlendEquationIndexed,
GetBlendFuncIndexed,
GetBoundTransformFeedbackName,
GetBoundVertexArray,
GetBufferBindingSlot,
GetBufferBindingPoint,
GetBufferBindingPointCount,
GetTouchedBufferBindingPointCount,
GetClampReadColor,
GetClearColor,
GetClearDepth,
GetClearStencil,
GetColorMaskIndexed,
GetCullFaceMode,
GetCurrentVertexAttribute,
GetDepthFunc,
GetDepthMask,
GetDepthRangeIndexed,
GetFramebufferBindingSlot,
GetImageTextureBinding,
GetLineWidth,
GetLogicOp,
GetMaxTouchedTextureUnit,
GetMinSampleShadingValue,
GetPatchDefaultInnerLevel,
GetPatchDefaultOuterLevel,
GetPatchVertices,
GetPipelineStateVersion,
GetPixelStoreParameters,
GetPolygonModeFront,
GetPolygonOffsetFactor,
GetPolygonOffsetUnits,
GetPrimitiveRestartIndex,
GetProgramForDispatch,
GetProgramForDraw,
GetProgramObject,
GetProvokingVertexMode,
GetRenderStateParameters,
GetRenderStateParametersVersion,
GetSamplingResolutionGeneration,
GetScissorBox,
GetStencilState,
GetTextureBindGeneration,
GetTextureContextId,
GetTextureObject,
GetTextureUnitObject,
GetTransformFeedbackCapturedVertices,
GetTransformFeedbackGeneration,
GetTransformFeedbackPausedPrimitiveCounter,
GetTransformFeedbackProgram,
GetViewport,
GetViewportIndexed,
IsCapabilityEnabled,
IsCapabilityEnabledIndexed,
IsTransformFeedbackActive,
IsTransformFeedbackPaused,
InvalidateCompileEnv,
ValidateProgramName,
RecordError,
kFieldCount,
};
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"GetActiveTextureUnit",
"GetBlendColor",
"GetBlendEquationIndexed",
"GetBlendFuncIndexed",
"GetBoundTransformFeedbackName",
"GetBoundVertexArray",
"GetBufferBindingSlot",
"GetBufferBindingPoint",
"GetBufferBindingPointCount",
"GetTouchedBufferBindingPointCount",
"GetClampReadColor",
"GetClearColor",
"GetClearDepth",
"GetClearStencil",
"GetColorMaskIndexed",
"GetCullFaceMode",
"GetCurrentVertexAttribute",
"GetDepthFunc",
"GetDepthMask",
"GetDepthRangeIndexed",
"GetFramebufferBindingSlot",
"GetImageTextureBinding",
"GetLineWidth",
"GetLogicOp",
"GetMaxTouchedTextureUnit",
"GetMinSampleShadingValue",
"GetPatchDefaultInnerLevel",
"GetPatchDefaultOuterLevel",
"GetPatchVertices",
"GetPipelineStateVersion",
"GetPixelStoreParameters",
"GetPolygonModeFront",
"GetPolygonOffsetFactor",
"GetPolygonOffsetUnits",
"GetPrimitiveRestartIndex",
"GetProgramForDispatch",
"GetProgramForDraw",
"GetProgramObject",
"GetProvokingVertexMode",
"GetRenderStateParameters",
"GetRenderStateParametersVersion",
"GetSamplingResolutionGeneration",
"GetScissorBox",
"GetStencilState",
"GetTextureBindGeneration",
"GetTextureContextId",
"GetTextureObject",
"GetTextureUnitObject",
"GetTransformFeedbackCapturedVertices",
"GetTransformFeedbackGeneration",
"GetTransformFeedbackPausedPrimitiveCounter",
"GetTransformFeedbackProgram",
"GetViewport",
"GetViewportIndexed",
"IsCapabilityEnabled",
"IsCapabilityEnabledIndexed",
"IsTransformFeedbackActive",
"IsTransformFeedbackPaused",
"InvalidateCompileEnv",
"ValidateProgramName",
"RecordError",
};
// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each
// true has to be argued for in P1 when the fillers land: a sticky field is a field
// the poison cannot protect.
inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetActiveTextureUnit
false, // GetBlendColor
false, // GetBlendEquationIndexed
false, // GetBlendFuncIndexed
false, // GetBoundTransformFeedbackName
false, // GetBoundVertexArray
false, // GetBufferBindingSlot
false, // GetBufferBindingPoint
false, // GetBufferBindingPointCount
false, // GetTouchedBufferBindingPointCount
false, // GetClampReadColor
false, // GetClearColor
false, // GetClearDepth
false, // GetClearStencil
false, // GetColorMaskIndexed
false, // GetCullFaceMode
false, // GetCurrentVertexAttribute
false, // GetDepthFunc
false, // GetDepthMask
false, // GetDepthRangeIndexed
false, // GetFramebufferBindingSlot
false, // GetImageTextureBinding
false, // GetLineWidth
false, // GetLogicOp
false, // GetMaxTouchedTextureUnit
false, // GetMinSampleShadingValue
false, // GetPatchDefaultInnerLevel
false, // GetPatchDefaultOuterLevel
false, // GetPatchVertices
false, // GetPipelineStateVersion
false, // GetPixelStoreParameters
false, // GetPolygonModeFront
false, // GetPolygonOffsetFactor
false, // GetPolygonOffsetUnits
false, // GetPrimitiveRestartIndex
false, // GetProgramForDispatch
false, // GetProgramForDraw
false, // GetProgramObject
false, // GetProvokingVertexMode
false, // GetRenderStateParameters
false, // GetRenderStateParametersVersion
false, // GetSamplingResolutionGeneration
false, // GetScissorBox
false, // GetStencilState
false, // GetTextureBindGeneration
false, // GetTextureContextId
false, // GetTextureObject
false, // GetTextureUnitObject
false, // GetTransformFeedbackCapturedVertices
false, // GetTransformFeedbackGeneration
false, // GetTransformFeedbackPausedPrimitiveCounter
false, // GetTransformFeedbackProgram
false, // GetViewport
false, // GetViewportIndexed
false, // IsCapabilityEnabled
false, // IsCapabilityEnabledIndexed
false, // IsTransformFeedbackActive
false, // IsTransformFeedbackPaused
false, // InvalidateCompileEnv
false, // ValidateProgramName
false, // RecordError
};
// Which call is expected to have filled a field by the time a verb reads it. Names
// come from Coverage.def, so this table and the coverage table cannot disagree.
inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] = {
"SetSamplerViews",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"SetStreamOutputTargets",
"BindVertexElements",
"SetIndirectBuffers",
"SetShaderBuffers",
"SetShaderBuffers",
"SetShaderBuffers",
"SetDynamicState",
"SetDynamicState",
"SetDynamicState",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"SetVertexAttribDefaults",
"CreateRenderState",
"CreateRenderState",
"SetDynamicState",
"SetFramebufferState",
"SetShaderImages",
"SetDynamicState",
"CreateRenderState",
"SetSamplerViews",
"CreateRenderState",
"SetPatchState",
"SetPatchState",
"SetPatchState",
"BindRenderState",
"SetPixelPackState",
"CreateRenderState",
"SetDynamicState",
"SetDynamicState",
"DrawVbo",
"SetDispatchProgram",
"SetDrawProgram",
"CreateShaderState",
"CreateRenderState",
"CreateRenderState",
"BindRenderState",
"SetSamplerViews",
"SetDynamicState",
"CreateRenderState",
"SetSamplerViews",
"SetSamplerViews",
"SetSamplerViews",
"SetSamplerViews",
"DrawVbo",
"SetStreamOutputTargets",
"EndStreamOutput",
"SetStreamOutputTargets",
"SetDynamicState",
"SetDynamicState",
"CreateRenderState",
"CreateRenderState",
"BeginStreamOutput",
"PauseStreamOutput",
"kClientResolved", // pseudo-call: not filled by a forward record
"kClientResolved", // pseudo-call: not filled by a forward record
"kReverseChannel", // pseudo-call: not filled by a forward record
};
struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
Uint64 FilledGen[kMGPipeInputFieldCount];
};
[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb) {
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"}",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], verb);
std::abort();
}
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
}
@@ -0,0 +1,67 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeSpanTable.inc
// 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
// G7: the render-state pipeline subset, by member name.
//
// GENERATED by scripts/gen_pipe.py from the field list in scripts/gen_pipe.py - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
// derived from a list that was reviewed rather than invented.
//
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
// table freezes:
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
// backend read points are constant false today. Pipeline state or dead capability?
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
// are handled elsewhere in the payload path rather than in the memo word.
//
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
// glViewport must not mint a new CSO.
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
"CullFaceEnabled",
"DepthTestEnabled",
"PolygonOffsetFillEnabled",
"RasterizerDiscardEnabled",
"ColorLogicOpEnabled",
"StencilTestEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"DepthMask",
"SampleShadingEnabled",
"MultisampleEnabled",
"SampleMaskEnabled",
"SampleMaskValue",
"MinSampleShadingValue",
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"PolygonModeFront",
"CullFaceModeSetting",
"DepthFunc",
"LogicOp",
"StencilStates",
"BlendStates",
"ColorMasks",
};
inline constexpr SizeT kMGPipePipelineStateMemberCount = 24;
static_assert(kMGPipePipelineStateMemberCount ==
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets
// in C++ with offsetof rather than guessing them in python.
extern const MGPStateChunk kMGPipePipelineChunks[];
extern const MGPStateChunk kMGPipeDynamicChunks[];
+108
View File
@@ -0,0 +1,108 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeTables.inc
// 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
// G1: the two MGPipe interface tables.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// share group: 11 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply);
void (*ResourceCreate)(const MGPResourceDesc* payload);
void (*ResourceRespecify)(const MGPResourceDesc* payload);
void (*ResourceDestroy)(const MGPHandleOnly* payload);
void (*MapPersistent)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*UnmapPersistent)(const MGPHandleOnly* payload);
void (*FenceCreate)(const MGPHandleOnly* payload);
void (*FenceStatus)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
void (*FenceDestroy)(const MGPHandleOnly* payload);
void (*FenceWaitServer)(const MGPFenceWait* payload);
};
// context: 60 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeContext {
void (*QueryCreate)(const MGPQueryDesc* payload);
void (*QueryBegin)(const MGPQueryDesc* payload);
void (*QueryEnd)(const MGPQueryDesc* payload);
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
void (*QueryDestroy)(const MGPHandleOnly* payload);
void (*CreateRenderState)(const MGPRenderStateDesc* payload);
void (*BindRenderState)(const MGPBindRenderState* payload);
void (*DeleteRenderState)(const MGPHandleOnly* payload);
void (*CreateVertexElements)(const MGPVertexElements* payload);
void (*BindVertexElements)(const MGPHandleOnly* payload);
void (*DeleteVertexElements)(const MGPHandleOnly* payload);
void (*CreateSamplerState)(const MGPSamplerDesc* payload);
void (*DeleteSamplerState)(const MGPHandleOnly* payload);
void (*CreateSamplerView)(const MGPSamplerView* payload);
void (*DeleteSamplerView)(const MGPHandleOnly* payload);
void (*CreateShaderState)(const MGPProgramDesc* payload);
void (*BindShaderState)(const MGPHandleOnly* payload);
void (*DeleteShaderState)(const MGPHandleOnly* payload);
void (*SetDynamicState)(const MGPDynamicState* payload);
void (*SetFramebufferState)(const MGPFramebufferState* payload);
void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
void (*SetIndirectBuffers)(const MGPIndirectBuffers* payload);
void (*SetSamplerViews)(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount);
void (*BindSamplerStates)(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount);
void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount);
void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount);
void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount);
void (*SetGlobalConstants)(const MGPGlobalConstants* payload);
void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount);
void (*SetPixelPackState)(const MGPPixelPackState* payload);
void (*SetPatchState)(const MGPPatchState* payload);
void (*SetDrawProgram)(const MGPHandleOnly* payload);
void (*SetDispatchProgram)(const MGPHandleOnly* payload);
void (*SetResidualValueState)(const MGPResidualValueState* payload);
void (*SetTextureParams)(const MGPTextureParams* payload);
void (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount);
void (*BufferSubDataResident)(const MGPSubData* payload);
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
void (*ResourceFlushRange)(const MGPFlushRange* payload);
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
void (*ResourceCopyRegion)(const MGPCopyRegion* payload);
void (*GenerateMipmap)(const MGPMipPlan* payload);
void (*GetTextureImage)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
void (*Blit)(const MGPBlit* payload);
void (*Clear)(const MGPClear* payload);
void (*ReadPixels)(const MGPReadbackInfo* payload, MGPReplySlot* reply);
void (*DrawVbo)(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount);
void (*LaunchGrid)(const MGPGridInfo* payload);
void (*MemoryBarrier)(const MGPMemoryBarrier* payload);
void (*BeginStreamOutput)(const MGPStreamOutputBegin* payload);
void (*EndStreamOutput)(const MGPXfbAccounting* payload);
void (*PauseStreamOutput)(const MGPStreamOutputControl* payload);
void (*ResumeStreamOutput)(const MGPStreamOutputControl* payload);
void (*Flush)(const MGPFlush* payload);
void (*Present)(const MGPPresent* payload);
void (*SetSwapInterval)(const MGPSwapInterval* payload);
void (*QueryTimestamp)(const MGPTimestampRequest* payload, MGPReplySlot* reply);
void (*QueryCounter)(const MGPQueryDesc* payload);
};
inline constexpr SizeT kMGPipeScreenCallCount = 11;
inline constexpr SizeT kMGPipeContextCallCount = 60;
inline constexpr SizeT kMGPipeCallCount = 71;
// A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about.
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()),
"MGPipeScreen is not exactly its catalogue's function pointers");
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()),
"MGPipeContext is not exactly its catalogue's function pointers");
static_assert(kMGPipeScreenCallCount + kMGPipeContextCallCount == kMGPipeCallCount);
static_assert(kMGPipeCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT,
"the catalogue and its documented count disagree");
+302
View File
@@ -0,0 +1,302 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeThunks.inc
// 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
// G2: monolith thunks over the two tables.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// One inline call through the installed table. These are the names MG_Impl call
// sites move onto, replacing gBackendFunctionsTable.GL.* one at a time. An
// unimplemented (null) entry is the caller's business to check, exactly as it is
// with the table this replaces.
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) {
gMGPipeScreen.GetCaps(payload, reply);
}
inline void MGP_ResourceCreate(const MGPResourceDesc* payload) {
gMGPipeScreen.ResourceCreate(payload);
}
inline void MGP_ResourceRespecify(const MGPResourceDesc* payload) {
gMGPipeScreen.ResourceRespecify(payload);
}
inline void MGP_ResourceDestroy(const MGPHandleOnly* payload) {
gMGPipeScreen.ResourceDestroy(payload);
}
inline void MGP_MapPersistent(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeScreen.MapPersistent(payload, reply);
}
inline void MGP_UnmapPersistent(const MGPHandleOnly* payload) {
gMGPipeScreen.UnmapPersistent(payload);
}
inline void MGP_FenceCreate(const MGPHandleOnly* payload) {
gMGPipeScreen.FenceCreate(payload);
}
inline void MGP_FenceStatus(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeScreen.FenceStatus(payload, reply);
}
inline void MGP_FenceWait(const MGPFenceWait* payload, MGPReplySlot* reply) {
gMGPipeScreen.FenceWait(payload, reply);
}
inline void MGP_FenceDestroy(const MGPHandleOnly* payload) {
gMGPipeScreen.FenceDestroy(payload);
}
inline void MGP_QueryCreate(const MGPQueryDesc* payload) {
gMGPipeContext.QueryCreate(payload);
}
inline void MGP_QueryBegin(const MGPQueryDesc* payload) {
gMGPipeContext.QueryBegin(payload);
}
inline void MGP_QueryEnd(const MGPQueryDesc* payload) {
gMGPipeContext.QueryEnd(payload);
}
inline void MGP_QueryAvailable(const MGPHandleOnly* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryAvailable(payload, reply);
}
inline void MGP_QueryResult(const MGPQueryResultRequest* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryResult(payload, reply);
}
inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
gMGPipeContext.QueryDestroy(payload);
}
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) {
gMGPipeContext.CreateRenderState(payload);
}
inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
gMGPipeContext.BindRenderState(payload);
}
inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteRenderState(payload);
}
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) {
gMGPipeContext.CreateVertexElements(payload);
}
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
gMGPipeContext.BindVertexElements(payload);
}
inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteVertexElements(payload);
}
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) {
gMGPipeContext.CreateSamplerState(payload);
}
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteSamplerState(payload);
}
inline void MGP_CreateSamplerView(const MGPSamplerView* payload) {
gMGPipeContext.CreateSamplerView(payload);
}
inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteSamplerView(payload);
}
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) {
gMGPipeContext.CreateShaderState(payload);
}
inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
gMGPipeContext.BindShaderState(payload);
}
inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
gMGPipeContext.DeleteShaderState(payload);
}
inline void MGP_SetDynamicState(const MGPDynamicState* payload) {
gMGPipeContext.SetDynamicState(payload);
}
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
gMGPipeContext.SetFramebufferState(payload);
}
inline void MGP_SetVertexBuffers(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetVertexBuffers(payload, varTail, varTailCount);
}
inline void MGP_SetIndexBuffer(const MGPIndexBuffer* payload) {
gMGPipeContext.SetIndexBuffer(payload);
}
inline void MGP_SetIndirectBuffers(const MGPIndirectBuffers* payload) {
gMGPipeContext.SetIndirectBuffers(payload);
}
inline void MGP_SetSamplerViews(const MGPSamplerViews* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetSamplerViews(payload, varTail, varTailCount);
}
inline void MGP_BindSamplerStates(const MGPSamplerStates* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.BindSamplerStates(payload, varTail, varTailCount);
}
inline void MGP_SetShaderImages(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetShaderImages(payload, varTail, varTailCount);
}
inline void MGP_SetShaderBuffers(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetShaderBuffers(payload, varTail, varTailCount);
}
inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
}
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) {
gMGPipeContext.SetGlobalConstants(payload);
}
inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetVertexAttribDefaults(payload, varTail, varTailCount);
}
inline void MGP_SetPixelPackState(const MGPPixelPackState* payload) {
gMGPipeContext.SetPixelPackState(payload);
}
inline void MGP_SetPatchState(const MGPPatchState* payload) {
gMGPipeContext.SetPatchState(payload);
}
inline void MGP_SetDrawProgram(const MGPHandleOnly* payload) {
gMGPipeContext.SetDrawProgram(payload);
}
inline void MGP_SetDispatchProgram(const MGPHandleOnly* payload) {
gMGPipeContext.SetDispatchProgram(payload);
}
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) {
gMGPipeContext.SetResidualValueState(payload);
}
inline void MGP_SetTextureParams(const MGPTextureParams* payload) {
gMGPipeContext.SetTextureParams(payload);
}
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount);
}
inline void MGP_BufferSubDataResident(const MGPSubData* payload) {
gMGPipeContext.BufferSubDataResident(payload);
}
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
gMGPipeContext.ResourceSubDataComplete(payload);
}
inline void MGP_ResourceFlushRange(const MGPFlushRange* payload) {
gMGPipeContext.ResourceFlushRange(payload);
}
inline void MGP_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
gMGPipeContext.ResourceReadback(payload, reply);
}
inline void MGP_ResourceCopyRegion(const MGPCopyRegion* payload) {
gMGPipeContext.ResourceCopyRegion(payload);
}
inline void MGP_GenerateMipmap(const MGPMipPlan* payload) {
gMGPipeContext.GenerateMipmap(payload);
}
inline void MGP_GetTextureImage(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
gMGPipeContext.GetTextureImage(payload, reply);
}
inline void MGP_Blit(const MGPBlit* payload) {
gMGPipeContext.Blit(payload);
}
inline void MGP_Clear(const MGPClear* payload) {
gMGPipeContext.Clear(payload);
}
inline void MGP_ReadPixels(const MGPReadbackInfo* payload, MGPReplySlot* reply) {
gMGPipeContext.ReadPixels(payload, reply);
}
inline void MGP_DrawVbo(const MGPDrawInfo* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.DrawVbo(payload, varTail, varTailCount);
}
inline void MGP_LaunchGrid(const MGPGridInfo* payload) {
gMGPipeContext.LaunchGrid(payload);
}
inline void MGP_MemoryBarrier(const MGPMemoryBarrier* payload) {
gMGPipeContext.MemoryBarrier(payload);
}
inline void MGP_BeginStreamOutput(const MGPStreamOutputBegin* payload) {
gMGPipeContext.BeginStreamOutput(payload);
}
inline void MGP_EndStreamOutput(const MGPXfbAccounting* payload) {
gMGPipeContext.EndStreamOutput(payload);
}
inline void MGP_PauseStreamOutput(const MGPStreamOutputControl* payload) {
gMGPipeContext.PauseStreamOutput(payload);
}
inline void MGP_ResumeStreamOutput(const MGPStreamOutputControl* payload) {
gMGPipeContext.ResumeStreamOutput(payload);
}
inline void MGP_Flush(const MGPFlush* payload) {
gMGPipeContext.Flush(payload);
}
inline void MGP_Present(const MGPPresent* payload) {
gMGPipeContext.Present(payload);
}
inline void MGP_SetSwapInterval(const MGPSwapInterval* payload) {
gMGPipeContext.SetSwapInterval(payload);
}
inline void MGP_QueryTimestamp(const MGPTimestampRequest* payload, MGPReplySlot* reply) {
gMGPipeContext.QueryTimestamp(payload, reply);
}
inline void MGP_QueryCounter(const MGPQueryDesc* payload) {
gMGPipeContext.QueryCounter(payload);
}
inline void MGP_FenceWaitServer(const MGPFenceWait* payload) {
gMGPipeScreen.FenceWaitServer(payload);
}
+573
View File
@@ -0,0 +1,573 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeVerify.inc
// 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
// G4: the MOBILEGL_PIPE_VERIFY field-wise comparators.
//
// GENERATED by scripts/gen_pipe.py from PipeFields.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// Field by field, never memcmp over a whole payload: RenderStateParameters is documented
// in DirectGLES.cpp to false-DIFFER on padding under memcmp (harmlessly there, fatally
// here - a comparator with false positives is a comparator nobody reads). Each function
// reports the FIRST differing field by name, which with the draw serial is what the verify
// harness prints.
//
// Floating-point fields are compared by BITS, so a NaN patch level - which
// glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals
// itself instead of tripping every draw.
#include "../PipeFields.def"
template <class T>
struct MGPipeHasFieldVerifier : std::false_type {};
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField);
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField);
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField);
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField);
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField);
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField);
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField);
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField);
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField);
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField);
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField);
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField);
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField);
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField);
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField);
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField);
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField);
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField);
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField);
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField);
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField);
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField);
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField);
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField);
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField);
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField);
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField);
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField);
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField);
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField);
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField);
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField);
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField);
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField);
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField);
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField);
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField);
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField);
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField);
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
template <>
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBox> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReplySlot> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStateChunk> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPHandleOnly> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPCaps> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPResourceDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFenceWait> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPQueryDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPQueryResultRequest> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPTimestampRequest> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPRenderStateDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBindRenderState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDynamicState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexElements> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPTextureParams> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramDesc> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSurface> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFramebufferState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexBuffer> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPIndexBuffer> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPIndirectBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBoundView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerViews> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSamplerStates> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPImageView> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPShaderImages> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBufferRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPShaderBuffers> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputTargets> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPGlobalConstants> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPAttribValue> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexAttribDefaults> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPixelPackState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPatchState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<ResidualValueBlock> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPResidualValueState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubRegion> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubData> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSubDataComplete> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFlushRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReadback> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPCopyRegion> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBlit> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPClear> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPMipPlan> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPReadbackInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawRange> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPDrawIndirect> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPGridInfo> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPMemoryBarrier> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputBegin> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPXfbAccounting> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPStreamOutputControl> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPFlush> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPPresent> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSwapInterval> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSurfaceInfo> : std::true_type {};
template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
if constexpr (MGPipeHasFieldVerifier<T>::value) {
const char* unusedField = nullptr;
return MGPipeVerify(a, b, &unusedField);
} else if constexpr (std::is_floating_point_v<T>) {
return std::memcmp(&a, &b, sizeof(T)) == 0;
} else if constexpr (std::is_scalar_v<T> || std::is_enum_v<T>) {
return a == b;
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
return a == b;
} else {
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
// and this branch stops being reachable from any payload.
return std::memcmp(&a, &b, sizeof(T)) == 0;
}
}
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!MGPipeFieldEqual(a[i], b[i])) return false;
}
return true;
}
#define MGP_VERIFY_FIELD(FieldName) \
if (!MGPipeFieldEqual(a.FieldName, b.FieldName)) { \
if (outField != nullptr) *outField = #FieldName; \
return false; \
}
inline Bool MGPipeVerify(const MGPBlobRef& a, const MGPBlobRef& b, const char** outField) {
MGP_FIELDS_MGPBlobRef(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPRange& a, const MGPRange& b, const char** outField) {
MGP_FIELDS_MGPRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBox& a, const MGPBox& b, const char** outField) {
MGP_FIELDS_MGPBox(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReplySlot& a, const MGPReplySlot& b, const char** outField) {
MGP_FIELDS_MGPReplySlot(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStateChunk& a, const MGPStateChunk& b, const char** outField) {
MGP_FIELDS_MGPStateChunk(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPHandleOnly& a, const MGPHandleOnly& b, const char** outField) {
MGP_FIELDS_MGPHandleOnly(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPCaps& a, const MGPCaps& b, const char** outField) {
MGP_FIELDS_MGPCaps(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPResourceDesc& a, const MGPResourceDesc& b, const char** outField) {
MGP_FIELDS_MGPResourceDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFenceWait& a, const MGPFenceWait& b, const char** outField) {
MGP_FIELDS_MGPFenceWait(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPQueryDesc& a, const MGPQueryDesc& b, const char** outField) {
MGP_FIELDS_MGPQueryDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPQueryResultRequest& a, const MGPQueryResultRequest& b, const char** outField) {
MGP_FIELDS_MGPQueryResultRequest(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPTimestampRequest& a, const MGPTimestampRequest& b, const char** outField) {
MGP_FIELDS_MGPTimestampRequest(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPRenderStateDesc& a, const MGPRenderStateDesc& b, const char** outField) {
MGP_FIELDS_MGPRenderStateDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBindRenderState& a, const MGPBindRenderState& b, const char** outField) {
MGP_FIELDS_MGPBindRenderState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDynamicState& a, const MGPDynamicState& b, const char** outField) {
MGP_FIELDS_MGPDynamicState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexElements& a, const MGPVertexElements& b, const char** outField) {
MGP_FIELDS_MGPVertexElements(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerDesc& a, const MGPSamplerDesc& b, const char** outField) {
MGP_FIELDS_MGPSamplerDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerView& a, const MGPSamplerView& b, const char** outField) {
MGP_FIELDS_MGPSamplerView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPTextureParams& a, const MGPTextureParams& b, const char** outField) {
MGP_FIELDS_MGPTextureParams(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPProgramDesc& a, const MGPProgramDesc& b, const char** outField) {
MGP_FIELDS_MGPProgramDesc(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSurface& a, const MGPSurface& b, const char** outField) {
MGP_FIELDS_MGPSurface(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFramebufferState& a, const MGPFramebufferState& b, const char** outField) {
MGP_FIELDS_MGPFramebufferState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexBuffer& a, const MGPVertexBuffer& b, const char** outField) {
MGP_FIELDS_MGPVertexBuffer(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexBuffers& a, const MGPVertexBuffers& b, const char** outField) {
MGP_FIELDS_MGPVertexBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPIndexBuffer& a, const MGPIndexBuffer& b, const char** outField) {
MGP_FIELDS_MGPIndexBuffer(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPIndirectBuffers& a, const MGPIndirectBuffers& b, const char** outField) {
MGP_FIELDS_MGPIndirectBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBoundView& a, const MGPBoundView& b, const char** outField) {
MGP_FIELDS_MGPBoundView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerViews& a, const MGPSamplerViews& b, const char** outField) {
MGP_FIELDS_MGPSamplerViews(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSamplerStates& a, const MGPSamplerStates& b, const char** outField) {
MGP_FIELDS_MGPSamplerStates(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPImageView& a, const MGPImageView& b, const char** outField) {
MGP_FIELDS_MGPImageView(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPShaderImages& a, const MGPShaderImages& b, const char** outField) {
MGP_FIELDS_MGPShaderImages(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBufferRange& a, const MGPBufferRange& b, const char** outField) {
MGP_FIELDS_MGPBufferRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPShaderBuffers& a, const MGPShaderBuffers& b, const char** outField) {
MGP_FIELDS_MGPShaderBuffers(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputTargets& a, const MGPStreamOutputTargets& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputTargets(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPGlobalConstants& a, const MGPGlobalConstants& b, const char** outField) {
MGP_FIELDS_MGPGlobalConstants(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPAttribValue& a, const MGPAttribValue& b, const char** outField) {
MGP_FIELDS_MGPAttribValue(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexAttribDefaults& a, const MGPVertexAttribDefaults& b, const char** outField) {
MGP_FIELDS_MGPVertexAttribDefaults(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPixelPackState& a, const MGPPixelPackState& b, const char** outField) {
MGP_FIELDS_MGPPixelPackState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPatchState& a, const MGPPatchState& b, const char** outField) {
MGP_FIELDS_MGPPatchState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const ResidualValueBlock& a, const ResidualValueBlock& b, const char** outField) {
MGP_FIELDS_ResidualValueBlock(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPResidualValueState& a, const MGPResidualValueState& b, const char** outField) {
MGP_FIELDS_MGPResidualValueState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubRegion& a, const MGPSubRegion& b, const char** outField) {
MGP_FIELDS_MGPSubRegion(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubData& a, const MGPSubData& b, const char** outField) {
MGP_FIELDS_MGPSubData(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSubDataComplete& a, const MGPSubDataComplete& b, const char** outField) {
MGP_FIELDS_MGPSubDataComplete(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFlushRange& a, const MGPFlushRange& b, const char** outField) {
MGP_FIELDS_MGPFlushRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReadback& a, const MGPReadback& b, const char** outField) {
MGP_FIELDS_MGPReadback(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPCopyRegion& a, const MGPCopyRegion& b, const char** outField) {
MGP_FIELDS_MGPCopyRegion(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPBlit& a, const MGPBlit& b, const char** outField) {
MGP_FIELDS_MGPBlit(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPClear& a, const MGPClear& b, const char** outField) {
MGP_FIELDS_MGPClear(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPMipPlan& a, const MGPMipPlan& b, const char** outField) {
MGP_FIELDS_MGPMipPlan(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPReadbackInfo& a, const MGPReadbackInfo& b, const char** outField) {
MGP_FIELDS_MGPReadbackInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawInfo& a, const MGPDrawInfo& b, const char** outField) {
MGP_FIELDS_MGPDrawInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawRange& a, const MGPDrawRange& b, const char** outField) {
MGP_FIELDS_MGPDrawRange(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPDrawIndirect& a, const MGPDrawIndirect& b, const char** outField) {
MGP_FIELDS_MGPDrawIndirect(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPGridInfo& a, const MGPGridInfo& b, const char** outField) {
MGP_FIELDS_MGPGridInfo(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPMemoryBarrier& a, const MGPMemoryBarrier& b, const char** outField) {
MGP_FIELDS_MGPMemoryBarrier(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputBegin& a, const MGPStreamOutputBegin& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputBegin(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPXfbAccounting& a, const MGPXfbAccounting& b, const char** outField) {
MGP_FIELDS_MGPXfbAccounting(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPStreamOutputControl& a, const MGPStreamOutputControl& b, const char** outField) {
MGP_FIELDS_MGPStreamOutputControl(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outField) {
MGP_FIELDS_MGPFlush(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPPresent& a, const MGPPresent& b, const char** outField) {
MGP_FIELDS_MGPPresent(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, const char** outField) {
MGP_FIELDS_MGPSwapInterval(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField) {
MGP_FIELDS_MGPSurfaceInfo(MGP_VERIFY_FIELD)
return true;
}
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 63;
+931
View File
@@ -0,0 +1,931 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeWire.inc
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G3: wire records, size assertions and the applier's bounds gate.
//
// GENERATED by scripts/gen_pipe.py from PipeCalls.def - DO NOT EDIT.
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// Every record is a fixed header plus its payload, padded to the stream's 8-byte
// granularity. The size assertion is stated as a COMPOSITION so it fires on any padding
// the compiler inserts between the header and the payload while staying honest about the
// tail padding the alignment requires.
//
// The applier's precondition is checked BEFORE dispatch, on every record, in every build:
// a record that is shorter than its own type, longer than what is left in the buffer, or
// not a multiple of 8 is protocol corruption and is fatal. There is no recovery path -
// silently applying a truncated record is how a corrupt stream becomes a wrong picture.
//
// OVERSIZED PAYLOADS ARE CHUNKED, NEVER EMITTED WHOLE (plan section 8.2: G3 has to define
// the path for a record larger than the segment). The bound is the ring's,
// RingProducer::MaxRecordBytes() == Capacity()/2, and it is exact rather than
// conservative: a record has to be placeable at every head offset of an empty ring, the
// wrap pad in front of it costs up to total-8 bytes, and only a record of at most half the
// ring survives that at every offset. An emitter holding more than Capacity()/2 bytes of
// record (a large resource_subdata, a create_shader_state archive) splits it into several
// records of at most that size; the transport refuses a bigger one outright - nullptr plus
// an MGLOG_E - rather than let the producer wait on free bytes that can never suffice.
struct MGPWireRecHeader {
Uint16 Op; // MGPWireOp
Uint16 Flags; // MGPipeCallFlags of the call, for asserts and tracing
Uint32 Size; // bytes of this record including the header and the variable tail
};
static_assert(sizeof(MGPWireRecHeader) == 8, "the wire header is 8 bytes");
static_assert(std::is_trivially_copyable_v<MGPWireRecHeader>);
// The opcode is the call's position in PipeCalls.def. Reordering that file is a protocol
// break; appending to it is not.
enum class MGPWireOp : Uint16 {
kInvalid = 0,
GetCaps = 1,
ResourceCreate = 2,
ResourceRespecify = 3,
ResourceDestroy = 4,
MapPersistent = 5,
UnmapPersistent = 6,
FenceCreate = 7,
FenceStatus = 8,
FenceWait = 9,
FenceDestroy = 10,
QueryCreate = 11,
QueryBegin = 12,
QueryEnd = 13,
QueryAvailable = 14,
QueryResult = 15,
QueryDestroy = 16,
CreateRenderState = 17,
BindRenderState = 18,
DeleteRenderState = 19,
CreateVertexElements = 20,
BindVertexElements = 21,
DeleteVertexElements = 22,
CreateSamplerState = 23,
DeleteSamplerState = 24,
CreateSamplerView = 25,
DeleteSamplerView = 26,
CreateShaderState = 27,
BindShaderState = 28,
DeleteShaderState = 29,
SetDynamicState = 30,
SetFramebufferState = 31,
SetVertexBuffers = 32,
SetIndexBuffer = 33,
SetIndirectBuffers = 34,
SetSamplerViews = 35,
BindSamplerStates = 36,
SetShaderImages = 37,
SetShaderBuffers = 38,
SetStreamOutputTargets = 39,
SetGlobalConstants = 40,
SetVertexAttribDefaults = 41,
SetPixelPackState = 42,
SetPatchState = 43,
SetDrawProgram = 44,
SetDispatchProgram = 45,
SetResidualValueState = 46,
SetTextureParams = 47,
ResourceSubData = 48,
BufferSubDataResident = 49,
ResourceSubDataComplete = 50,
ResourceFlushRange = 51,
ResourceReadback = 52,
ResourceCopyRegion = 53,
GenerateMipmap = 54,
GetTextureImage = 55,
Blit = 56,
Clear = 57,
ReadPixels = 58,
DrawVbo = 59,
LaunchGrid = 60,
MemoryBarrier = 61,
BeginStreamOutput = 62,
EndStreamOutput = 63,
PauseStreamOutput = 64,
ResumeStreamOutput = 65,
Flush = 66,
Present = 67,
SetSwapInterval = 68,
QueryTimestamp = 69,
QueryCounter = 70,
FenceWaitServer = 71,
kOpCount = 72,
};
struct alignas(8) MGPWireRec_GetCaps {
MGPWireRecHeader Header;
MGPCaps Payload;
};
static_assert(sizeof(MGPWireRec_GetCaps) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPCaps) + 7u) & ~SizeT(7u)),
"MGPWireRec_GetCaps gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceCreate {
MGPWireRecHeader Header;
MGPResourceDesc Payload;
};
static_assert(sizeof(MGPWireRec_ResourceCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceRespecify {
MGPWireRecHeader Header;
MGPResourceDesc Payload;
};
static_assert(sizeof(MGPWireRec_ResourceRespecify) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResourceDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceRespecify gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_ResourceDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_MapPersistent {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_MapPersistent) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_MapPersistent gained padding; the wire format moved");
struct alignas(8) MGPWireRec_UnmapPersistent {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_UnmapPersistent) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_UnmapPersistent gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceCreate {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceStatus {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceStatus) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceStatus gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceWait {
MGPWireRecHeader Header;
MGPFenceWait Payload;
};
static_assert(sizeof(MGPWireRec_FenceWait) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceWait gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_FenceDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryCreate {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryCreate) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryCreate gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryBegin {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryBegin) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryBegin gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryEnd {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryEnd) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryEnd gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryAvailable {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_QueryAvailable) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryAvailable gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryResult {
MGPWireRecHeader Header;
MGPQueryResultRequest Payload;
};
static_assert(sizeof(MGPWireRec_QueryResult) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryResultRequest) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryResult gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryDestroy {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_QueryDestroy) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryDestroy gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateRenderState {
MGPWireRecHeader Header;
MGPRenderStateDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPRenderStateDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindRenderState {
MGPWireRecHeader Header;
MGPBindRenderState Payload;
};
static_assert(sizeof(MGPWireRec_BindRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBindRenderState) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteRenderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteRenderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteRenderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateVertexElements {
MGPWireRecHeader Header;
MGPVertexElements Payload;
};
static_assert(sizeof(MGPWireRec_CreateVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexElements) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindVertexElements {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_BindVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteVertexElements {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteVertexElements) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteVertexElements gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateSamplerState {
MGPWireRecHeader Header;
MGPSamplerDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateSamplerState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateSamplerState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteSamplerState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteSamplerState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteSamplerState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateSamplerView {
MGPWireRecHeader Header;
MGPSamplerView Payload;
};
static_assert(sizeof(MGPWireRec_CreateSamplerView) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerView) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateSamplerView gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteSamplerView {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteSamplerView) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteSamplerView gained padding; the wire format moved");
struct alignas(8) MGPWireRec_CreateShaderState {
MGPWireRecHeader Header;
MGPProgramDesc Payload;
};
static_assert(sizeof(MGPWireRec_CreateShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPProgramDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_CreateShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindShaderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_BindShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteShaderState {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_DeleteShaderState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteShaderState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDynamicState {
MGPWireRecHeader Header;
MGPDynamicState Payload;
};
static_assert(sizeof(MGPWireRec_SetDynamicState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPDynamicState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDynamicState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetFramebufferState {
MGPWireRecHeader Header;
MGPFramebufferState Payload;
};
static_assert(sizeof(MGPWireRec_SetFramebufferState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFramebufferState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetFramebufferState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetVertexBuffers {
MGPWireRecHeader Header;
MGPVertexBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetVertexBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetVertexBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetIndexBuffer {
MGPWireRecHeader Header;
MGPIndexBuffer Payload;
};
static_assert(sizeof(MGPWireRec_SetIndexBuffer) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPIndexBuffer) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetIndexBuffer gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetIndirectBuffers {
MGPWireRecHeader Header;
MGPIndirectBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetIndirectBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPIndirectBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetIndirectBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetSamplerViews {
MGPWireRecHeader Header;
MGPSamplerViews Payload;
};
static_assert(sizeof(MGPWireRec_SetSamplerViews) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerViews) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetSamplerViews gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BindSamplerStates {
MGPWireRecHeader Header;
MGPSamplerStates Payload;
};
static_assert(sizeof(MGPWireRec_BindSamplerStates) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSamplerStates) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindSamplerStates gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetShaderImages {
MGPWireRecHeader Header;
MGPShaderImages Payload;
};
static_assert(sizeof(MGPWireRec_SetShaderImages) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderImages) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetShaderImages gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetShaderBuffers {
MGPWireRecHeader Header;
MGPShaderBuffers Payload;
};
static_assert(sizeof(MGPWireRec_SetShaderBuffers) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPShaderBuffers) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetShaderBuffers gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetStreamOutputTargets {
MGPWireRecHeader Header;
MGPStreamOutputTargets Payload;
};
static_assert(sizeof(MGPWireRec_SetStreamOutputTargets) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputTargets) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetStreamOutputTargets gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetGlobalConstants {
MGPWireRecHeader Header;
MGPGlobalConstants Payload;
};
static_assert(sizeof(MGPWireRec_SetGlobalConstants) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPGlobalConstants) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetGlobalConstants gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetVertexAttribDefaults {
MGPWireRecHeader Header;
MGPVertexAttribDefaults Payload;
};
static_assert(sizeof(MGPWireRec_SetVertexAttribDefaults) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPVertexAttribDefaults) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetVertexAttribDefaults gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetPixelPackState {
MGPWireRecHeader Header;
MGPPixelPackState Payload;
};
static_assert(sizeof(MGPWireRec_SetPixelPackState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPixelPackState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetPixelPackState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetPatchState {
MGPWireRecHeader Header;
MGPPatchState Payload;
};
static_assert(sizeof(MGPWireRec_SetPatchState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPatchState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetPatchState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDrawProgram {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_SetDrawProgram) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDrawProgram gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetDispatchProgram {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_SetDispatchProgram) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetDispatchProgram gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetResidualValueState {
MGPWireRecHeader Header;
MGPResidualValueState Payload;
};
static_assert(sizeof(MGPWireRec_SetResidualValueState) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPResidualValueState) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetResidualValueState gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetTextureParams {
MGPWireRecHeader Header;
MGPTextureParams Payload;
};
static_assert(sizeof(MGPWireRec_SetTextureParams) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPTextureParams) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetTextureParams gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceSubData {
MGPWireRecHeader Header;
MGPSubData Payload;
};
static_assert(sizeof(MGPWireRec_ResourceSubData) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceSubData gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BufferSubDataResident {
MGPWireRecHeader Header;
MGPSubData Payload;
};
static_assert(sizeof(MGPWireRec_BufferSubDataResident) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubData) + 7u) & ~SizeT(7u)),
"MGPWireRec_BufferSubDataResident gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceSubDataComplete {
MGPWireRecHeader Header;
MGPSubDataComplete Payload;
};
static_assert(sizeof(MGPWireRec_ResourceSubDataComplete) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSubDataComplete) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceSubDataComplete gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceFlushRange {
MGPWireRecHeader Header;
MGPFlushRange Payload;
};
static_assert(sizeof(MGPWireRec_ResourceFlushRange) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFlushRange) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceFlushRange gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceReadback {
MGPWireRecHeader Header;
MGPReadback Payload;
};
static_assert(sizeof(MGPWireRec_ResourceReadback) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadback) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceReadback gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResourceCopyRegion {
MGPWireRecHeader Header;
MGPCopyRegion Payload;
};
static_assert(sizeof(MGPWireRec_ResourceCopyRegion) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyRegion) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResourceCopyRegion gained padding; the wire format moved");
struct alignas(8) MGPWireRec_GenerateMipmap {
MGPWireRecHeader Header;
MGPMipPlan Payload;
};
static_assert(sizeof(MGPWireRec_GenerateMipmap) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPMipPlan) + 7u) & ~SizeT(7u)),
"MGPWireRec_GenerateMipmap gained padding; the wire format moved");
struct alignas(8) MGPWireRec_GetTextureImage {
MGPWireRecHeader Header;
MGPReadbackInfo Payload;
};
static_assert(sizeof(MGPWireRec_GetTextureImage) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_GetTextureImage gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Blit {
MGPWireRecHeader Header;
MGPBlit Payload;
};
static_assert(sizeof(MGPWireRec_Blit) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBlit) + 7u) & ~SizeT(7u)),
"MGPWireRec_Blit gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Clear {
MGPWireRecHeader Header;
MGPClear Payload;
};
static_assert(sizeof(MGPWireRec_Clear) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPClear) + 7u) & ~SizeT(7u)),
"MGPWireRec_Clear gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ReadPixels {
MGPWireRecHeader Header;
MGPReadbackInfo Payload;
};
static_assert(sizeof(MGPWireRec_ReadPixels) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPReadbackInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_ReadPixels gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DrawVbo {
MGPWireRecHeader Header;
MGPDrawInfo Payload;
};
static_assert(sizeof(MGPWireRec_DrawVbo) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPDrawInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_DrawVbo gained padding; the wire format moved");
struct alignas(8) MGPWireRec_LaunchGrid {
MGPWireRecHeader Header;
MGPGridInfo Payload;
};
static_assert(sizeof(MGPWireRec_LaunchGrid) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPGridInfo) + 7u) & ~SizeT(7u)),
"MGPWireRec_LaunchGrid gained padding; the wire format moved");
struct alignas(8) MGPWireRec_MemoryBarrier {
MGPWireRecHeader Header;
MGPMemoryBarrier Payload;
};
static_assert(sizeof(MGPWireRec_MemoryBarrier) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPMemoryBarrier) + 7u) & ~SizeT(7u)),
"MGPWireRec_MemoryBarrier gained padding; the wire format moved");
struct alignas(8) MGPWireRec_BeginStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputBegin Payload;
};
static_assert(sizeof(MGPWireRec_BeginStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputBegin) + 7u) & ~SizeT(7u)),
"MGPWireRec_BeginStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_EndStreamOutput {
MGPWireRecHeader Header;
MGPXfbAccounting Payload;
};
static_assert(sizeof(MGPWireRec_EndStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPXfbAccounting) + 7u) & ~SizeT(7u)),
"MGPWireRec_EndStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_PauseStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputControl Payload;
};
static_assert(sizeof(MGPWireRec_PauseStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
"MGPWireRec_PauseStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ResumeStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputControl Payload;
};
static_assert(sizeof(MGPWireRec_ResumeStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputControl) + 7u) & ~SizeT(7u)),
"MGPWireRec_ResumeStreamOutput gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Flush {
MGPWireRecHeader Header;
MGPFlush Payload;
};
static_assert(sizeof(MGPWireRec_Flush) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFlush) + 7u) & ~SizeT(7u)),
"MGPWireRec_Flush gained padding; the wire format moved");
struct alignas(8) MGPWireRec_Present {
MGPWireRecHeader Header;
MGPPresent Payload;
};
static_assert(sizeof(MGPWireRec_Present) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPPresent) + 7u) & ~SizeT(7u)),
"MGPWireRec_Present gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetSwapInterval {
MGPWireRecHeader Header;
MGPSwapInterval Payload;
};
static_assert(sizeof(MGPWireRec_SetSwapInterval) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSwapInterval) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetSwapInterval gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryTimestamp {
MGPWireRecHeader Header;
MGPTimestampRequest Payload;
};
static_assert(sizeof(MGPWireRec_QueryTimestamp) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPTimestampRequest) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryTimestamp gained padding; the wire format moved");
struct alignas(8) MGPWireRec_QueryCounter {
MGPWireRecHeader Header;
MGPQueryDesc Payload;
};
static_assert(sizeof(MGPWireRec_QueryCounter) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPQueryDesc) + 7u) & ~SizeT(7u)),
"MGPWireRec_QueryCounter gained padding; the wire format moved");
struct alignas(8) MGPWireRec_FenceWaitServer {
MGPWireRecHeader Header;
MGPFenceWait Payload;
};
static_assert(sizeof(MGPWireRec_FenceWaitServer) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPFenceWait) + 7u) & ~SizeT(7u)),
"MGPWireRec_FenceWaitServer gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
std::abort();
}
#define MGP_WIRE_CHECK_BOUNDS(RecType, CallName) \
do { \
if (!(size >= sizeof(RecType) && size <= remaining && (size % 8) == 0)) { \
MGPipeWireProtocolFatal(CallName, size, remaining); \
} \
} while (0)
// Returns whether the record was applied. P0 is a SKELETON: every case validates its
// bounds and then reports "not applied", because no applier exists until P5 wires
// MG_Remote/Server/PipeApplier.cpp to the real backend tables. The switch and the opcode
// enum come from the same list, so a call added to the catalogue cannot be forgotten here;
// the default arm is for the opcode that never came from this catalogue at all - a byte
// off a corrupt stream - and it is fatal for the same reason the bounds check is.
inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, Uint64 remaining) {
(void)record;
switch (op) {
case MGPWireOp::GetCaps:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetCaps, "GetCaps");
return false;
case MGPWireOp::ResourceCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCreate, "ResourceCreate");
return false;
case MGPWireOp::ResourceRespecify:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceRespecify, "ResourceRespecify");
return false;
case MGPWireOp::ResourceDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceDestroy, "ResourceDestroy");
return false;
case MGPWireOp::MapPersistent:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MapPersistent, "MapPersistent");
return false;
case MGPWireOp::UnmapPersistent:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_UnmapPersistent, "UnmapPersistent");
return false;
case MGPWireOp::FenceCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceCreate, "FenceCreate");
return false;
case MGPWireOp::FenceStatus:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceStatus, "FenceStatus");
return false;
case MGPWireOp::FenceWait:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWait, "FenceWait");
return false;
case MGPWireOp::FenceDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceDestroy, "FenceDestroy");
return false;
case MGPWireOp::QueryCreate:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCreate, "QueryCreate");
return false;
case MGPWireOp::QueryBegin:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryBegin, "QueryBegin");
return false;
case MGPWireOp::QueryEnd:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryEnd, "QueryEnd");
return false;
case MGPWireOp::QueryAvailable:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryAvailable, "QueryAvailable");
return false;
case MGPWireOp::QueryResult:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryResult, "QueryResult");
return false;
case MGPWireOp::QueryDestroy:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryDestroy, "QueryDestroy");
return false;
case MGPWireOp::CreateRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateRenderState, "CreateRenderState");
return false;
case MGPWireOp::BindRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindRenderState, "BindRenderState");
return false;
case MGPWireOp::DeleteRenderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteRenderState, "DeleteRenderState");
return false;
case MGPWireOp::CreateVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateVertexElements, "CreateVertexElements");
return false;
case MGPWireOp::BindVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindVertexElements, "BindVertexElements");
return false;
case MGPWireOp::DeleteVertexElements:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteVertexElements, "DeleteVertexElements");
return false;
case MGPWireOp::CreateSamplerState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerState, "CreateSamplerState");
return false;
case MGPWireOp::DeleteSamplerState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerState, "DeleteSamplerState");
return false;
case MGPWireOp::CreateSamplerView:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateSamplerView, "CreateSamplerView");
return false;
case MGPWireOp::DeleteSamplerView:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteSamplerView, "DeleteSamplerView");
return false;
case MGPWireOp::CreateShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CreateShaderState, "CreateShaderState");
return false;
case MGPWireOp::BindShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindShaderState, "BindShaderState");
return false;
case MGPWireOp::DeleteShaderState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteShaderState, "DeleteShaderState");
return false;
case MGPWireOp::SetDynamicState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDynamicState, "SetDynamicState");
return false;
case MGPWireOp::SetFramebufferState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetFramebufferState, "SetFramebufferState");
return false;
case MGPWireOp::SetVertexBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexBuffers, "SetVertexBuffers");
return false;
case MGPWireOp::SetIndexBuffer:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndexBuffer, "SetIndexBuffer");
return false;
case MGPWireOp::SetIndirectBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetIndirectBuffers, "SetIndirectBuffers");
return false;
case MGPWireOp::SetSamplerViews:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSamplerViews, "SetSamplerViews");
return false;
case MGPWireOp::BindSamplerStates:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindSamplerStates, "BindSamplerStates");
return false;
case MGPWireOp::SetShaderImages:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderImages, "SetShaderImages");
return false;
case MGPWireOp::SetShaderBuffers:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetShaderBuffers, "SetShaderBuffers");
return false;
case MGPWireOp::SetStreamOutputTargets:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetStreamOutputTargets, "SetStreamOutputTargets");
return false;
case MGPWireOp::SetGlobalConstants:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetGlobalConstants, "SetGlobalConstants");
return false;
case MGPWireOp::SetVertexAttribDefaults:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetVertexAttribDefaults, "SetVertexAttribDefaults");
return false;
case MGPWireOp::SetPixelPackState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPixelPackState, "SetPixelPackState");
return false;
case MGPWireOp::SetPatchState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetPatchState, "SetPatchState");
return false;
case MGPWireOp::SetDrawProgram:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDrawProgram, "SetDrawProgram");
return false;
case MGPWireOp::SetDispatchProgram:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetDispatchProgram, "SetDispatchProgram");
return false;
case MGPWireOp::SetResidualValueState:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetResidualValueState, "SetResidualValueState");
return false;
case MGPWireOp::SetTextureParams:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetTextureParams, "SetTextureParams");
return false;
case MGPWireOp::ResourceSubData:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubData, "ResourceSubData");
return false;
case MGPWireOp::BufferSubDataResident:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BufferSubDataResident, "BufferSubDataResident");
return false;
case MGPWireOp::ResourceSubDataComplete:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceSubDataComplete, "ResourceSubDataComplete");
return false;
case MGPWireOp::ResourceFlushRange:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceFlushRange, "ResourceFlushRange");
return false;
case MGPWireOp::ResourceReadback:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceReadback, "ResourceReadback");
return false;
case MGPWireOp::ResourceCopyRegion:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResourceCopyRegion, "ResourceCopyRegion");
return false;
case MGPWireOp::GenerateMipmap:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GenerateMipmap, "GenerateMipmap");
return false;
case MGPWireOp::GetTextureImage:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_GetTextureImage, "GetTextureImage");
return false;
case MGPWireOp::Blit:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Blit, "Blit");
return false;
case MGPWireOp::Clear:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Clear, "Clear");
return false;
case MGPWireOp::ReadPixels:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ReadPixels, "ReadPixels");
return false;
case MGPWireOp::DrawVbo:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DrawVbo, "DrawVbo");
return false;
case MGPWireOp::LaunchGrid:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_LaunchGrid, "LaunchGrid");
return false;
case MGPWireOp::MemoryBarrier:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_MemoryBarrier, "MemoryBarrier");
return false;
case MGPWireOp::BeginStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BeginStreamOutput, "BeginStreamOutput");
return false;
case MGPWireOp::EndStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_EndStreamOutput, "EndStreamOutput");
return false;
case MGPWireOp::PauseStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_PauseStreamOutput, "PauseStreamOutput");
return false;
case MGPWireOp::ResumeStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ResumeStreamOutput, "ResumeStreamOutput");
return false;
case MGPWireOp::Flush:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Flush, "Flush");
return false;
case MGPWireOp::Present:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_Present, "Present");
return false;
case MGPWireOp::SetSwapInterval:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetSwapInterval, "SetSwapInterval");
return false;
case MGPWireOp::QueryTimestamp:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryTimestamp, "QueryTimestamp");
return false;
case MGPWireOp::QueryCounter:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_QueryCounter, "QueryCounter");
return false;
case MGPWireOp::FenceWaitServer:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_FenceWaitServer, "FenceWaitServer");
return false;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
MGPipeWireProtocolFatal("<unknown opcode>", size, remaining);
}
}
#undef MGP_WIRE_CHECK_BOUNDS
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
// MobileGL - MobileGL/MG_Remote/Protocol/mg_protocol_base.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
// Shared vocabulary of the MG_Remote wire contracts (transport, framing, ring,
// shm). Inherited from the earlier `Feat/CS-Delta-IPC` branch
// (MobileGL/Protocol/mg_protocol_base.h) and cut down to what plan B's
// transport actually needs: result codes, byte spans, a shm region reference
// and the id typedefs.
//
// Deliberately NOT inherited: MobileGLObjectKind / MobileGLObjectScope /
// MobileGLObjectHandle. Plan B does not put GL object identity on the wire at
// all - the frontend allocates {slot, generation} handles in MG_Pipe
// (PLAN-B.md section 4.2.1) and those are the only identity the backend ever
// sees, so a second object-identity vocabulary here would be a drift surface
// with no reader.
//
// This header must stay:
// - pure C (compilable from C and C++, no MG C++ types, no exceptions/RTTI),
// - dependency-free (only <stdbool.h>/<stddef.h>/<stdint.h>),
// - append-only within an ABI major (see versioning rules below).
//
// Versioning rules (contract-wide):
// - Every versioned struct starts with uint32_t structSize.
// - Appending fields at the tail is a MINOR bump; receivers must ignore
// bytes beyond the structSize they know.
// - Changing/removing/reordering existing fields is a MAJOR bump.
// - A major mismatch is a hard, structured failure, never an exception.
// (Plan B keeps the structSize-first discipline as the answer to risk B-R10,
// PLAN-B.md section 14.2.)
#ifndef MOBILEGL_REMOTE_PROTOCOL_BASE_H
#define MOBILEGL_REMOTE_PROTOCOL_BASE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// ---------------------------------------------------------------------------
// ABI versions
// ---------------------------------------------------------------------------
#define MOBILEGL_PROTOCOL_ABI_MAJOR 1
#define MOBILEGL_PROTOCOL_ABI_MINOR 0
#define MOBILEGL_ABI_VERSION(major, minor) (((uint32_t)(major) << 16) | (uint32_t)(minor))
#define MOBILEGL_ABI_MAJOR_OF(version) ((uint32_t)(version) >> 16)
#define MOBILEGL_ABI_MINOR_OF(version) ((uint32_t)(version) & 0xFFFFu)
// ---------------------------------------------------------------------------
// Ids
// ---------------------------------------------------------------------------
typedef uint64_t MobileGLSessionId; // one client GL context flow
typedef uint64_t MobileGLRequestSeq; // matches a request to its reply
typedef uint32_t MobileGLSegmentId; // shm segment id within a connection
// ---------------------------------------------------------------------------
// Spans / regions
// ---------------------------------------------------------------------------
// Borrowed, read-only byte span. The pointee is owned by the producing side
// and is only valid for the duration documented at the consuming call site.
typedef struct MobileGLByteSpan {
const void* data;
uint64_t size;
} MobileGLByteSpan;
typedef struct MobileGLMutableByteSpan {
void* data;
uint64_t size;
} MobileGLMutableByteSpan;
// A byte range inside an already-established shm segment. Segments are
// announced out of band (the SegmentRef table on the control channel, with the
// fd itself passed by SCM_RIGHTS) and stay stable for their declared lifetime;
// offsets are segment-relative.
typedef struct MobileGLShmRegion {
MobileGLSegmentId segmentId;
uint32_t reserved;
uint64_t offset;
uint64_t size;
} MobileGLShmRegion;
// ---------------------------------------------------------------------------
// Result codes (structured errors across every contract boundary)
// ---------------------------------------------------------------------------
typedef enum MobileGLResult {
MOBILEGL_OK = 0,
MOBILEGL_ERR_NOT_INITIALIZED = 1,
MOBILEGL_ERR_INVALID_ARGUMENT = 2,
MOBILEGL_ERR_UNSUPPORTED = 3,
MOBILEGL_ERR_OUT_OF_MEMORY = 4,
MOBILEGL_ERR_PROTOCOL_MISMATCH = 5, // ABI/wire major mismatch, bad framing
MOBILEGL_ERR_TRANSPORT_CLOSED = 6, // peer gone / EOF
MOBILEGL_ERR_TIMEOUT = 7, // nothing arrived within the deadline
MOBILEGL_ERR_SHM_EXHAUSTED = 8,
MOBILEGL_ERR_SESSION_UNKNOWN = 9,
MOBILEGL_ERR_HANDLE_UNKNOWN = 10,
// The caller's buffer is smaller than the pending message. The message is
// NOT consumed and the required size is reported back; see
// ITransport::ReceiveFrame.
MOBILEGL_ERR_BUFFER_TOO_SMALL = 11,
MOBILEGL_ERR_FORCE_U32 = 0x7FFFFFFF
} MobileGLResult;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // MOBILEGL_REMOTE_PROTOCOL_BASE_H
+235
View File
@@ -0,0 +1,235 @@
// MobileGL - MobileGL/MG_Remote/Protocol/protocol.fbs
// 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
// MobileGL disaggregated wire protocol - CONTROL PLANE ONLY.
//
// Plan B (docs plan "MGPipe") section 8.1 inherits the transport design of the
// earlier plan verbatim, and its section 7.1 splits the schema in two:
//
// - rare / variable-length / must-evolve messages -> FlatBuffers *tables*,
// carried as complete framed messages over the control channel. That is
// everything in this file.
// - the hot path -> FlatBuffers *structs* (fixed layout, no vtable, no
// offset indirection) written straight into the SEG_CMD ring. Those
// records are generated from MG_Pipe/PipeCalls.def and are deliberately
// NOT in this schema yet: the call catalogue is a separate P0 deliverable
// and record numbering must never churn.
//
// Regeneration: scripts/gen_protocol.py (flatc is NOT part of the default
// build graph). generated/protocol_generated.h is committed and CI's
// flatc-check regenerates it and runs `git diff --exit-code`.
namespace MobileGL.Wire;
// ---------------------------------------------------------------------------
// Segments
// ---------------------------------------------------------------------------
// Segment layout is inherited unchanged (earlier plan section 6.1):
// SEG_CMD 8MiB / SEG_STAGE 32MiB+ / SEG_REPLY 8MiB / SEG_EVENT 256KiB /
// SEG_SHADOW[n] / SEG_ADOPT[n].
enum SegmentKind : ubyte {
None = 0,
Cmd = 1, // client-owned command ring (RingControl + records)
Stage = 2, // client-owned bulk staging
Reply = 3, // server-owned reply pool
Event = 4, // server-owned event ring
Shadow = 5, // client-owned per-object shadow (P4.5+)
Adopt = 6, // server-owned adopted store, client RW (>= 16MiB)
}
// The fd itself never travels in a message: POSIX passes it with SCM_RIGHTS on
// the aux socket (ITransport::ShareFd), Windows resolves `name`.
table SegmentRef {
id: uint;
kind: SegmentKind;
sizeBytes: ulong;
name: string;
}
// ---------------------------------------------------------------------------
// Handshake
// ---------------------------------------------------------------------------
table Hello {
abiMajor: uint;
abiMinor: uint;
buildFingerprint: string;
backendType: uint;
pid: uint;
configBlob: [ubyte];
}
table Welcome {
abiMajor: uint;
abiMinor: uint;
serverPid: uint;
cmdRing: SegmentRef;
stageRing: SegmentRef;
replyPool: SegmentRef;
eventRing: SegmentRef;
}
// ---------------------------------------------------------------------------
// Capabilities
// ---------------------------------------------------------------------------
// Replaces the 40 `pActiveBackendObject->` reads plus the 89 caps read sites
// (plan B appendix A, `get_caps`). The three blobs are byte-for-byte images of
// the corresponding POD structs; they are versioned by structSize-first
// discipline, not by this schema.
table CapsSnapshot {
dynamicParameters: [ubyte];
rendererInfo: [ubyte];
formatCaps: [ubyte];
extensions: [string];
apiVersion: string;
maxComputeWorkGroupCount: [int]; // 3 entries
maxComputeWorkGroupSize: [int]; // 3 entries
tableSlotMask: ulong; // which GLFunctionsTable slots the peer registered
prefersCpuXfbPrimitiveAccounting: bool;
}
table DefaultFramebufferInfo {
width: int;
height: int;
colorFormat: uint;
depthFormat: uint;
stencilFormat: uint;
}
// ---------------------------------------------------------------------------
// Surface / EGL lifecycle
// ---------------------------------------------------------------------------
enum SurfaceOpKind : ubyte {
None = 0,
InitializeDisplay = 1,
CreateWindowSurface = 2,
CreatePbufferSurface = 3,
ResizeWindowSurface = 4,
ReleaseSurface = 5,
MakeCurrent = 6,
ReleaseCurrent = 7,
}
enum WindowKind : ubyte {
None = 0,
AndroidNativeWindow = 1,
X11 = 2,
Win32Hwnd = 3,
Surfaceless = 4,
Pbuffer = 5,
}
table SurfaceOp {
seq: ulong;
kind: SurfaceOpKind;
display: ulong;
surface: ulong;
windowKind: WindowKind;
nativeToken: ulong; // X11 XID / HWND; Android transfers the window out of band
width: int;
height: int;
swapInterval: int;
}
table SurfaceReply {
seq: ulong;
ok: bool;
eglMajor: int;
eglMinor: int;
defaultFb: DefaultFramebufferInfo;
}
// ---------------------------------------------------------------------------
// Resync / aux / diagnostics
// ---------------------------------------------------------------------------
// Sent by the client after it observes a serverEpoch bump (context lost or
// server restart): every cached ring offset and every server-side object is
// gone and the whole pushed state has to be replayed.
table ResyncRequest {
serverEpoch: uint;
}
table ResyncDone {}
enum AuxRequestKind : ubyte {
None = 0,
FenceClientWait = 1,
QueryResult = 2,
ScalarGet = 3,
}
// Requests issued from a thread that is not the ring producer (foreign-thread
// sync / query polling), so they cannot take the SPSC ring.
table AuxRequest {
seq: ulong;
kind: AuxRequestKind;
payload: [ubyte];
}
enum FatalCode : uint {
None = 0,
ProtocolCorruption = 1, // record bounds / self-describing length violated
RingOverrun = 2,
SegmentMismatch = 3,
DeviceLost = 4,
ServerCrashed = 5,
AbiMismatch = 6,
}
table Fatal {
code: FatalCode;
message: string;
}
// Severity-graded per plan B section 8.2: <= Warn is lossy, >= Error is
// lossless and rate limited.
enum LogLevel : ubyte {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4,
}
table LogLine {
level: LogLevel;
text: string;
}
// ---------------------------------------------------------------------------
// Envelope
// ---------------------------------------------------------------------------
// Union tags are wire values: only ever APPEND to this list.
// ProgramReflection from the earlier plan's section 7.1 is intentionally
// absent - plan B ships program artifacts inside the create_shader_state CSO
// blob, so if a control-plane reflection message is ever needed it appends
// here rather than reserving a tag today.
union CtrlMsg {
Hello,
Welcome,
CapsSnapshot,
SurfaceOp,
SurfaceReply,
ResyncRequest,
ResyncDone,
AuxRequest,
Fatal,
LogLine,
}
table CtrlEnvelope {
msg: CtrlMsg;
}
root_type CtrlEnvelope;
file_identifier "MGLC";
+259
View File
@@ -0,0 +1,259 @@
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.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 "Doorbell.h"
#include <MG_Util/Debug/Log.h>
#include <condition_variable>
#include <mutex>
#if !defined(_WIN32)
#include <cerrno>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
// Same fallback as FdPassing.cpp: on macOS / BSD the protection is SO_NOSIGPIPE on the
// socket, set in SocketDoorbell's constructor, not a per-send flag.
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL 0
#endif
namespace MobileGL::MG_Remote::Transport {
// -----------------------------------------------------------------------
// CondVarDoorbell
// -----------------------------------------------------------------------
struct CondVarDoorbell::Impl {
std::mutex mutex;
std::condition_variable cv;
// Counted, not a flag: a wakeup that arrives while nobody is parked
// must still be observed by the next Park.
std::uint32_t signals = 0;
};
CondVarDoorbell::CondVarDoorbell() : m_impl(new Impl()) {}
CondVarDoorbell::~CondVarDoorbell() { delete m_impl; }
void CondVarDoorbell::Notify() {
{
std::lock_guard<std::mutex> lock(m_impl->mutex);
++m_impl->signals;
}
m_impl->cv.notify_one();
}
bool CondVarDoorbell::Park(std::uint32_t timeoutMs) {
std::unique_lock<std::mutex> lock(m_impl->mutex);
// The death latch is tested under the same mutex Kill sets it under, so
// a Kill cannot slip between this test and the wait below: it either
// returns here or wakes the predicate.
if (m_dead.load(std::memory_order_relaxed)) {
return false;
}
if (m_impl->signals != 0) {
--m_impl->signals;
return true;
}
if (timeoutMs == 0) {
return false;
}
const auto woken = [this] {
return m_impl->signals != 0 || m_dead.load(std::memory_order_relaxed);
};
if (timeoutMs == kWaitForever) {
m_impl->cv.wait(lock, woken);
} else if (!m_impl->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), woken)) {
return false;
}
if (m_dead.load(std::memory_order_relaxed)) {
// Woken by Kill, not by an event. The caller re-tests its condition
// regardless (Doorbell::Wait always does) and then sees Dead().
return false;
}
--m_impl->signals;
return true;
}
void CondVarDoorbell::Kill() {
{
std::lock_guard<std::mutex> lock(m_impl->mutex);
m_dead.store(true, std::memory_order_release);
}
// notify_all, not notify_one: both a raw Park and a Doorbell::Wait may
// be parked here, and after this nobody will ring again.
m_impl->cv.notify_all();
}
void CondVarDoorbell::Reset() {
std::lock_guard<std::mutex> lock(m_impl->mutex);
m_impl->signals = 0;
}
#if !defined(_WIN32)
// -----------------------------------------------------------------------
// SocketDoorbell
// -----------------------------------------------------------------------
SocketDoorbell::SocketDoorbell(int fd, std::uint8_t code, bool ownsFd)
: m_fd(fd), m_code(code), m_ownsFd(ownsFd) {
#if defined(SO_NOSIGPIPE)
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one:
// a Notify to a hung-up peer must come back as EPIPE, not as a fatal signal.
if (m_fd >= 0) {
const int one = 1;
(void)::setsockopt(m_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
}
#endif
}
SocketDoorbell::~SocketDoorbell() {
if (m_ownsFd && m_fd >= 0) {
::close(m_fd);
}
}
void SocketDoorbell::Notify() {
if (m_fd < 0) {
return;
}
const std::uint8_t byte = m_code;
for (;;) {
const ssize_t written = ::send(m_fd, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL);
if (written == 1) {
return;
}
if (written < 0 && errno == EINTR) {
continue;
}
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// The socket buffer already holds unread wakeups: the peer has
// one pending, which is all a doorbell promises.
return;
}
if (written < 0 && (errno == EPIPE || errno == ECONNRESET)) {
// The peer is gone: it can never ring back either, so latch it
// here too rather than waiting for a Park to discover it.
m_dead = true;
return;
}
MGLOG_D("MG_Remote doorbell: send failed (errno=%d)", errno);
return;
}
}
bool SocketDoorbell::Park(std::uint32_t timeoutMs) {
if (m_fd < 0 || m_dead) {
return false;
}
const auto start = std::chrono::steady_clock::now();
for (;;) {
int pollTimeout = -1;
if (timeoutMs != kWaitForever) {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
}
struct pollfd pfd{};
pfd.fd = m_fd;
pfd.events = POLLIN;
const int ready = ::poll(&pfd, 1, pollTimeout);
if (ready < 0) {
if (errno == EINTR) {
continue; // a signal is not a wakeup; keep the deadline
}
MGLOG_D("MG_Remote doorbell: poll failed (errno=%d)", errno);
return false;
}
if (ready == 0) {
return false; // timed out
}
// revents has to be inspected, not just `ready > 0`. Once the peer
// closes its end the descriptor is permanently poll-ready with
// nothing to read (measured on Linux: revents=POLLIN|POLLHUP,
// recv()==0), so treating any readiness as a wakeup turns every
// park on a dead peer into a 100% CPU spin - unbounded, because
// Doorbell::Wait re-parks until its deadline and kWaitForever has
// none.
if ((pfd.revents & (POLLERR | POLLNVAL)) != 0) {
MGLOG_D("MG_Remote doorbell: fd %d unusable (revents=0x%X)", m_fd,
static_cast<unsigned>(pfd.revents));
m_dead = true;
return false;
}
if ((pfd.revents & POLLIN) != 0) {
if (Drain() != 0) {
return true; // a real wakeup byte
}
if (m_dead) {
return false; // EOF, not an event
}
// Ready but empty and still alive: someone else drained it.
// Report the wakeup and let the caller re-test its condition.
return true;
}
if ((pfd.revents & POLLHUP) != 0) {
m_dead = true;
return false;
}
// Readiness with no bit we requested or recognise: there is
// nothing to consume and no way to make progress, so refuse to
// poll this descriptor again.
MGLOG_D("MG_Remote doorbell: fd %d ready with revents=0x%X", m_fd,
static_cast<unsigned>(pfd.revents));
m_dead = true;
return false;
}
}
std::uint64_t SocketDoorbell::Drain() {
// Level-triggered to edge-triggered: swallow every queued byte so one
// stale wakeup cannot make later Parks return without an event.
std::uint64_t consumed = 0;
std::uint8_t scratch[64];
for (;;) {
const ssize_t got = ::recv(m_fd, scratch, sizeof(scratch), MSG_DONTWAIT);
if (got > 0) {
consumed += static_cast<std::uint64_t>(got);
continue;
}
if (got == 0) {
// Orderly shutdown on a stream socket: the peer is gone and
// will never ring again.
m_dead = true;
return consumed;
}
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return consumed; // drained
}
MGLOG_D("MG_Remote doorbell: recv failed (errno=%d)", errno);
m_dead = true;
return consumed;
}
}
void SocketDoorbell::Reset() {
if (m_fd < 0 || m_dead) {
return;
}
(void)Drain();
}
#endif // !_WIN32
} // namespace MobileGL::MG_Remote::Transport
+268
View File
@@ -0,0 +1,268 @@
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.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 bidirectional doorbell: spin briefly, then park.
//
// Both directions exist, and that is the point (inherited design, earlier plan
// section 6.2a):
// - client -> server: the consumer spins, sets consumerParked, then blocks;
// the producer rings only when consumerParked is set.
// - server -> client: the client spins MOBILEGL_IPC_SPIN_US (default 50us),
// sets producerParked, then blocks; the server rings after advancing any
// watermark, only when producerParked is set.
// Without the second direction every client wait - present credit, a blocking
// kNeedsAck request, a full ring - degenerates into a cross-process spin on
// one shared cache line: up to a whole frame of a big core at full clock on a
// phone, fighting the GPU and the game's JVM for it. MobileGL has no affinity
// control anywhere in the tree, so it cannot even be pushed to a little core.
//
// Two implementations, no platform-specific wakeup primitive (no futex, no
// eventfd, no named event):
// - CondVarDoorbell for `inproc` (one process, two threads),
// - SocketDoorbell for `spawn` (one byte on a socket; POSIX only).
//
// The lost-wakeup window is closed by two seq_cst FENCES, not by the ordering
// of the park flag's own load and store:
// - the waiter sets the flag, executes std::atomic_thread_fence(seq_cst),
// and THEN re-tests the condition (Doorbell::Wait);
// - the notifier publishes its watermark, executes the same fence, and THEN
// reads the flag (NotifyIfParked).
// Both fences sit in the single seq_cst total order, so one precedes the
// other, and [atomics.order] then forces at least one side to observe the
// other's store. The flag's own accesses may be relaxed: they are not what
// closes the window.
//
// A seq_cst store paired with a seq_cst load would NOT be enough, which is
// why the fences are here and why neither may be removed. That Dekker
// argument needs all FOUR accesses in the total order, and the other two are
// not: the watermark publish is a release store (RingProducer::Publish) and
// the condition re-test is an acquire load. On x86 the gap is concrete rather
// than theoretical - a release store is a plain MOV that can still sit in the
// store buffer while the load of the park flag, also a plain MOV, reads 0, so
// the notifier skips the ring and the waiter parks on a stale watermark
// forever. (ARMv8 survives it only because STLR->LDAR is RCsc, i.e. by luck.)
//
// The other half of the contract is ordering between the caller and the
// fence: NotifyIfParked must be called AFTER the watermark is published. A
// fence only orders what precedes it.
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#if defined(__x86_64__) || defined(__i386__)
#include <immintrin.h>
#endif
namespace MobileGL::MG_Remote::Transport {
// MOBILEGL_IPC_SPIN_US default.
inline constexpr std::uint32_t kDefaultSpinUs = 50;
// Park with no deadline.
inline constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
// Wire codes, so a shared socket can carry both directions distinguishably.
inline constexpr std::uint8_t kDoorbellRingAdvanced = 0x01; // client -> server
inline constexpr std::uint8_t kDoorbellWatermarkAdvanced = 0x02; // server -> client
inline void CpuRelax() {
#if defined(__x86_64__) || defined(__i386__)
_mm_pause();
#elif defined(__aarch64__) || defined(__arm__)
__asm__ __volatile__("yield" ::: "memory");
#else
std::atomic_signal_fence(std::memory_order_seq_cst);
#endif
}
class Doorbell {
public:
virtual ~Doorbell() = default;
Doorbell(const Doorbell&) = delete;
Doorbell& operator=(const Doorbell&) = delete;
// Wakes a parked peer. Cheap and idempotent: a wakeup that arrives when
// nobody is parked is remembered, so the next Park returns immediately
// rather than sleeping through an event that already happened.
virtual void Notify() = 0;
// Blocks until notified or the deadline passes. Returns true when a
// wakeup was consumed. timeoutMs == 0 polls; kWaitForever never times
// out.
virtual bool Park(std::uint32_t timeoutMs) = 0;
// Drops pending wakeups. Used when a waiter gives up, so a stale byte
// does not make the next Park return spuriously forever.
virtual void Reset() = 0;
// True once the wakeup channel is permanently unusable: the peer closed
// its end of the socket, or the inproc channel was shut down. A dead
// doorbell can never deliver another wakeup, and Wait must stop
// re-parking on it - for the socket because its descriptor is
// permanently poll-ready and a waiter with no deadline would burn a
// big core at full clock, for the condvar because Park would otherwise
// block forever and Shutdown could never join the waiter. Every
// implementation has a death state; the base default is only for a
// bell that cannot die.
virtual bool Dead() const { return false; }
// Spin `spinUs`, then park until `ready()` or the deadline.
// `parked` is the RingControl flag the peer tests before ringing.
template <class Ready>
bool Wait(std::atomic<std::uint32_t>& parked, Ready&& ready, std::uint32_t spinUs,
std::uint32_t timeoutMs) {
if (ready()) {
return true;
}
const auto start = std::chrono::steady_clock::now();
const auto deadline = timeoutMs == kWaitForever
? std::chrono::steady_clock::time_point::max()
: start + std::chrono::milliseconds(timeoutMs);
const auto spinEnd = start + std::chrono::microseconds(spinUs);
while (std::chrono::steady_clock::now() < spinEnd) {
if (ready()) {
return true;
}
CpuRelax();
}
for (;;) {
// Announce, FENCE, then re-test. The fence is the mechanism -
// see the file header - so setting the flag itself is relaxed.
parked.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if (ready()) {
parked.store(0, std::memory_order_relaxed);
return true;
}
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
parked.store(0, std::memory_order_relaxed);
return ready();
}
std::uint32_t chunkMs = kWaitForever;
if (timeoutMs != kWaitForever) {
const auto remaining =
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count();
chunkMs = remaining <= 0 ? 0 : static_cast<std::uint32_t>(remaining);
}
Park(chunkMs);
// Clearing is relaxed on purpose: a notifier that reads a
// stale 1 only rings a bell nobody is waiting on, which the
// doorbell remembers and the next Park consumes. The dangerous
// direction - a notifier reading 0 while the waiter is really
// parked - is the one the fence above rules out.
parked.store(0, std::memory_order_relaxed);
if (ready()) {
return true;
}
if (Dead()) {
// Nothing can ring this bell again and parking on it no
// longer blocks, so looping here would spin at full clock
// for as long as the caller is willing to wait - which,
// with kWaitForever, is forever.
return false;
}
if (timeoutMs != kWaitForever && std::chrono::steady_clock::now() >= deadline) {
return false;
}
}
}
protected:
Doorbell() = default;
};
// Rings `bell` only when the peer said it is parked.
//
// PRECONDITION: whatever the waiter's condition reads - the ring head, a
// sequence watermark, a queue push - is ALREADY published when this is
// called. The fence only orders what precedes it, so ringing before
// publishing reopens the window this closes. The fence pairs with the one
// in Doorbell::Wait; see the file header for why the flag's own memory
// order is not what makes this sound.
inline void NotifyIfParked(Doorbell& bell, std::atomic<std::uint32_t>& parked) {
std::atomic_thread_fence(std::memory_order_seq_cst);
if (parked.load(std::memory_order_relaxed) != 0) {
bell.Notify();
}
}
// `inproc`: one process, two threads.
class CondVarDoorbell final : public Doorbell {
public:
CondVarDoorbell();
~CondVarDoorbell() override;
void Notify() override;
bool Park(std::uint32_t timeoutMs) override;
void Reset() override;
bool Dead() const override { return m_dead.load(std::memory_order_acquire); }
// Hangs the bell up for good: every parked waiter returns false now and
// every later Park returns false at once. The inproc twin of the socket
// peer closing its end (SocketDoorbell latches m_dead on EOF), and what
// InProcessChannel::Close rings instead of Notify. A Notify is consumed
// by ONE Park; Doorbell::Wait then re-tests its condition, finds
// nothing published, finds the bell alive, and with kWaitForever parks
// again - so a Shutdown that only rang could never join a server thread
// sitting in the design's own steady state (spun, set consumerParked,
// blocked). Irreversible by design, like the socket's.
void Kill();
private:
struct Impl;
Impl* m_impl;
std::atomic<bool> m_dead{false};
};
#if !defined(_WIN32)
// `spawn`: one byte on a socket (one direction of a socketpair, or the aux
// socket). POSIX only; the Windows path will use an overlapped named pipe
// and is not part of this skeleton.
class SocketDoorbell final : public Doorbell {
public:
// `fd` must be one end of an AF_UNIX socket pair, not a pipe: Notify
// uses send() with MSG_DONTWAIT|MSG_NOSIGNAL and Park uses
// poll()+recv(), which a pipe end refuses with ENOTSOCK. Prefer
// SOCK_STREAM for the spawn transport - measured on Linux, a closed
// peer makes a stream end report POLLIN|POLLHUP with recv()==0, which
// is how death is detected, while a SOCK_DGRAM end reports no
// readiness at all and a waiter with no deadline would simply hang.
// When `ownsFd` the descriptor is closed with this object. `code` is
// the byte written by Notify.
SocketDoorbell(int fd, std::uint8_t code, bool ownsFd);
~SocketDoorbell() override;
void Notify() override;
bool Park(std::uint32_t timeoutMs) override;
void Reset() override;
bool Dead() const override { return m_dead; }
int Fd() const { return m_fd; }
private:
// Consumes every queued wakeup byte and returns how many. Latches
// m_dead on EOF: recv returning 0 on a stream socket is the peer's
// hangup, not a wakeup, and the descriptor stays poll-ready forever
// afterwards.
std::uint64_t Drain();
int m_fd;
std::uint8_t m_code;
bool m_ownsFd;
bool m_dead = false;
};
#endif
} // namespace MobileGL::MG_Remote::Transport
+323
View File
@@ -0,0 +1,323 @@
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.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 "FdPassing.h"
#include <MG_Util/Debug/Log.h>
#include <chrono>
#include <cstring>
#if !defined(_WIN32)
#include <cerrno>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#endif
// MSG_NOSIGNAL is Linux (and Android). macOS and the BSDs spell the same protection as the
// SO_NOSIGPIPE socket option, set once per socket at creation (CreateSocketPair below, and
// SocketDoorbell's constructor). With neither, a write to a hung-up peer raises SIGPIPE and
// kills the process instead of returning EPIPE.
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL 0
#endif
namespace MobileGL::MG_Remote::Transport::FdPassing {
#if defined(_WIN32)
bool Supported() { return false; }
MobileGLResult CreateSocketPair(int[2]) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult SendFd(int, int, MobileGLByteSpan) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult ReceiveFd(int, int*, MobileGLMutableByteSpan, std::uint64_t*, std::uint32_t) {
return MOBILEGL_ERR_UNSUPPORTED;
}
#else
namespace {
// Every datagram starts with this, so the sideband length is explicit
// and a stray datagram is recognisable.
struct SidebandHeader {
std::uint32_t magic;
std::uint32_t sidebandSize;
};
constexpr std::uint32_t kSidebandMagic = 0x4446474Du; // 'MGFD' on the wire
int WaitReadable(int socket, std::uint32_t timeoutMs) {
const auto start = std::chrono::steady_clock::now();
for (;;) {
int pollTimeout = -1;
if (timeoutMs != 0xFFFFFFFFu) {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
}
struct pollfd pfd{};
pfd.fd = socket;
pfd.events = POLLIN;
const int ready = ::poll(&pfd, 1, pollTimeout);
if (ready < 0 && errno == EINTR) {
continue;
}
return ready;
}
}
} // namespace
bool Supported() { return true; }
MobileGLResult CreateSocketPair(int outFds[2]) {
if (outFds == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
int fds[2] = {-1, -1};
int type = SOCK_DGRAM;
#if defined(SOCK_CLOEXEC)
type |= SOCK_CLOEXEC;
#endif
if (::socketpair(AF_UNIX, type, 0, fds) != 0) {
MGLOG_E("MG_Remote fd passing: socketpair failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
#if defined(SO_NOSIGPIPE)
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one.
for (int fd : fds) {
const int one = 1;
(void)::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
}
#endif
outFds[0] = fds[0];
outFds[1] = fds[1];
return MOBILEGL_OK;
}
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband) {
if (socket < 0 || fd < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (sideband.size > kMaxSidebandBytes || (sideband.size != 0 && sideband.data == nullptr)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
SidebandHeader header{};
header.magic = kSidebandMagic;
header.sidebandSize = static_cast<std::uint32_t>(sideband.size);
std::memcpy(payload, &header, sizeof(header));
if (sideband.size != 0) {
std::memcpy(payload + sizeof(header), sideband.data,
static_cast<std::size_t>(sideband.size));
}
const std::size_t payloadSize = sizeof(header) + static_cast<std::size_t>(sideband.size);
struct iovec iov{};
iov.iov_base = payload;
iov.iov_len = payloadSize;
// CMSG_SPACE, not sizeof: the control buffer has to hold the aligned
// cmsghdr as well as the descriptor.
union {
struct cmsghdr align;
char bytes[CMSG_SPACE(sizeof(int))];
} control{};
std::memset(&control, 0, sizeof(control));
struct msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control.bytes;
msg.msg_controllen = sizeof(control.bytes);
struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
std::memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
for (;;) {
const ssize_t sent = ::sendmsg(socket, &msg, MSG_NOSIGNAL);
if (sent >= 0) {
if (static_cast<std::size_t>(sent) != payloadSize) {
// A datagram socket sends all or nothing.
MGLOG_E("MG_Remote fd passing: short datagram (%zd of %zu bytes)", sent,
payloadSize);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
return MOBILEGL_OK;
}
if (errno == EINTR) {
continue;
}
if (errno == EPIPE || errno == ECONNRESET) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MGLOG_E("MG_Remote fd passing: sendmsg failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
}
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) {
if (socket < 0 || outFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outFd = -1;
if (outSidebandSize != nullptr) {
*outSidebandSize = 0;
}
// Checked before the recvmsg: a datagram cannot be partially consumed,
// so a too-small destination must never cost us the descriptor.
if (sideband.size < kMaxSidebandBytes) {
if (outSidebandSize != nullptr) {
*outSidebandSize = kMaxSidebandBytes;
}
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (sideband.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const int ready = WaitReadable(socket, timeoutMs);
if (ready < 0) {
MGLOG_E("MG_Remote fd passing: poll failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (ready == 0) {
return MOBILEGL_ERR_TIMEOUT;
}
std::uint8_t payload[sizeof(SidebandHeader) + kMaxSidebandBytes];
struct iovec iov{};
iov.iov_base = payload;
iov.iov_len = sizeof(payload);
union {
struct cmsghdr align;
char bytes[CMSG_SPACE(sizeof(int) * 4)];
} control{};
std::memset(&control, 0, sizeof(control));
struct msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control.bytes;
msg.msg_controllen = sizeof(control.bytes);
ssize_t got = 0;
for (;;) {
int flags = 0;
#if defined(MSG_CMSG_CLOEXEC)
flags |= MSG_CMSG_CLOEXEC;
#endif
got = ::recvmsg(socket, &msg, flags);
if (got >= 0) {
break;
}
if (errno == EINTR) {
continue;
}
if (errno == ECONNRESET) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MGLOG_E("MG_Remote fd passing: recvmsg failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
if (got == 0) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
// Collect every descriptor first, so an unexpected extra one is closed
// rather than leaked, whatever else is wrong with the message.
int received[4];
int receivedCount = 0;
for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); cmsg != nullptr;
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
continue;
}
const std::size_t bytes = cmsg->cmsg_len - CMSG_LEN(0);
const int count = static_cast<int>(bytes / sizeof(int));
for (int i = 0; i < count && receivedCount < 4; ++i) {
int fd = -1;
std::memcpy(&fd, CMSG_DATA(cmsg) + i * sizeof(int), sizeof(fd));
received[receivedCount++] = fd;
}
}
#if !defined(MSG_CMSG_CLOEXEC)
// No atomic close-on-exec on receive here (macOS, the BSDs): set it by hand on every
// descriptor that arrived, before anything else can fork. The window between the
// recvmsg and this loop is the platform's, not ours; leaving the flag off altogether
// would hand every shared segment to every child the process ever spawns.
for (int i = 0; i < receivedCount; ++i) {
if (received[i] >= 0) {
(void)::fcntl(received[i], F_SETFD, FD_CLOEXEC);
}
}
#endif
const auto closeAll = [&](int keepIndex) {
for (int i = 0; i < receivedCount; ++i) {
if (i != keepIndex && received[i] >= 0) {
::close(received[i]);
}
}
};
if ((msg.msg_flags & MSG_CTRUNC) != 0) {
// The kernel dropped ancillary data: whatever arrived is not a
// complete offer, and silently continuing would hand the caller a
// half-transferred segment.
MGLOG_E("MG_Remote fd passing: ancillary data truncated; the descriptor did not "
"arrive intact");
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (receivedCount != 1) {
MGLOG_E("MG_Remote fd passing: expected exactly one descriptor, got %d", receivedCount);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (static_cast<std::size_t>(got) < sizeof(SidebandHeader)) {
MGLOG_E("MG_Remote fd passing: %zd byte datagram is shorter than the header", got);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
SidebandHeader header{};
std::memcpy(&header, payload, sizeof(header));
if (header.magic != kSidebandMagic ||
header.sidebandSize > kMaxSidebandBytes ||
sizeof(SidebandHeader) + header.sidebandSize != static_cast<std::size_t>(got)) {
MGLOG_E("MG_Remote fd passing: bad sideband header (magic=0x%08X size=%u datagram=%zd)",
header.magic, header.sidebandSize, got);
closeAll(-1);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (header.sidebandSize != 0) {
std::memcpy(sideband.data, payload + sizeof(SidebandHeader), header.sidebandSize);
}
if (outSidebandSize != nullptr) {
*outSidebandSize = header.sidebandSize;
}
*outFd = received[0];
closeAll(0);
return MOBILEGL_OK;
}
#endif // _WIN32
} // namespace MobileGL::MG_Remote::Transport::FdPassing
+67
View File
@@ -0,0 +1,67 @@
// MobileGL - MobileGL/MG_Remote/Transport/FdPassing.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
// SCM_RIGHTS descriptor passing over an AF_UNIX socket pair. POSIX only.
//
// This is the FIRST transport commit, deliberately (inherited design, plan
// section 8.1, "SCM_RIGHTS must be implemented in the first transport
// commit"). The earlier branch pushed it to a later phase and hardcoded
// `out->fd = -1` in its offer poll, so on the only platform that matters its
// data plane could never move a byte: every segment announcement resolved to
// "no descriptor". A transport whose shm cannot cross the process boundary is
// not a transport.
//
// Channel shape: a dedicated AF_UNIX SOCK_DGRAM socketpair, NOT the control
// byte stream. Two reasons:
// - SOCK_DGRAM preserves message boundaries on every POSIX (SOCK_SEQPACKET
// does not exist on macOS), so one sendmsg is exactly one recvmsg and the
// ancillary data can never be split away from its payload;
// - ancillary data attached to a byte stream binds to whichever ordinary
// byte happens to be at the front of the reader's buffer, which is
// unmanageable once frames are being reassembled.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstdint>
namespace MobileGL::MG_Remote::Transport::FdPassing {
// Upper bound for the bytes that travel with a descriptor (a SegmentRef
// sized announcement, not payload).
inline constexpr std::uint64_t kMaxSidebandBytes = 256;
// False on platforms without SCM_RIGHTS (Windows).
bool Supported();
// Creates the aux socket pair. Both descriptors are CLOEXEC and owned by
// the caller. outFds[0] is conventionally the client end, [1] the server's
// (the one that is inherited or passed to the spawned process).
MobileGLResult CreateSocketPair(int outFds[2]);
// Sends `fd` with `sideband` attached. The caller keeps ownership of `fd`
// (the peer gets its own descriptor for the same open file description).
// sideband.size must be <= kMaxSidebandBytes.
MobileGLResult SendFd(int socket, int fd, MobileGLByteSpan sideband);
// Receives one descriptor and its sideband bytes.
//
// `sideband` must be at least kMaxSidebandBytes: a datagram cannot be
// partially consumed, so the capacity is checked BEFORE anything is read.
// A short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with
// *outSidebandSize = kMaxSidebandBytes and consumes nothing, so no
// descriptor is ever dropped on the floor.
//
// On success *outFd owns a descriptor this process must close.
// MOBILEGL_ERR_TIMEOUT when nothing arrived (timeoutMs 0 = poll),
// MOBILEGL_ERR_TRANSPORT_CLOSED on peer close.
MobileGLResult ReceiveFd(int socket, int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs);
} // namespace MobileGL::MG_Remote::Transport::FdPassing
+208
View File
@@ -0,0 +1,208 @@
// MobileGL - MobileGL/MG_Remote/Transport/Framing.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
// Control-channel wire framing: [u32 magic 'MGLF'][u32 payloadLength][payload].
// Length excludes the 8-byte header and is capped at 64 MiB.
//
// Two defects of the earlier branch's codec are fixed here, and both are the
// reason this file is not a copy of it:
//
// 1. Its Feed() unconditionally returned OK and its header peek merely
// returned false on a bad magic or an oversized length. A corrupt or
// desynchronized stream therefore turned into a silent, permanent hang -
// the reader kept waiting for a message that could never be parsed, with
// no error anywhere. Here a violation latches a failed state, is logged at
// ERROR, and every later call returns MOBILEGL_ERR_PROTOCOL_MISMATCH.
//
// 2. Its receive path failed the call and consumed the message when the
// caller's buffer was too small, wedging the stream. Here
// MOBILEGL_ERR_BUFFER_TOO_SMALL reports the required size and KEEPS the
// message queued.
//
// The reader is a plain byte-stream reassembler: it never assumes a read()
// returned a whole frame.
#pragma once
#include "../Protocol/mg_protocol_base.h"
// NOT <MG_Util/Debug/Log.h>: that header pulls the GL frontend's umbrella into
// every translation unit that reassembles a frame. See WireLog.h.
#include "WireLog.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
namespace MobileGL::MG_Remote::Transport {
// 'MGLF', little-endian on the wire (both ends are the same machine).
inline constexpr std::uint32_t kFrameMagic = 0x464C474Du;
inline constexpr std::uint64_t kFrameHeaderSize = 8;
inline constexpr std::uint64_t kMaxFramePayloadSize = 64ull * 1024 * 1024;
// Compaction threshold: consumed bytes are dropped from the front once
// enough of them accumulate, so a long-lived reader neither memmoves per
// message nor grows without bound.
inline constexpr std::uint64_t kFrameReaderCompactThreshold = 64ull * 1024;
// Appends one framed message to `out`.
inline MobileGLResult AppendFrame(std::vector<std::uint8_t>& out, const void* payload,
std::uint64_t size) {
if (size > kMaxFramePayloadSize) {
WireLogError("MG_Remote framing: refusing to send a %llu byte payload (cap %llu); "
"bulk bytes belong in shm",
static_cast<unsigned long long>(size),
static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (size != 0 && payload == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::uint8_t header[kFrameHeaderSize];
const std::uint32_t magic = kFrameMagic;
const std::uint32_t length = static_cast<std::uint32_t>(size);
std::memcpy(header + 0, &magic, sizeof(magic));
std::memcpy(header + 4, &length, sizeof(length));
out.insert(out.end(), header, header + kFrameHeaderSize);
const auto* bytes = static_cast<const std::uint8_t*>(payload);
out.insert(out.end(), bytes, bytes + size);
return MOBILEGL_OK;
}
// Incremental frame extractor over a raw byte stream.
class FrameReader {
public:
// Feeds raw stream bytes. Validates the frame header the moment enough
// bytes for one exist - a bad magic or an oversized length is reported
// here, not swallowed.
MobileGLResult Feed(const void* data, std::uint64_t size) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (size != 0) {
if (data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const auto* bytes = static_cast<const std::uint8_t*>(data);
m_buffer.insert(m_buffer.end(), bytes, bytes + size);
}
return ParseHeader();
}
bool Failed() const { return m_failed; }
bool HasMessage() const {
return !m_failed && m_haveHeader && Available() >= kFrameHeaderSize + m_pendingSize;
}
// Size of the next complete message, or 0 when none is complete yet.
std::uint64_t PendingMessageSize() const { return HasMessage() ? m_pendingSize : 0; }
std::uint64_t BufferedBytes() const { return Available(); }
// Copies the next complete message out.
// MOBILEGL_OK - copied, *outSize set, message consumed
// MOBILEGL_ERR_BUFFER_TOO_SMALL - *outSize = required size, message KEPT
// MOBILEGL_ERR_TIMEOUT - no complete message buffered
// MOBILEGL_ERR_PROTOCOL_MISMATCH- the stream is latched failed
MobileGLResult TakeMessage(MobileGLMutableByteSpan buffer, std::uint64_t* outSize) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (!HasMessage()) {
return MOBILEGL_ERR_TIMEOUT;
}
if (outSize != nullptr) {
*outSize = m_pendingSize;
}
if (buffer.size < m_pendingSize) {
// The message stays queued; the caller retries with a big
// enough buffer.
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (m_pendingSize != 0) {
if (buffer.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::memcpy(buffer.data, m_buffer.data() + m_readPos + kFrameHeaderSize,
static_cast<std::size_t>(m_pendingSize));
}
Consume();
return MOBILEGL_OK;
}
// Convenience overload that sizes the destination itself.
MobileGLResult TakeMessage(std::vector<std::uint8_t>& out) {
if (m_failed) {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (!HasMessage()) {
return MOBILEGL_ERR_TIMEOUT;
}
const auto* first = m_buffer.data() + m_readPos + kFrameHeaderSize;
out.assign(first, first + m_pendingSize);
Consume();
return MOBILEGL_OK;
}
private:
std::uint64_t Available() const { return m_buffer.size() - m_readPos; }
MobileGLResult ParseHeader() {
if (m_haveHeader || Available() < kFrameHeaderSize) {
return MOBILEGL_OK;
}
std::uint32_t magic = 0;
std::uint32_t length = 0;
std::memcpy(&magic, m_buffer.data() + m_readPos, sizeof(magic));
std::memcpy(&length, m_buffer.data() + m_readPos + 4, sizeof(length));
if (magic != kFrameMagic) {
m_failed = true;
WireLogError("MG_Remote framing: bad frame magic 0x%08X (expected 0x%08X); the "
"control stream is desynchronized and this transport is now dead",
magic, kFrameMagic);
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (length > kMaxFramePayloadSize) {
m_failed = true;
WireLogError("MG_Remote framing: frame length %u exceeds the %llu byte cap; "
"refusing to allocate on a peer-supplied length",
length, static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
m_pendingSize = length;
m_haveHeader = true;
return MOBILEGL_OK;
}
void Consume() {
m_readPos += kFrameHeaderSize + m_pendingSize;
m_pendingSize = 0;
m_haveHeader = false;
if (m_readPos == m_buffer.size()) {
m_buffer.clear();
m_readPos = 0;
} else if (m_readPos >= kFrameReaderCompactThreshold) {
m_buffer.erase(m_buffer.begin(),
m_buffer.begin() + static_cast<std::ptrdiff_t>(m_readPos));
m_readPos = 0;
}
// Header of the next message may already be buffered.
(void)ParseHeader();
}
std::vector<std::uint8_t> m_buffer;
std::uint64_t m_readPos = 0;
std::uint64_t m_pendingSize = 0;
bool m_haveHeader = false;
bool m_failed = false;
};
} // namespace MobileGL::MG_Remote::Transport
+130
View File
@@ -0,0 +1,130 @@
// MobileGL - MobileGL/MG_Remote/Transport/ITransport.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 control-plane transport interface.
//
// It is deliberately dumb: complete messages in, complete messages out, plus
// the one thing shared memory cannot do without help - handing a file
// descriptor to the peer. No session routing, no seq accounting, no
// serialization; those live above, in the protocol layer.
//
// Everything on the hot path bypasses this interface entirely: records go into
// the SEG_CMD ring (Ring.h) and the peer is woken through a Doorbell
// (Doorbell.h). ITransport carries the handshake, surface ops, resync, aux
// requests and fatals - the rare, variable-length, must-evolve traffic that
// plan section 7.1 assigns to FlatBuffers tables.
//
// This header stays dependency-light on purpose (mg_protocol_base.h plus the
// standard library): it is included by both roles and by the eventual
// server-side binary, and nothing about a byte pipe needs the GL frontend's
// umbrella header.
//
// Threading: one instance is not internally synchronized for send; callers
// serialize sends. ReceiveFrame/ReceiveFd may be called from one dedicated
// reader thread concurrently with sends from another.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// Which end of the connection this instance is.
enum class TransportRole : std::uint32_t {
Server = 1, // accepts the client connection
Client = 2, // connects to the server endpoint
InProcess = 3, // same-process hand-off (CI / inproc delivery mode)
};
class ITransport {
public:
virtual ~ITransport() = default;
ITransport(const ITransport&) = delete;
ITransport& operator=(const ITransport&) = delete;
// ---- control plane -------------------------------------------------
// Sends one complete message. `bytes` is borrowed: the implementation
// either copies it or completes the underlying write before returning.
// A payload larger than Framing::kMaxFramePayloadSize is rejected with
// MOBILEGL_ERR_INVALID_ARGUMENT - bulk bytes belong in shm, never here.
virtual MobileGLResult SendFrame(MobileGLByteSpan bytes) = 0;
// Receives the next complete message.
//
// MOBILEGL_OK - copied into `buffer`, *outSize is
// the message size, message consumed.
// MOBILEGL_ERR_BUFFER_TOO_SMALL - `buffer` is too small. *outSize is
// the size required and THE MESSAGE
// STAYS QUEUED: call again with a
// buffer of at least that size and it
// is still there.
// MOBILEGL_ERR_TIMEOUT - nothing arrived within timeoutMs
// (0 = non-blocking poll).
// MOBILEGL_ERR_TRANSPORT_CLOSED - peer gone, nothing left buffered.
// MOBILEGL_ERR_PROTOCOL_MISMATCH- framing violated; the transport is
// latched failed and never recovers.
//
// The buffer-too-small half of that contract is the whole point of
// having one: the earlier branch's transport failed the call AND
// dropped the message, which wedges the stream permanently the first
// time a message is bigger than the reader's guess.
virtual MobileGLResult ReceiveFrame(MobileGLMutableByteSpan buffer, std::uint64_t* outSize,
std::uint32_t timeoutMs) = 0;
// Size of the next pending message, or 0 when none is buffered. Lets a
// caller size its buffer without a failed receive first.
virtual std::uint64_t PeekFrameSize() = 0;
// ---- descriptor passing --------------------------------------------
// Hands `fd` to the peer. POSIX: SCM_RIGHTS over the aux socket (see
// FdPassing.h). Windows: not applicable, returns
// MOBILEGL_ERR_UNSUPPORTED - the section name travels inside SegmentRef
// instead. The caller keeps ownership of `fd` and closes it itself.
//
// This is a first-class member of the interface, not a later phase: the
// earlier branch deferred it and hardcoded `out->fd = -1` in its offer
// poll, so its data plane could not move a single byte on the only
// platform that matters.
virtual MobileGLResult ShareFd(int fd, MobileGLByteSpan sideband) = 0;
// Receives one fd previously shared by the peer. On success *outFd owns
// a descriptor this process must close. `sideband` receives the bytes
// that travelled with it (may be empty) and must be at least
// FdPassing::kMaxSidebandBytes: an fd offer is one datagram and cannot
// be half-consumed, so the capacity is checked BEFORE anything is read
// and a short buffer returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the
// required size, having consumed nothing and dropped no descriptor.
virtual MobileGLResult ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) = 0;
// ---- lifecycle ------------------------------------------------------
// Idempotent. Tears down the WHOLE connection, not just this end:
// both directions are half-closed, so after either endpoint calls it
// neither side can send any more (SendFrame returns
// MOBILEGL_ERR_TRANSPORT_CLOSED) and every waiter on either side is
// unblocked. That is what closing a socket does, and the spawn
// transport behaves the same way, so a one-sided contract here would
// be a promise only the in-process implementation could keep.
//
// Messages already queued stay readable until drained: a peer that
// shuts down right after sending does not lose its last message.
virtual void Shutdown() = 0;
virtual TransportRole Role() const = 0;
protected:
ITransport() = default;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -0,0 +1,293 @@
// MobileGL - MobileGL/MG_Remote/Transport/InProcessTransport.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 "InProcessTransport.h"
#include "FdPassing.h"
#include "Framing.h"
#include <MG_Util/Debug/Log.h>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <mutex>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
struct FdOffer {
int fd = -1;
std::vector<std::uint8_t> sideband;
};
} // namespace
// One direction of the channel: everything queued FOR one endpoint.
class InProcessChannel {
public:
struct Direction {
std::mutex mutex;
// One variable per predicate. A single cv signalled with
// notify_one would let a SendFrame's wakeup land on a thread
// blocked in ReceiveFd, which re-tests its own predicate and goes
// straight back to sleep - leaving a queued message undelivered
// until some unrelated later event. ITransport narrows the
// contract to one dedicated reader thread, but a comment is not a
// reason to ship a primitive that breaks the moment someone
// splits the reader.
std::condition_variable cv; // messages
std::condition_variable fdCv; // fdOffers
std::deque<std::vector<std::uint8_t>> messages;
std::deque<FdOffer> fdOffers;
bool closed = false;
};
~InProcessChannel() {
for (Direction& dir : m_directions) {
for (FdOffer& offer : dir.fdOffers) {
#if !defined(_WIN32)
if (offer.fd >= 0) {
::close(offer.fd);
}
#endif
}
dir.fdOffers.clear();
}
}
Direction& Inbox(int endpoint) { return m_directions[endpoint]; }
Direction& Outbox(int endpoint) { return m_directions[1 - endpoint]; }
CondVarDoorbell& Bell(int endpoint) { return m_bells[endpoint]; }
void Close() {
for (Direction& dir : m_directions) {
{
std::lock_guard<std::mutex> lock(dir.mutex);
dir.closed = true;
}
dir.cv.notify_all();
dir.fdCv.notify_all();
}
// Anything parked on a ring doorbell has to come back too, or a
// shutdown mid-frame hangs the peer forever. Kill, not Notify: a
// ring is consumed by one Park, after which Doorbell::Wait re-tests
// a condition nothing published and - the bell still reporting
// alive - parks again, with no deadline forever. Only Dead() ends
// that loop.
for (CondVarDoorbell& bell : m_bells) {
bell.Kill();
}
}
private:
Direction m_directions[2];
CondVarDoorbell m_bells[2];
};
InProcessTransport::InProcessTransport(std::shared_ptr<InProcessChannel> channel, int endpoint)
: m_channel(std::move(channel)), m_endpoint(endpoint) {}
InProcessTransport::~InProcessTransport() = default;
void InProcessTransport::CreatePair(std::unique_ptr<InProcessTransport>& outClient,
std::unique_ptr<InProcessTransport>& outServer) {
auto channel = std::make_shared<InProcessChannel>();
outClient.reset(new InProcessTransport(channel, 0));
outServer.reset(new InProcessTransport(channel, 1));
}
MobileGLResult InProcessTransport::SendFrame(MobileGLByteSpan bytes) {
if (bytes.size != 0 && bytes.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Same cap as the byte-stream transports, so nothing legal here becomes
// illegal the day the delivery mode changes to `spawn`.
if (bytes.size > kMaxFramePayloadSize) {
MGLOG_E("MG_Remote inproc: refusing a %llu byte message (cap %llu)",
static_cast<unsigned long long>(bytes.size),
static_cast<unsigned long long>(kMaxFramePayloadSize));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
{
std::lock_guard<std::mutex> lock(dir.mutex);
if (dir.closed) {
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
const auto* first = static_cast<const std::uint8_t*>(bytes.data);
dir.messages.emplace_back(first, first + bytes.size);
}
dir.cv.notify_one();
return MOBILEGL_OK;
}
MobileGLResult InProcessTransport::ReceiveFrame(MobileGLMutableByteSpan buffer,
std::uint64_t* outSize,
std::uint32_t timeoutMs) {
if (outSize != nullptr) {
*outSize = 0;
}
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::unique_lock<std::mutex> lock(dir.mutex);
if (dir.messages.empty() && !dir.closed && timeoutMs != 0) {
const auto ready = [&dir] { return !dir.messages.empty() || dir.closed; };
if (timeoutMs == kWaitForever) {
dir.cv.wait(lock, ready);
} else {
dir.cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
}
}
if (dir.messages.empty()) {
// Queued messages outlive the peer's Shutdown; only an empty inbox
// is a closed one.
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
}
const std::vector<std::uint8_t>& front = dir.messages.front();
const std::uint64_t size = front.size();
if (outSize != nullptr) {
*outSize = size;
}
if (buffer.size < size) {
// Contract: the message STAYS QUEUED. The earlier branch's
// transport failed the call and popped the message anyway, which
// wedges the stream permanently the first time a reader guesses the
// size wrong.
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (size != 0) {
if (buffer.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::memcpy(buffer.data, front.data(), static_cast<std::size_t>(size));
}
dir.messages.pop_front();
return MOBILEGL_OK;
}
std::uint64_t InProcessTransport::PeekFrameSize() {
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::lock_guard<std::mutex> lock(dir.mutex);
return dir.messages.empty() ? 0 : dir.messages.front().size();
}
MobileGLResult InProcessTransport::ShareFd(int fd, MobileGLByteSpan sideband) {
#if defined(_WIN32)
(void)fd;
(void)sideband;
return MOBILEGL_ERR_UNSUPPORTED;
#else
if (fd < 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
if (sideband.size > FdPassing::kMaxSidebandBytes ||
(sideband.size != 0 && sideband.data == nullptr)) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Same ownership rule as SCM_RIGHTS: the peer gets its own descriptor
// for the same open file description and the caller keeps its own.
const int duplicate = ::dup(fd);
if (duplicate < 0) {
MGLOG_E("MG_Remote inproc: dup failed (errno=%d)", errno);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
FdOffer offer;
offer.fd = duplicate;
if (sideband.size != 0) {
const auto* first = static_cast<const std::uint8_t*>(sideband.data);
offer.sideband.assign(first, first + sideband.size);
}
InProcessChannel::Direction& dir = m_channel->Outbox(m_endpoint);
{
std::lock_guard<std::mutex> lock(dir.mutex);
if (dir.closed) {
::close(duplicate);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
dir.fdOffers.push_back(std::move(offer));
}
dir.fdCv.notify_one();
return MOBILEGL_OK;
#endif
}
MobileGLResult InProcessTransport::ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize,
std::uint32_t timeoutMs) {
if (outFd == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outFd = -1;
if (outSidebandSize != nullptr) {
*outSidebandSize = 0;
}
#if defined(_WIN32)
(void)sideband;
(void)timeoutMs;
return MOBILEGL_ERR_UNSUPPORTED;
#else
// Symmetric with FdPassing::ReceiveFd so callers behave identically in
// both delivery modes.
if (sideband.size < FdPassing::kMaxSidebandBytes) {
if (outSidebandSize != nullptr) {
*outSidebandSize = FdPassing::kMaxSidebandBytes;
}
return MOBILEGL_ERR_BUFFER_TOO_SMALL;
}
if (sideband.data == nullptr) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
InProcessChannel::Direction& dir = m_channel->Inbox(m_endpoint);
std::unique_lock<std::mutex> lock(dir.mutex);
if (dir.fdOffers.empty() && !dir.closed && timeoutMs != 0) {
const auto ready = [&dir] { return !dir.fdOffers.empty() || dir.closed; };
if (timeoutMs == kWaitForever) {
dir.fdCv.wait(lock, ready);
} else {
dir.fdCv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready);
}
}
if (dir.fdOffers.empty()) {
return dir.closed ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
}
FdOffer offer = std::move(dir.fdOffers.front());
dir.fdOffers.pop_front();
if (!offer.sideband.empty()) {
std::memcpy(sideband.data, offer.sideband.data(), offer.sideband.size());
}
if (outSidebandSize != nullptr) {
*outSidebandSize = offer.sideband.size();
}
*outFd = offer.fd;
return MOBILEGL_OK;
#endif
}
// Whole-connection teardown, as ITransport::Shutdown documents: both
// directions are half-closed and both ring doorbells are KILLED, because a
// peer parked on a ring doorbell mid-frame would otherwise never come back
// (a mere ring is consumed once and the waiter parks again).
void InProcessTransport::Shutdown() { m_channel->Close(); }
Doorbell& InProcessTransport::PeerDoorbell() { return m_channel->Bell(1 - m_endpoint); }
Doorbell& InProcessTransport::SelfDoorbell() { return m_channel->Bell(m_endpoint); }
} // namespace MobileGL::MG_Remote::Transport
@@ -0,0 +1,77 @@
// MobileGL - MobileGL/MG_Remote/Transport/InProcessTransport.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 `inproc` transport: two in-memory message queues and a pair of condvar
// doorbells, one connected endpoint at each end.
//
// It is not a test double. `inproc` is a delivery mode of its own - the server
// side is the monolith's own render thread, which is the single largest CPU
// lever this project has, and it is also the CI form of the split build. What
// it does NOT exercise is serialization of the byte stream, so the framing
// codec is covered separately by FramingTest.
//
// It is built by MOBILEGL_BUILD_DISAGGREGATED, the one option this skeleton
// adds, and selected at RUNTIME (plan appendix B: MOBILEGL_TRANSPORT =
// monolith / inproc / spawn / ...). The plan also reserves a separate
// MOBILEGL_BUILD_DISAGGREGATED_INPROC option for the role-isolation shim that
// a single-process CI build will need; that option does not exist yet, and
// nothing here depends on it.
//
// Messages are queued whole, so no framing bytes are involved; the size cap is
// still enforced so that a payload which would be illegal on a socket is
// illegal here too and does not pass CI only to fail after the switch to
// `spawn`.
//
// Descriptor passing is a plain dup(): both ends are the same process, so
// there is nothing to transfer, but the API stays identical so callers can be
// written once.
#pragma once
#include "Doorbell.h"
#include "ITransport.h"
#include <memory>
namespace MobileGL::MG_Remote::Transport {
class InProcessChannel;
class InProcessTransport final : public ITransport {
public:
~InProcessTransport() override;
// Creates one connected pair. Endpoint 0 is the client, endpoint 1 the
// server; both share one channel and either may be destroyed first.
static void CreatePair(std::unique_ptr<InProcessTransport>& outClient,
std::unique_ptr<InProcessTransport>& outServer);
MobileGLResult SendFrame(MobileGLByteSpan bytes) override;
MobileGLResult ReceiveFrame(MobileGLMutableByteSpan buffer, std::uint64_t* outSize,
std::uint32_t timeoutMs) override;
std::uint64_t PeekFrameSize() override;
MobileGLResult ShareFd(int fd, MobileGLByteSpan sideband) override;
MobileGLResult ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize, std::uint32_t timeoutMs) override;
void Shutdown() override;
TransportRole Role() const override { return TransportRole::InProcess; }
// The wake channel for the SEG_CMD/SEG_STAGE rings living beside this
// transport: ring the peer's bell after publishing a watermark (only
// when its park flag is set - see NotifyIfParked), park on your own.
Doorbell& PeerDoorbell();
Doorbell& SelfDoorbell();
private:
InProcessTransport(std::shared_ptr<InProcessChannel> channel, int endpoint);
std::shared_ptr<InProcessChannel> m_channel;
int m_endpoint = 0;
};
} // namespace MobileGL::MG_Remote::Transport
+306
View File
@@ -0,0 +1,306 @@
// MobileGL - MobileGL/MG_Remote/Transport/Ring.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 "Ring.h"
#include <MG_Util/Debug/Log.h>
#include <cstring>
namespace MobileGL::MG_Remote::Transport {
namespace {
constexpr std::uint64_t Align8(std::uint64_t value) {
return (value + (kRingRecordAlignment - 1)) & ~(kRingRecordAlignment - 1);
}
bool IsPowerOfTwo(std::uint64_t value) { return value != 0 && (value & (value - 1)) == 0; }
std::atomic<std::uint64_t>& Head(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdHead : c.stageHead;
}
const std::atomic<std::uint64_t>& Head(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdHead : c.stageHead;
}
std::atomic<std::uint64_t>& AppliedTail(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdAppliedTail : c.stageAppliedTail;
}
const std::atomic<std::uint64_t>& AppliedTail(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdAppliedTail : c.stageAppliedTail;
}
std::atomic<std::uint64_t>& RetiredTail(RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdRetiredTail : c.stageRetiredTail;
}
const std::atomic<std::uint64_t>& RetiredTail(const RingControl& c, RingCursorSet which) {
return which == RingCursorSet::Cmd ? c.cmdRetiredTail : c.stageRetiredTail;
}
} // namespace
void InitRingControl(RingControl& control) {
std::memset(static_cast<void*>(&control), 0, sizeof(RingControl));
// 0 means "uninitialized" for both generations, so a peer that reads a
// zero page can tell it from a legal generation.
control.serverEpoch.store(1, std::memory_order_relaxed);
control.ringGeneration.store(1, std::memory_order_relaxed);
}
bool RingCursorsValid(const RingControl& control, RingCursorSet cursors,
std::uint64_t capacityBytes) {
const std::uint64_t head = Head(control, cursors).load(std::memory_order_acquire);
const std::uint64_t applied = AppliedTail(control, cursors).load(std::memory_order_acquire);
const std::uint64_t retired = RetiredTail(control, cursors).load(std::memory_order_acquire);
if (applied > head || retired > applied) {
return false;
}
return head - retired <= capacityBytes;
}
MobileGLResult HardDrainRing(RingControl& control, RingCursorSet cursors) {
const std::uint64_t head = Head(control, cursors).load(std::memory_order_acquire);
const std::uint64_t applied = AppliedTail(control, cursors).load(std::memory_order_acquire);
const std::uint64_t retired = RetiredTail(control, cursors).load(std::memory_order_acquire);
if (head != applied || applied != retired) {
MGLOG_E("MG_Remote ring: hard drain refused, ring is not quiesced "
"(head=%llu applied=%llu retired=%llu)",
static_cast<unsigned long long>(head),
static_cast<unsigned long long>(applied),
static_cast<unsigned long long>(retired));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// Cursors stay monotonic across the drain - only the generation moves,
// so any offset either side cached is now recognisably stale.
control.ringGeneration.fetch_add(1, std::memory_order_acq_rel);
return MOBILEGL_OK;
}
// -----------------------------------------------------------------------
// Producer
// -----------------------------------------------------------------------
RingProducer::RingProducer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors)
: m_control(control), m_base(static_cast<std::uint8_t*>(base)), m_capacity(capacityBytes),
m_mask(capacityBytes - 1), m_cursors(cursors) {
if (control == nullptr || base == nullptr || !IsPowerOfTwo(capacityBytes) ||
capacityBytes < kMinRingCapacity || capacityBytes > kMaxRingCapacity) {
MGLOG_E("MG_Remote ring: producer rejected, capacity %llu must be a power of two "
"between %llu and %llu bytes over a non-null mapping (a record may be at most "
"half the ring, and the record header's size field is 32-bit, so a bigger ring "
"would truncate it)",
static_cast<unsigned long long>(capacityBytes),
static_cast<unsigned long long>(kMinRingCapacity),
static_cast<unsigned long long>(kMaxRingCapacity));
m_control = nullptr;
m_base = nullptr;
m_capacity = 0;
m_mask = 0;
return;
}
m_localHead = Head(*control, cursors).load(std::memory_order_acquire);
}
std::uint64_t RingProducer::TailForReclaim() const {
// The conservative watermark: a slot borrowed into the GPU timeline is
// only free after retiredTail passes it. A consumer that never borrows
// publishes retired together with applied, so this costs nothing there.
return RetiredTail(*m_control, m_cursors).load(std::memory_order_acquire);
}
std::uint64_t RingProducer::FreeBytes() const {
if (m_control == nullptr) {
return 0;
}
const std::uint64_t inFlight = m_localHead - TailForReclaim();
return inFlight >= m_capacity ? 0 : m_capacity - inFlight;
}
void* RingProducer::Reserve(std::uint16_t kind, std::uint16_t flags,
std::uint64_t payloadBytes) {
if (m_control == nullptr) {
return nullptr;
}
const std::uint64_t total = Align8(sizeof(RingRecordHeader) + payloadBytes);
if (total > MaxRecordBytes()) {
// A single record larger than HALF the ring is a caller bug: the
// record catalogue has to chunk oversized payloads (large subdata
// becomes several records) rather than emit one giant record.
//
// Half, not the whole ring, because a record has to be placeable at
// EVERY head offset of an empty ring. Straddling the wrap boundary
// costs a pad of spaceToEnd bytes on top of the record, and with
// spaceToEnd < total that is at most 2*total-8, which stays within
// the capacity exactly up to capacity/2. Above it the record is
// placeable at some offsets and not at others: at head offset 16 of
// an empty 256-byte ring a 248-byte record needs 240+248 bytes while
// FreeBytes() reports 256, so a producer that waits for FreeBytes()
// >= total stalls forever, and nothing is ever logged. Refusing here
// makes that impossible - a nullptr with FreeBytes() >= total can no
// longer mean "wait".
MGLOG_E("MG_Remote ring: record kind %u of %llu bytes exceeds half of a %llu byte ring; "
"the emitter must chunk it",
static_cast<unsigned>(kind), static_cast<unsigned long long>(total),
static_cast<unsigned long long>(m_capacity));
return nullptr;
}
const std::uint64_t offset = m_localHead & m_mask;
const std::uint64_t spaceToEnd = m_capacity - offset;
// Every record is a multiple of 8, so the distance to the wrap boundary
// is too, and a pad header always fits.
const bool needsPad = spaceToEnd < total;
const std::uint64_t needed = needsPad ? spaceToEnd + total : total;
if (FreeBytes() < needed) {
MGLOG_D("MG_Remote ring: full, %llu bytes free, %llu needed",
static_cast<unsigned long long>(FreeBytes()),
static_cast<unsigned long long>(needed));
return nullptr;
}
if (needsPad) {
RingRecordHeader pad{};
pad.kind = kRingPadRecordKind;
pad.flags = kRecPad;
pad.size = static_cast<std::uint32_t>(spaceToEnd);
std::memcpy(SlotAt(m_localHead), &pad, sizeof(pad));
m_localHead += spaceToEnd;
}
RingRecordHeader header{};
header.kind = kind;
header.flags = static_cast<std::uint16_t>(flags & ~static_cast<std::uint16_t>(kRecPad));
header.size = static_cast<std::uint32_t>(total);
std::uint8_t* slot = SlotAt(m_localHead);
std::memcpy(slot, &header, sizeof(header));
m_localHead += total;
return slot + sizeof(RingRecordHeader);
}
void RingProducer::Publish() {
if (m_control == nullptr) {
return;
}
// Release: everything written into the slots happens-before the peer's
// acquire load of the head.
Head(*m_control, m_cursors).store(m_localHead, std::memory_order_release);
}
// -----------------------------------------------------------------------
// Consumer
// -----------------------------------------------------------------------
RingConsumer::RingConsumer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors)
: m_control(control), m_base(static_cast<const std::uint8_t*>(base)),
m_capacity(capacityBytes), m_mask(capacityBytes - 1), m_cursors(cursors) {
if (control == nullptr || base == nullptr || !IsPowerOfTwo(capacityBytes) ||
capacityBytes < kMinRingCapacity || capacityBytes > kMaxRingCapacity) {
MGLOG_E("MG_Remote ring: consumer rejected, capacity %llu must be a power of two "
"between %llu and %llu bytes over a non-null mapping (a record may be at most "
"half the ring, and the record header's size field is 32-bit, so a bigger ring "
"would truncate it)",
static_cast<unsigned long long>(capacityBytes),
static_cast<unsigned long long>(kMinRingCapacity),
static_cast<unsigned long long>(kMaxRingCapacity));
m_control = nullptr;
m_base = nullptr;
m_capacity = 0;
m_mask = 0;
return;
}
m_localTail = AppliedTail(*control, cursors).load(std::memory_order_acquire);
}
bool RingConsumer::Pop(RingRecordView& out, bool* outCorrupt) {
if (outCorrupt != nullptr) {
*outCorrupt = false;
}
if (m_control == nullptr) {
return false;
}
const std::uint64_t head = Head(*m_control, m_cursors).load(std::memory_order_acquire);
while (m_localTail != head) {
const std::uint64_t available = head - m_localTail;
if (available < sizeof(RingRecordHeader) || available > m_capacity) {
MGLOG_E("MG_Remote ring: %llu bytes between tail and head is impossible for a %llu "
"byte ring",
static_cast<unsigned long long>(available),
static_cast<unsigned long long>(m_capacity));
if (outCorrupt != nullptr) {
*outCorrupt = true;
}
return false;
}
const std::uint64_t offset = m_localTail & m_mask;
RingRecordHeader header{};
std::memcpy(&header, m_base + offset, sizeof(header));
// SEG_CMD is written by the peer process: compile-time asserts on
// record sizes cannot see runtime corruption, so every dispatch is
// preceded by these bounds checks and a violation is fatal, never a
// retry (plan section 6.3, runtime bounds discipline).
const std::uint64_t size = header.size;
if (size < sizeof(RingRecordHeader) || (size % kRingRecordAlignment) != 0 ||
size > available || offset + size > m_capacity) {
MGLOG_E("MG_Remote ring: corrupt record header at cursor %llu "
"(kind=%u flags=0x%04X size=%u available=%llu)",
static_cast<unsigned long long>(m_localTail),
static_cast<unsigned>(header.kind), static_cast<unsigned>(header.flags),
header.size, static_cast<unsigned long long>(available));
if (outCorrupt != nullptr) {
*outCorrupt = true;
}
return false;
}
if ((header.flags & kRecPad) != 0) {
m_localTail += size;
continue;
}
out.kind = header.kind;
out.flags = header.flags;
out.payload = m_base + offset + sizeof(RingRecordHeader);
// Includes the alignment tail; the record catalogue knows the real
// payload length.
out.payloadSize = size - sizeof(RingRecordHeader);
out.cursor = m_localTail;
m_localTail += size;
return true;
}
return false;
}
void RingConsumer::PublishApplied() {
if (m_control == nullptr) {
return;
}
AppliedTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
}
void RingConsumer::PublishRetired() {
if (m_control == nullptr) {
return;
}
// retiredTail must never overtake appliedTail, so publish both.
AppliedTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
RetiredTail(*m_control, m_cursors).store(m_localTail, std::memory_order_release);
}
void RingConsumer::PublishRetiredUpTo(std::uint64_t cursor) {
if (m_control == nullptr) {
return;
}
const std::uint64_t applied = AppliedTail(*m_control, m_cursors).load(std::memory_order_acquire);
const std::uint64_t clamped = cursor > applied ? applied : cursor;
const std::uint64_t current = RetiredTail(*m_control, m_cursors).load(std::memory_order_relaxed);
if (clamped > current) {
RetiredTail(*m_control, m_cursors).store(clamped, std::memory_order_release);
}
}
} // namespace MobileGL::MG_Remote::Transport
+256
View File
@@ -0,0 +1,256 @@
// MobileGL - MobileGL/MG_Remote/Transport/Ring.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
// SEG_CMD / SEG_STAGE ring control and the SPSC producer/consumer over it.
//
// RingControl is the shared page at the head of SEG_CMD, laid out exactly as
// the inherited transport design (plan section 8.1, referring the earlier
// plan's section 6.2) specifies:
//
// - TWO independent cursor triples, one for SEG_CMD and one for SEG_STAGE.
// The stage ring needs its own because "SEG_STAGE has less than a quarter
// left" is a publish trigger and that occupancy cannot be derived from the
// command ring's cursors, and because a stage slot retires on a different
// event than a command record does.
// - THREE separate sequence watermarks. Conflating them is the classic bug:
// appliedSeq releases *AppliedTail, submittedSeq releases staging,
// retiredSeq / completedFrameSerial release *RetiredTail and adopted
// stores.
// - TWO tails per ring, not one. Once the server borrows a ring slot into
// the GPU timeline instead of copying it out again, that slot can only be
// recycled after completedFrameSerial; a single tail would silently
// degrade to conservative reclaim the day borrowing lands.
// - Both park flags, because the doorbell is bidirectional: without the
// server->client direction every client wait degenerates into a
// cross-process spin on one shared cache line (a whole 16.6ms frame of a
// big core, on a phone, competing with the GPU and the game's JVM).
//
// Cursors are monotonically increasing byte counts; the ring is indexed with a
// power-of-two mask. They are never reset, so a torn read can never look like
// a valid earlier position. ringGeneration is bumped after a hard drain to
// invalidate every cached offset.
//
// Record framing inside the ring is the 8-byte header below, which is the
// layout the plan's RecHeader already fixes ({u16 kind, u16 flags, u32 size},
// size including the header and a multiple of 8). The record CATALOGUE
// (Records.def / PipeCalls.def) is a separate deliverable; the ring itself
// only needs kind/flags/size, so it can carry the real records the day they
// land without changing shape.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// The shared control page. One 4 KiB page so it can be mapped alone, with
// each contended group on its own cache line.
struct alignas(4096) RingControl {
// ---- SEG_CMD cursors ------------------------------------------------
alignas(64) std::atomic<std::uint64_t> cmdHead; // producer: bytes written
alignas(64) std::atomic<std::uint64_t> cmdAppliedTail; // consumer: bytes decoded/copied out
std::atomic<std::uint64_t> cmdRetiredTail; // consumer: borrowed slots released
// ---- SEG_STAGE cursors ----------------------------------------------
alignas(64) std::atomic<std::uint64_t> stageHead;
alignas(64) std::atomic<std::uint64_t> stageAppliedTail;
std::atomic<std::uint64_t> stageRetiredTail;
// ---- sequence / frame watermarks -------------------------------------
alignas(64) std::atomic<std::uint64_t> appliedSeq; // records applied
std::atomic<std::uint64_t> submittedSeq; // handed to the driver
std::atomic<std::uint64_t> retiredSeq; // GPU finished
std::atomic<std::uint64_t> completedFrameSerial;
std::atomic<std::uint64_t> presentAckSerial;
// ---- doorbell / generation -------------------------------------------
alignas(64) std::atomic<std::uint32_t> serverEpoch; // ++ on context loss / server restart
std::atomic<std::uint32_t> ringGeneration; // ++ after a hard drain
std::atomic<std::uint32_t> consumerParked; // server asleep, producer must ring
std::atomic<std::uint32_t> producerParked; // client asleep, server must ring
std::atomic<std::uint32_t> eventRingFull; // SEG_EVENT full, server stopped applying
std::atomic<std::uint32_t> eventDropped; // dropped lossy events
};
static_assert(sizeof(RingControl) == 4096, "RingControl must be exactly one page");
static_assert(alignof(RingControl) == 4096, "RingControl must be page aligned");
static_assert(std::atomic<std::uint64_t>::is_always_lock_free,
"the ring cursors are shared across processes: they must be lock-free");
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
"the doorbell flags are shared across processes: they must be lock-free");
// Per-record header. Prefix-identical to the plan's RecHeader so the
// generated record catalogue drops straight in.
struct RingRecordHeader {
std::uint16_t kind;
std::uint16_t flags;
std::uint32_t size; // header + payload + alignment padding, multiple of 8
};
static_assert(sizeof(RingRecordHeader) == 8, "RecHeader is 8 bytes on the wire");
enum RingRecordFlags : std::uint16_t {
kRecNone = 0,
kRecNeedsAck = 1u << 0,
kRecHasBlob = 1u << 1,
kRecPad = 1u << 2, // filler to the wrap boundary, no payload meaning
kRecBorrowSlot = 1u << 3, // slot is borrowed into the GPU timeline; retires late
kRecVarTail = 1u << 4,
};
// Reserved kind for the wrap filler. The catalogue starts at 1.
inline constexpr std::uint16_t kRingPadRecordKind = 0;
inline constexpr std::uint64_t kRingRecordAlignment = 8;
// Largest ring the 8-byte header can describe. Both a record's size and a
// wrap filler's size are bounded only by the capacity and are stored in
// RingRecordHeader::size, which is 32 bits by wire contract: a ring of
// 4 GiB or more would silently truncate them, and the consumer would then
// bounds-check the truncated value against the real one. SEG_CMD is 8 MiB
// and SEG_STAGE 32 MiB today, so this is unreachable - it is the same
// class of construction-time guard as the power-of-two check beside it.
inline constexpr std::uint64_t kMaxRingCapacity = 0xFFFFFFFFull;
// Smallest ring: two record headers. A record may be at most HALF the ring
// (see RingProducer::Reserve), so a ring of one header could carry nothing
// at all - not even the smallest record, a bare header.
inline constexpr std::uint64_t kMinRingCapacity = 2 * sizeof(RingRecordHeader);
// Which cursor triple a producer/consumer pair drives.
enum class RingCursorSet : std::uint32_t {
Cmd = 0,
Stage = 1,
};
// Zeroes every cursor and starts serverEpoch / ringGeneration at 1, so that
// a zero read is always "uninitialized", never a legal generation.
void InitRingControl(RingControl& control);
// head >= appliedTail >= retiredTail, and the ring never holds more than
// its capacity. False means the shared page is corrupt (or a peer is
// misbehaving), which is a Fatal{ProtocolCorruption}, never a retry.
bool RingCursorsValid(const RingControl& control, RingCursorSet cursors,
std::uint64_t capacityBytes);
// Bumps ringGeneration, invalidating every offset either side has cached.
// Both sides must be quiesced and the ring fully drained
// (head == appliedTail == retiredTail); otherwise this returns
// MOBILEGL_ERR_INVALID_ARGUMENT and changes nothing.
MobileGLResult HardDrainRing(RingControl& control, RingCursorSet cursors);
// A record as seen by the consumer.
struct RingRecordView {
std::uint16_t kind = 0;
std::uint16_t flags = 0;
const void* payload = nullptr;
std::uint64_t payloadSize = 0;
std::uint64_t cursor = 0; // producer cursor at the START of this record
};
// Single producer. Not thread-safe: one writer thread, by construction.
class RingProducer {
public:
RingProducer() = default;
// `base` is the ring's byte area (NOT the control page) and
// `capacityBytes` must be a power of two between kMinRingCapacity and
// kMaxRingCapacity. Anything else leaves Valid() false.
RingProducer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors);
bool Valid() const { return m_control != nullptr; }
// Bytes still writable before the consumer has to catch up.
std::uint64_t FreeBytes() const;
// Reserves room for one record and returns a pointer to its payload,
// or nullptr when the ring is full. The payload is uninitialized;
// alignment padding at its tail is NOT zeroed. Emits a pad record
// automatically when the record would straddle the wrap boundary, so
// every record is contiguous.
//
// A record whose total (header + payload, rounded up to 8) exceeds
// MaxRecordBytes() == Capacity()/2 is refused outright, with an error
// log and however empty the ring is: chunking it is the emitter's job
// (plan section 8.2, the G3 chunking rule). Half is exact, not
// conservative - it is the largest record EVERY head offset can place,
// because a wrap pad costs at most total-8 bytes on top of the record
// and 2*total-8 <= capacity-8 holds exactly up to capacity/2. Above it
// a record is placeable at some offsets and not at others, and a
// producer waiting for FreeBytes() >= total stalls forever on an empty
// ring. So: nullptr with FreeBytes() >= total never means "wait"; it
// can only mean "too big, chunk".
void* Reserve(std::uint16_t kind, std::uint16_t flags, std::uint64_t payloadBytes);
// The largest header+payload total Reserve accepts: Capacity()/2. This
// is the number the emitter chunks against.
std::uint64_t MaxRecordBytes() const { return m_capacity / 2; }
// Makes every reserved record visible to the consumer (release store on
// the head cursor). Cheap: publishing per record is fine, batching 8-16
// only amortizes the doorbell store.
void Publish();
// Producer-local cursor including records not yet published.
std::uint64_t LocalHead() const { return m_localHead; }
std::uint64_t Capacity() const { return m_capacity; }
private:
std::uint64_t TailForReclaim() const;
std::uint8_t* SlotAt(std::uint64_t cursor) const {
return m_base + static_cast<std::size_t>(cursor & m_mask);
}
RingControl* m_control = nullptr;
std::uint8_t* m_base = nullptr;
std::uint64_t m_capacity = 0;
std::uint64_t m_mask = 0;
std::uint64_t m_localHead = 0;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
// Single consumer. Not thread-safe: one reader thread, by construction.
class RingConsumer {
public:
RingConsumer() = default;
RingConsumer(RingControl* control, void* base, std::uint64_t capacityBytes,
RingCursorSet cursors);
bool Valid() const { return m_control != nullptr; }
// Pops the next record, skipping wrap fillers. Returns false when the
// ring is empty at this moment. A record whose header is impossible
// (size not 8-aligned, smaller than a header, or larger than what the
// producer has published) is refused: *outCorrupt is set, which the
// caller must escalate to Fatal{ProtocolCorruption} rather than retry.
bool Pop(RingRecordView& out, bool* outCorrupt = nullptr);
// Publishes the applied cursor, releasing those bytes to the producer.
void PublishApplied();
// Publishes the retired cursor. Records without kRecBorrowSlot retire
// as soon as they are applied; borrowed slots retire on
// completedFrameSerial, which is why this is a separate call.
void PublishRetired();
void PublishRetiredUpTo(std::uint64_t cursor);
std::uint64_t LocalTail() const { return m_localTail; }
std::uint64_t Capacity() const { return m_capacity; }
private:
RingControl* m_control = nullptr;
const std::uint8_t* m_base = nullptr;
std::uint64_t m_capacity = 0;
std::uint64_t m_mask = 0;
std::uint64_t m_localTail = 0;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegment.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
// Platform-independent half of ShmSegment. The create/map/close bodies live in
// ShmSegmentPosix.cpp and ShmSegmentWin32.cpp.
#include "ShmSegment.h"
#include <cstring>
#include <utility>
namespace MobileGL::MG_Remote::Transport {
ShmSegment::~ShmSegment() { Close(); }
ShmSegment::ShmSegment(ShmSegment&& other) noexcept { Steal(std::move(other)); }
ShmSegment& ShmSegment::operator=(ShmSegment&& other) noexcept {
if (this != &other) {
Close();
Steal(std::move(other));
}
return *this;
}
void ShmSegment::Steal(ShmSegment&& other) noexcept {
std::memcpy(m_name, other.m_name, sizeof(m_name));
m_mapping = other.m_mapping;
m_nativeHandle = other.m_nativeHandle;
m_size = other.m_size;
m_fd = other.m_fd;
m_readOnly = other.m_readOnly;
std::memset(other.m_name, 0, sizeof(other.m_name));
other.m_mapping = nullptr;
other.m_nativeHandle = nullptr;
other.m_size = 0;
other.m_fd = -1;
other.m_readOnly = false;
}
bool ShmSegment::Valid() const { return m_size != 0 && (m_fd >= 0 || m_nativeHandle != nullptr); }
} // namespace MobileGL::MG_Remote::Transport
+87
View File
@@ -0,0 +1,87 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegment.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
// One shared-memory segment: SEG_CMD, SEG_STAGE, SEG_REPLY, SEG_EVENT, a
// per-object SEG_SHADOW or a SEG_ADOPT store (inherited segment layout, plan
// section 8.1).
//
// Creation matrix (earlier plan section 6.1):
// - Android: ASharedMemory_create (API 26; libc's memfd_create wrapper
// only appears at API 30, which is above our floor)
// - desktop Linux: syscall(SYS_memfd_create, ...) directly, for the same
// reason - the glibc wrapper is recent and this file has to
// build against old sysroots
// - other POSIX: shm_open + immediate shm_unlink, the fd keeps it alive
// - Windows: CreateFileMappingW in the Local\ namespace
//
// Transfer is NOT done here. On POSIX the fd travels by SCM_RIGHTS
// (FdPassing.h / ITransport::ShareFd) and the name is only a debugging label;
// on Windows the section name travels inside the SegmentRef table.
//
// The Windows implementation is compile-guarded and untested at the time it
// was written: no Windows machine is a correctness gate for this project.
#pragma once
#include "../Protocol/mg_protocol_base.h"
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
inline constexpr std::size_t kShmNameMax = 128;
class ShmSegment {
public:
ShmSegment() = default;
~ShmSegment();
ShmSegment(const ShmSegment&) = delete;
ShmSegment& operator=(const ShmSegment&) = delete;
ShmSegment(ShmSegment&& other) noexcept;
ShmSegment& operator=(ShmSegment&& other) noexcept;
// Creates a segment of `size` bytes owned by this process. `nameHint`
// is a short debug label (Windows: part of the section name peers
// resolve). The segment is NOT mapped yet.
static MobileGLResult Create(const char* nameHint, std::uint64_t size, ShmSegment& out);
// POSIX only: adopts a descriptor received over SCM_RIGHTS. Takes
// ownership of `fd` on success; on failure the caller still owns it.
static MobileGLResult Adopt(int fd, std::uint64_t size, ShmSegment& out);
// Windows only: opens a section the peer published by name.
static MobileGLResult OpenNamed(const char* name, std::uint64_t size, ShmSegment& out);
// Maps the whole segment. Read-only mappings are what the peer gets for
// a segment it does not own (SEG_CMD/SEG_STAGE on the server side).
MobileGLResult Map(bool readOnly);
void Unmap();
void Close(); // unmaps and releases the descriptor/handle
bool Valid() const;
void* Data() const { return m_mapping; }
std::uint64_t Size() const { return m_size; }
bool MappedReadOnly() const { return m_readOnly; }
const char* Name() const { return m_name; }
// POSIX: the descriptor to hand to ShareFd. -1 on Windows.
int Fd() const { return m_fd; }
private:
void Steal(ShmSegment&& other) noexcept;
char m_name[kShmNameMax] = {};
void* m_mapping = nullptr;
void* m_nativeHandle = nullptr; // Windows HANDLE; unused on POSIX
std::uint64_t m_size = 0;
int m_fd = -1;
bool m_readOnly = false;
};
} // namespace MobileGL::MG_Remote::Transport
@@ -0,0 +1,191 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegmentPosix.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 "ShmSegment.h"
#if !defined(_WIN32)
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#if defined(__ANDROID__)
#include <android/sharedmem.h>
#elif defined(__linux__)
#include <sys/syscall.h>
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
void CopyName(char (&dst)[kShmNameMax], const char* src) {
if (src == nullptr) {
dst[0] = '\0';
return;
}
std::snprintf(dst, kShmNameMax, "%s", src);
}
#if !defined(__ANDROID__)
// Unique per process; only used by the shm_open fallback, whose name
// must not collide with a concurrent creator's.
std::atomic<std::uint32_t> g_shmCounter{0};
#endif
} // namespace
MobileGLResult ShmSegment::Create(const char* nameHint, std::uint64_t size, ShmSegment& out) {
if (size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
char label[kShmNameMax];
std::snprintf(label, sizeof(label), "mgl-%s", nameHint != nullptr ? nameHint : "seg");
int fd = -1;
#if defined(__ANDROID__)
// API 26. libc's memfd_create wrapper is API 30, above MobileGL's floor.
fd = ASharedMemory_create(label, static_cast<size_t>(size));
if (fd < 0) {
MGLOG_W("MG_Remote shm: ASharedMemory_create(%s, %llu) failed (errno=%d)", label,
static_cast<unsigned long long>(size), errno);
}
#elif defined(__linux__)
// Raw syscall, not the glibc wrapper: the wrapper is too recent to rely
// on across the sysroots this builds against.
fd = static_cast<int>(::syscall(SYS_memfd_create, label, MFD_CLOEXEC));
if (fd >= 0 && ::ftruncate(fd, static_cast<off_t>(size)) != 0) {
MGLOG_E("MG_Remote shm: ftruncate(%llu) failed (errno=%d)",
static_cast<unsigned long long>(size), errno);
::close(fd);
fd = -1;
}
#endif
#if !defined(__ANDROID__)
if (fd < 0) {
// Fallback: shm_open + immediate unlink. The name disappears at
// once; the descriptor is what keeps the object alive and what
// travels by SCM_RIGHTS.
char shmName[kShmNameMax];
std::snprintf(shmName, sizeof(shmName), "/mgl-%d-%u-%s", static_cast<int>(::getpid()),
g_shmCounter.fetch_add(1, std::memory_order_relaxed),
nameHint != nullptr ? nameHint : "seg");
fd = ::shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0) {
MGLOG_E("MG_Remote shm: shm_open(%s) failed (errno=%d)", shmName, errno);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
::shm_unlink(shmName);
if (::ftruncate(fd, static_cast<off_t>(size)) != 0) {
MGLOG_E("MG_Remote shm: ftruncate(%llu) failed (errno=%d)",
static_cast<unsigned long long>(size), errno);
::close(fd);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
CopyName(out.m_name, shmName);
} else {
CopyName(out.m_name, label);
}
#else
if (fd < 0) {
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
CopyName(out.m_name, label);
#endif
out.m_fd = fd;
out.m_size = size;
out.m_nativeHandle = nullptr;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Adopt(int fd, std::uint64_t size, ShmSegment& out) {
if (fd < 0 || size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// The peer's declared size is not trusted: a segment smaller than what
// the announcement claims would turn every later offset into an
// out-of-bounds map.
struct stat st{};
if (::fstat(fd, &st) == 0 && st.st_size > 0 &&
static_cast<std::uint64_t>(st.st_size) < size) {
MGLOG_E("MG_Remote shm: peer announced %llu bytes but the descriptor is %lld",
static_cast<unsigned long long>(size), static_cast<long long>(st.st_size));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
out.m_fd = fd; // ownership transferred
out.m_size = size;
out.m_nativeHandle = nullptr;
out.m_mapping = nullptr;
out.m_readOnly = false;
CopyName(out.m_name, "adopted");
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::OpenNamed(const char*, std::uint64_t, ShmSegment&) {
// POSIX shares descriptors, not names.
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmSegment::Map(bool readOnly) {
if (m_fd < 0 || m_size == 0) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
if (m_mapping != nullptr) {
if (m_readOnly == readOnly) {
return MOBILEGL_OK;
}
Unmap();
}
const int prot = readOnly ? PROT_READ : (PROT_READ | PROT_WRITE);
void* addr = ::mmap(nullptr, static_cast<size_t>(m_size), prot, MAP_SHARED, m_fd, 0);
if (addr == MAP_FAILED) {
MGLOG_E("MG_Remote shm: mmap of %llu bytes failed (errno=%d)",
static_cast<unsigned long long>(m_size), errno);
return MOBILEGL_ERR_OUT_OF_MEMORY;
}
m_mapping = addr;
m_readOnly = readOnly;
return MOBILEGL_OK;
}
void ShmSegment::Unmap() {
if (m_mapping != nullptr) {
::munmap(m_mapping, static_cast<size_t>(m_size));
m_mapping = nullptr;
}
}
void ShmSegment::Close() {
Unmap();
if (m_fd >= 0) {
::close(m_fd);
m_fd = -1;
}
m_size = 0;
m_readOnly = false;
m_name[0] = '\0';
}
} // namespace MobileGL::MG_Remote::Transport
#endif // !_WIN32
@@ -0,0 +1,162 @@
// MobileGL - MobileGL/MG_Remote/Transport/ShmSegmentWin32.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
// Windows half of ShmSegment: a named file-mapping section in the Local\
// namespace, which the peer opens by the name carried in SegmentRef.
//
// UNTESTED. This project's Windows machine is not a correctness gate (its
// Vulkan lacks vkCreateHeadlessSurfaceEXT and accounts for most of its
// baseline integration failures), and the whole disaggregated build is gated
// behind MOBILEGL_BUILD_DISAGGREGATED, which is OFF by default. It is written
// now so the abstraction is shaped by two real platforms rather than one.
#include "ShmSegment.h"
#if defined(_WIN32)
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
namespace MobileGL::MG_Remote::Transport {
namespace {
std::atomic<std::uint32_t> g_sectionCounter{0};
bool ToWide(const char* utf8, wchar_t* out, int outChars) {
if (utf8 == nullptr || out == nullptr || outChars <= 0) {
return false;
}
const int written = ::MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out, outChars);
return written > 0;
}
} // namespace
MobileGLResult ShmSegment::Create(const char* nameHint, std::uint64_t size, ShmSegment& out) {
if (size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
char name[kShmNameMax];
std::snprintf(name, sizeof(name), "Local\\mgl-%lu-%u-%s",
static_cast<unsigned long>(::GetCurrentProcessId()),
g_sectionCounter.fetch_add(1, std::memory_order_relaxed),
nameHint != nullptr ? nameHint : "seg");
wchar_t wide[kShmNameMax];
if (!ToWide(name, wide, static_cast<int>(kShmNameMax))) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
HANDLE section = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
static_cast<DWORD>(size >> 32),
static_cast<DWORD>(size & 0xFFFFFFFFull), wide);
if (section == nullptr) {
MGLOG_E("MG_Remote shm: CreateFileMappingW(%s, %llu) failed (GetLastError=%lu)", name,
static_cast<unsigned long long>(size),
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
if (::GetLastError() == ERROR_ALREADY_EXISTS) {
::CloseHandle(section);
MGLOG_E("MG_Remote shm: section name %s already exists", name);
return MOBILEGL_ERR_SHM_EXHAUSTED;
}
std::snprintf(out.m_name, kShmNameMax, "%s", name);
out.m_nativeHandle = section;
out.m_size = size;
out.m_fd = -1;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Adopt(int, std::uint64_t, ShmSegment&) {
// No SCM_RIGHTS here: Windows peers resolve the section by name.
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmSegment::OpenNamed(const char* name, std::uint64_t size, ShmSegment& out) {
if (name == nullptr || name[0] == '\0' || size == 0) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
out.Close();
wchar_t wide[kShmNameMax];
if (!ToWide(name, wide, static_cast<int>(kShmNameMax))) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
HANDLE section = ::OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wide);
if (section == nullptr) {
MGLOG_E("MG_Remote shm: OpenFileMappingW(%s) failed (GetLastError=%lu)", name,
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
std::snprintf(out.m_name, kShmNameMax, "%s", name);
out.m_nativeHandle = section;
out.m_size = size;
out.m_fd = -1;
out.m_mapping = nullptr;
out.m_readOnly = false;
return MOBILEGL_OK;
}
MobileGLResult ShmSegment::Map(bool readOnly) {
if (m_nativeHandle == nullptr || m_size == 0) {
return MOBILEGL_ERR_NOT_INITIALIZED;
}
if (m_mapping != nullptr) {
if (m_readOnly == readOnly) {
return MOBILEGL_OK;
}
Unmap();
}
void* view = ::MapViewOfFile(static_cast<HANDLE>(m_nativeHandle),
readOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, 0, 0,
static_cast<SIZE_T>(m_size));
if (view == nullptr) {
MGLOG_E("MG_Remote shm: MapViewOfFile of %llu bytes failed (GetLastError=%lu)",
static_cast<unsigned long long>(m_size),
static_cast<unsigned long>(::GetLastError()));
return MOBILEGL_ERR_OUT_OF_MEMORY;
}
m_mapping = view;
m_readOnly = readOnly;
return MOBILEGL_OK;
}
void ShmSegment::Unmap() {
if (m_mapping != nullptr) {
::UnmapViewOfFile(m_mapping);
m_mapping = nullptr;
}
}
void ShmSegment::Close() {
Unmap();
if (m_nativeHandle != nullptr) {
::CloseHandle(static_cast<HANDLE>(m_nativeHandle));
m_nativeHandle = nullptr;
}
m_size = 0;
m_readOnly = false;
m_name[0] = '\0';
}
} // namespace MobileGL::MG_Remote::Transport
#endif // _WIN32
+33
View File
@@ -0,0 +1,33 @@
// MobileGL - MobileGL/MG_Remote/Transport/WireLog.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 "WireLog.h"
#include <MG_Util/Debug/Log.h>
#include <cstdarg>
#include <cstdio>
namespace MobileGL::MG_Remote::Transport {
void WireLogError(const char* format, ...) {
// One stack line, no allocation: this runs on paths that have just
// decided the connection is unusable.
char line[512];
va_list args;
va_start(args, format);
const int written = std::vsnprintf(line, sizeof(line), format, args);
va_end(args);
if (written < 0) {
MGLOG_E("MG_Remote wire: unformattable diagnostic (format=%s)", format);
return;
}
MGLOG_E("%s", line);
}
} // namespace MobileGL::MG_Remote::Transport
+38
View File
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_Remote/Transport/WireLog.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
// A one-function logging shim for the wire layer's header-only code.
//
// MG_Util/Debug/Log.h includes <Includes.h>, the GL frontend's umbrella
// header - 661 headers, measured with `clang++ -H`. That is fine inside a
// .cpp, and Ring.cpp / Doorbell.cpp / the transports all do it. It is not fine
// in a header of this layer: ITransport.h states the rule ("nothing about a
// byte pipe needs the GL frontend's umbrella header") because these headers
// are included by both roles and by the eventual server-side binary, and
// because the disaggregated build's include-graph purity gate (plan section
// 10.3, gate A) asserts on `-H` output rather than on symbols. Framing.h was
// the one header under Transport/ that broke the rule; it now calls this
// instead, and the umbrella stays inside WireLog.cpp.
//
// ERROR only, deliberately. Everything routed here is a latched protocol
// violation, never per-frame noise; non-critical wire lines use MGLOG_D from a
// .cpp, where the INFO build compiles them out entirely.
#pragma once
namespace MobileGL::MG_Remote::Transport {
// Formats one line and emits it at ERROR level (MGLOG_E). printf-style,
// with the format checked against the arguments at compile time.
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 1, 2)))
#endif
void
WireLogError(const char* format, ...);
} // namespace MobileGL::MG_Remote::Transport
+32
View File
@@ -14,6 +14,8 @@
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <Config.h>
#include <atomic>
namespace MobileGL::MG_State {
void Init() {
MGLOG_D("Initializing MobileGL State...");
@@ -1259,6 +1261,33 @@ namespace MobileGL::MG_State {
return m_renderbufferState.ValidateRenderbufferObject(index);
}
Uint64 GLContext::AllocateTransformFeedbackLifetimeId() {
// Starts at 1 so a zero-initialised backend slot can never carry a live object's id.
static std::atomic<Uint64> nextId{1};
return nextId.fetch_add(1, std::memory_order_relaxed);
}
GLContext::GLContext() {
// The default transform feedback object (name 0) exists from the start of the context
// (GL 4.6 core 13.2.1), but nothing binds it, so nothing else would materialise it.
// Materialising it here is what lets GetBoundTransformFeedbackLifetimeId() be a plain
// const read instead of an operator[] insert on the draw path.
m_boundTransformFeedbackLifetimeId = m_transformFeedbackObjects[0].lifetimeId;
}
Bool GLContext::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
if (lifetimeId == 0) return false;
for (const auto& [name, object] : m_transformFeedbackObjects) {
if (object.lifetimeId != lifetimeId) continue;
// The bound object's span state is live in the context; its saved copy is only
// written when a bind swaps it out.
return name == m_boundTransformFeedback ? m_transformFeedbackActive : object.active;
}
// No object carries this identity any more: it was deleted, and a deleted object can
// never resume.
return false;
}
void GLContext::SaveBoundTransformFeedbackState() {
auto& object = m_transformFeedbackObjects[m_boundTransformFeedback];
for (Uint i = 0; i < MAX_TRANSFORM_FEEDBACK_BUFFERS; ++i) {
@@ -1295,6 +1324,9 @@ namespace MobileGL::MG_State {
m_transformFeedbackGeneration = object.generation;
m_transformFeedbackCapturedVertices = object.capturedVertices;
m_transformFeedbackInputPrimitives = object.inputPrimitives;
// Every route that changes which object is bound - BindTransformFeedbackObject and the
// revert a delete of the bound object performs - comes through here.
m_boundTransformFeedbackLifetimeId = object.lifetimeId;
}
void GLContext::GenTransformFeedbackNames(Uint number, Vector<Uint>& ids) {
+33 -1
View File
@@ -60,7 +60,7 @@ namespace MobileGL {
class GLContext {
public:
GLContext() = default;
GLContext();
// Error
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
@@ -427,6 +427,27 @@ namespace MobileGL {
void BindTransformFeedbackObject(Uint index);
void MarkTransformFeedbackObjectForDeletion(Uint index);
Uint GetBoundTransformFeedbackName() const { return m_boundTransformFeedback; }
// The bound object's never-reused identity, for a backend that keys a per-object
// resource on it. The NAME is not an identity: glGenTransformFeedbacks recycles a
// deleted one (LIFO), so a memo keyed on the name hands a brand-new object the dead
// one's slot. Cached rather than looked up on demand: the backend asks twice per
// captured draw, and an operator[] on m_transformFeedbackObjects would be an
// INSERT on the draw path - ska::flat_hash_map invalidates every reference into
// itself when it rehashes. The cache is refreshed by
// RestoreBoundTransformFeedbackState, which every bind (and the revert a delete
// performs) goes through, and seeded for the default object by the constructor.
// Never returns 0 - the counter starts at 1 so a zero-initialised memo slot cannot
// be mistaken for a live object.
Uint64 GetBoundTransformFeedbackLifetimeId() const { return m_boundTransformFeedbackLifetimeId; }
// Whether the object carrying this identity still has an OPEN capture span - one
// that glBeginTransformFeedback started and glEndTransformFeedback has not closed,
// paused or not. A backend that hands out a bounded set of per-object slots must
// never take one of these over: a paused span's counters are precisely what its
// resume reads, and GL only lets other objects capture WHILE it is paused, so the
// paused object is also the one that looks idle. An identity no live object
// carries any more (its object was deleted) answers false, which is what makes
// such a slot reclaimable.
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
// Vertices the object captured in its last completed span; the vertex count
// glDrawTransformFeedback replays.
Uint64 GetTransformFeedbackRecordedVertices(Uint index) const;
@@ -514,6 +535,9 @@ namespace MobileGL {
GLuint m_conditionalRenderQuery = 0;
GLenum m_conditionalRenderMode = GL_NONE;
// Process-wide, never-reused. See GetBoundTransformFeedbackLifetimeId(); same
// contract as BufferObject::AllocateLifetimeId().
static Uint64 AllocateTransformFeedbackLifetimeId();
// Everything a transform feedback object owns while it is NOT the bound one.
struct TransformFeedbackObjectState {
struct SavedBufferBinding {
@@ -532,6 +556,10 @@ namespace MobileGL {
Uint64 recordedVertices = 0;
Bool hasCompletedSpan = false;
Bool everBound = false;
// Assigned by the default member initialiser, so every way an object comes into
// being - operator[] materialisation, `= {}` in Gen/Create - gets a fresh one,
// and a recycled NAME never brings the dead object's id back with it.
Uint64 lifetimeId = AllocateTransformFeedbackLifetimeId();
};
void SaveBoundTransformFeedbackState();
void RestoreBoundTransformFeedbackState();
@@ -540,6 +568,10 @@ namespace MobileGL {
UnorderedMap<Uint, TransformFeedbackObjectState> m_transformFeedbackObjects;
IndexGenerator<Uint> m_transformFeedbackNames;
Uint m_boundTransformFeedback = 0;
// Mirror of m_transformFeedbackObjects[m_boundTransformFeedback].lifetimeId, so
// the per-draw read is a load rather than a hash lookup that could insert.
// Seeded by the constructor and rewritten by RestoreBoundTransformFeedbackState.
Uint64 m_boundTransformFeedbackLifetimeId = 0;
// Map membership is object EXISTENCE, which is not the same as the answer
// glIsProgramPipeline gives: any command that needs somewhere to put state
// materializes a reserved name, so the object can exist well before it is
@@ -9,9 +9,21 @@
#include "RenderbufferObject.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <atomic>
namespace MobileGL {
namespace MG_State {
namespace GLState {
namespace {
// Starts at 1 so a zero-initialized cache slot can never carry a live
// renderbuffer's id.
std::atomic<Uint64> g_nextRenderbufferLifetimeId{1};
}
Uint64 RenderbufferObject::AllocateLifetimeId() {
return g_nextRenderbufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
}
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
Uint RenderbufferObject::GetExternalIndex() const {
@@ -42,9 +42,20 @@ namespace MobileGL {
Int GetDepthSize() const;
Int GetStencilSize() const;
Int GetSamples() const;
// Globally-unique, never-reused id for THIS object's lifetime - same contract
// and same motivation as BufferObject::GetLifetimeId(),
// ProgramObject::GetLifetimeId() and VertexArrayObject::GetLifetimeId(). A
// backend that folds a renderbuffer's IDENTITY into a cache key must use this,
// never the GL name (LIFO-recycled by glGenRenderbuffers) and never the heap
// address (recycled by the allocator): both let a deleted-and-recreated
// renderbuffer answer to a dead one's cache entry.
Uint64 GetLifetimeId() const { return m_lifetimeId; }
private:
static Uint64 AllocateLifetimeId();
Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
TextureInternalFormat m_internalFormat = TextureInternalFormat::RGBA;
Int m_width = 0;
Int m_height = 0;
@@ -37,6 +37,11 @@ namespace {
std::size_t ioBlockDraws = 0;
// Behavior knobs, configured per test before running the probe.
GLint maxVertexSsboBlocks = 4;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, answered through glGetIntegeri_v.
// Above the GL minimums and distinct per axis, so a loader that left an initialiser in
// place or copied one axis into another is caught.
GLint maxComputeWorkGroupCount[3] = {70001, 70002, 70003};
GLint maxComputeWorkGroupSize[3] = {1500, 1501, 100};
GLint glesMajorVersion = 3;
GLint glesMinorVersion = 1;
GLint maxVertexImageUniforms = 2;
@@ -460,8 +465,20 @@ namespace {
if (data == nullptr) return;
for (int i = 0; i < 4; ++i) data[i] = GL_TRUE;
};
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) {
if (data != nullptr) *data = 0;
funcs.glGetIntegeri_v = [](GLenum pname, GLuint index, GLint* data) {
if (data == nullptr) return;
*data = 0;
if (index >= 3) return;
switch (pname) {
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
*data = g_fake.maxComputeWorkGroupCount[index];
break;
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
*data = g_fake.maxComputeWorkGroupSize[index];
break;
default:
break;
}
};
funcs.glGetProgramInfoLog = [](GLuint, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
if (infoLog != nullptr && bufSize > 0) infoLog[0] = '\0';
@@ -1551,3 +1568,23 @@ TEST(LocatedIoBlockProbe, ReportsTheDefectOnlyWhenTheUnlocatedControlCarriesTheP
EXPECT_FALSE(ProbeLocatedIoBlocksLosePayload(crippled).detected);
EXPECT_EQ(g_fake.ioBlockDraws, 0u) << "an entry-point-gated probe must not draw at all";
}
// The six per-axis compute limits are the backend-owned answers that cross the MGPipe boundary
// inside MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size), so the loader has
// to take EACH axis from glGetIntegeri_v rather than leave an initialiser - or one axis's
// answer - in the other slots. The integration side (AdvertisedLimitsScenario) pins the copy
// against the live getter on both backends; this pins the driver-to-caps step on its own.
TEST(ComputeWorkGroupCapabilities, TakesEveryAxisFromTheIndexedQuery) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
for (int axis = 0; axis < 3; ++axis) {
EXPECT_EQ(caps.MaxComputeWorkGroupCount[axis], g_fake.maxComputeWorkGroupCount[axis]) << "axis " << axis;
EXPECT_EQ(caps.MaxComputeWorkGroupSize[axis], g_fake.maxComputeWorkGroupSize[axis]) << "axis " << axis;
}
// The initialisers are the GL 4.3 minimums and every fake answer is above them, so a
// value equal to its initialiser here would mean the query never ran.
EXPECT_GT(caps.MaxComputeWorkGroupCount[0], 65535);
EXPECT_GT(caps.MaxComputeWorkGroupSize[2], 64);
}
+8
View File
@@ -85,6 +85,9 @@ add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
add_subdirectory(Pipeline)
# The MGPipe catalogue arithmetic: no GL context and no driver, just the .def, the seven
# generated files and the payload layouts.
add_subdirectory(Pipe)
add_subdirectory(ShaderTranspiler)
add_subdirectory(Util)
add_subdirectory(SelfTest)
@@ -94,3 +97,8 @@ add_subdirectory(Backend/DirectGLES)
if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan)
endif()
# The wire layer only exists in the disaggregated configuration, so its suite
# is only registered there. Nothing under MG_Remote is compiled otherwise.
if (MOBILEGL_BUILD_DISAGGREGATED)
add_subdirectory(Wire)
endif()
+28
View File
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
PipeCatalogueTest
PipeCatalogueTest.cpp
)
target_include_directories(PipeCatalogueTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
PipeCatalogueTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+289
View File
@@ -0,0 +1,289 @@
// MobileGL - MobileGL/MG_Test/Pipe/PipeCatalogueTest.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 arithmetic of the MGPipe catalogue (plan B section 4.4, appendix A). Everything here
// is cheap on purpose: it is the test that fails when PipeCalls.def and the seven generated
// files stop agreeing, and it must not need a GL context to say so.
#include <gtest/gtest.h>
#include <cstring>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// Counting expansions of the catalogue. The Class parameter is a real enumerator, so a
// per-class count is a constant expression too.
#define MGP_COUNT_ONE(Name, Payload, Class, Flags) +1
#define MGP_COUNT_CLASS(Name, Payload, Class, Flags) +((Class) == countedClass ? 1 : 0)
constexpr SizeT kExpandedCallCount = 0 MGP_CALL_LIST(MGP_COUNT_ONE);
template <MGPipeCallClass countedClass>
constexpr SizeT ClassCount() {
return 0 MGP_CALL_LIST(MGP_COUNT_CLASS);
}
// Every payload named in the catalogue must be a memcpy-able POD, and so must every
// payload the verify comparator knows about.
#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags) \
static_assert(std::is_trivially_copyable_v<Payload>, #Name "'s payload " #Payload " is not trivially copyable");
MGP_CALL_LIST(MGP_ASSERT_CALL_PAYLOAD_POD)
#define MGP_ASSERT_VERIFY_PAYLOAD_POD(Payload) \
static_assert(std::is_trivially_copyable_v<Payload>, #Payload " is not trivially copyable");
MGP_VERIFY_PAYLOAD_LIST(MGP_ASSERT_VERIFY_PAYLOAD_POD)
} // namespace
// The handle is the whole object model. Eight bytes, a register pair, no padding.
TEST(PipeCatalogue, HandleIsEightBytes) {
static_assert(sizeof(MGPipeHandle) == 8);
static_assert(alignof(MGPipeHandle) == 4);
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
EXPECT_EQ(sizeof(MGPipeHandle), 8u);
// The two reserved handles, and the composite band that the program-pipeline resolver
// allocates out of.
EXPECT_TRUE(MGPipeHandleIsNull(kMGPipeNullHandle));
EXPECT_FALSE(MGPipeHandleIsNull(kMGPipeDefaultFramebuffer));
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeFirstAllocatableSlot));
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoCompositeSlotBase));
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoSlotLimit));
}
// The catalogue, the number documented in its header, and the two generated tables are one
// fact stated three times. This is the test that notices when they stop being.
TEST(PipeCatalogue, EntryCountMatchesTheDocumentedCount) {
static_assert(kExpandedCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT);
static_assert(kExpandedCallCount == kMGPipeCallCount);
EXPECT_EQ(kExpandedCallCount, static_cast<SizeT>(MGP_CALL_LIST_DOCUMENTED_COUNT));
EXPECT_EQ(kMGPipeCallCount, kExpandedCallCount);
}
TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
static_assert(ClassCount<kScreen>() == kMGPipeScreenCallCount);
static_assert(ClassCount<kScreen>() + ClassCount<kCtxCso>() + ClassCount<kCtxState>() +
ClassCount<kCtxObject>() + ClassCount<kCtxVerb>() + ClassCount<kCtxQuery>() ==
kMGPipeCallCount);
// The tables ARE their function pointers: a struct that is bigger than its call count
// has grown a member no generator knows about.
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()));
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()));
EXPECT_EQ(kMGPipeScreenCallCount, ClassCount<kScreen>());
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
// The per-class counts PipeCalls.def documents in its header.
EXPECT_EQ(ClassCount<kScreen>(), 11u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 17u);
EXPECT_EQ(ClassCount<kCtxObject>(), 9u);
EXPECT_EQ(ClassCount<kCtxVerb>(), 13u);
}
// An uninstalled pipe is every entry null - which is exactly what "this subsystem has not
// been migrated, keep pulling" means (plan B section 4.1).
TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
EXPECT_EQ(screen[i], nullptr) << "screen entry " << i;
}
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
EXPECT_EQ(context[i], nullptr) << "context entry " << i;
}
}
// The retirement ratchet of the migration carrier (section 6.3): the constant and the
// struct must agree, and the constant only ever goes down.
TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE);
EXPECT_EQ(sizeof(ResidualValueBlock), static_cast<SizeT>(MGL_RESIDUAL_BLOCK_SIZE));
// It carries the whole of both value structs today; that is what the later stages eat.
EXPECT_GE(sizeof(ResidualValueBlock), sizeof(RenderStateParameters) + sizeof(PixelStoreParameters));
}
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), kMGPipeCallCount + 1);
EXPECT_EQ(sizeof(MGPWireRecHeader), 8u);
// Every record is a multiple of the stream's 8-byte granularity, which is half of the
// applier's precondition.
EXPECT_EQ(sizeof(MGPWireRec_DrawVbo) % 8, 0u);
EXPECT_EQ(sizeof(MGPWireRec_BindRenderState) % 8, 0u);
EXPECT_EQ(sizeof(MGPWireRec_SetResidualValueState) % 8, 0u);
}
// Records are append-only. The three carriers added after the first cut - for the live
// GLFunctionsTable entries GetGpuTimestampNs, QueryCounterTimestamp and WaitSync - sit at
// the END of the list, after SetSwapInterval, so no opcode the first cut assigned has moved.
TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetSwapInterval), 68);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryTimestamp), 69);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryCounter), 70);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::FenceWaitServer), 71);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 72);
}
// A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted"
// is reported as "not applied" rather than "fatal".
TEST(PipeCatalogue, ApplierAcceptsAWellFormedRecord) {
MGPWireRec_Present record{};
record.Header.Op = static_cast<Uint16>(MGPWireOp::Present);
record.Header.Size = sizeof(record);
record.Payload.FrameSerial = 42;
EXPECT_FALSE(MGPipeApplyWireRecord(MGPWireOp::Present, &record, sizeof(record), sizeof(record)));
}
// G4 reports the FIRST differing field by name, and compares field by field so that
// padding cannot produce a difference that does not exist.
TEST(PipeCatalogue, VerifyComparatorNamesTheDifferingField) {
MGPDrawInfo a{};
MGPDrawInfo b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.InstanceCount = 7;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "InstanceCount");
// Padding bytes are not fields: writing to them cannot make two payloads differ.
MGPBindRenderState c{};
MGPBindRenderState d{};
c.Cso = MGPipeHandle{3, 1};
d.Cso = MGPipeHandle{3, 1};
field = nullptr;
EXPECT_TRUE(MGPipeVerify(c, d, &field));
// Nested payloads recurse, and arrays compare element-wise.
MGPFramebufferState left{};
MGPFramebufferState right{};
right.Color[3].Level = 2;
EXPECT_FALSE(MGPipeVerify(left, right, &field));
EXPECT_STREQ(field, "Color");
}
// G6's join over the backend read inventory. P0 allows unmapped rows; from P5 the gate is
// zero, so the numbers are asserted here to make a regression visible the day it happens.
TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
EXPECT_EQ(kMGPipeInventoryReadPoints, 477u);
EXPECT_EQ(kMGPipeInventoryUnmapped, 0u);
EXPECT_EQ(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
kMGPipeInventoryUnmapped,
kMGPipeInventoryReadPoints);
EXPECT_GT(kMGPipeCoverageEntryCount, 0u);
}
// G5's field ids come from the same accessor list as the coverage table, and every field
// starts un-filled: reading one before its verb fills it is the poison's whole job.
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
EXPECT_EQ(kMGPipeInputFieldCount, 61u);
MGPipeFilledState state{};
state.CurrentVerbSerial = 1;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
state.FilledGen[static_cast<SizeT>(MGPipeInputField::GetRenderStateParameters)] = 1;
EXPECT_TRUE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
// The next verb makes the same value stale, which a written-once bitmap could not see.
state.CurrentVerbSerial = 2;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
}
// G7 pins the member list the pipeline/dynamic split is derived from.
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "CullFaceEnabled");
EXPECT_STREQ(kMGPipePipelineStateMembers[kMGPipePipelineStateMemberCount - 1], "ColorMasks");
}
// The reverse channel is exactly ten callbacks (section 7.1).
TEST(PipeCatalogue, ReverseChannelHasTenCallbacks) {
EXPECT_EQ(kMGPipeCallbackCount, 10u);
EXPECT_EQ(sizeof(MGPipeCallbacks), kMGPipeCallbackCount * sizeof(void (*)()));
}
// The one shape that changes with the transport. In a monolith it resolves to the pointer
// it was given; with no transport installed a segment-backed span resolves to nothing
// rather than to garbage.
TEST(PipeCatalogue, HostSpanResolvesTheMonolithPointer) {
static_assert(sizeof(MGHostSpan) == 32);
const Uint8 bytes[8] = {0, 1, 2, 3, 4, 5, 6, 7};
MGHostSpan span{};
span.Ptr = bytes;
span.Size = sizeof(bytes);
span.Offset = 2;
EXPECT_EQ(MGPipeHostBytes(span), bytes + 2);
MGHostSpan staged{};
staged.Seg = 4;
staged.Size = 16;
EXPECT_EQ(gMGPipeSegmentResolver, nullptr);
EXPECT_EQ(MGPipeHostBytes(staged), nullptr);
}
// D-B8: a bound buffer range carries no inline host span. The named-UBO bytes are an
// optional second var-tail announced by HostSpanCount, so the SSBO, atomic-counter and XFB
// ranges - the majority - pay nothing for a payload whose shape is not frozen yet.
TEST(PipeCatalogue, BufferRangeCarriesNoInlineHostSpan) {
static_assert(sizeof(MGPBufferRange) == 24);
static_assert(sizeof(MGPShaderBuffers) == 32);
EXPECT_LT(sizeof(MGPBufferRange), sizeof(MGHostSpan));
// The call still declares the span it may carry, so the transport lays the tail out.
Uint32 flags = 0;
#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags) \
if (std::strcmp(#Name, "SetShaderBuffers") == 0) flags = static_cast<Uint32>(Flags);
MGP_CALL_LIST(MGP_FLAGS_OF_SET_SHADER_BUFFERS)
#undef MGP_FLAGS_OF_SET_SHADER_BUFFERS
EXPECT_EQ(flags & (kVarTail | kHostSpan), static_cast<Uint32>(kVarTail | kHostSpan));
// And the comparator sees the count that announces the tail.
MGPShaderBuffers a{};
MGPShaderBuffers b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.HostSpanCount = 4;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "HostSpanCount");
}
// The buffer half of resource_subdata has no level and no box of its own: [offset, size)
// rides in UnionBox.X / UnionBox.W, and only through the two helpers, which also say where
// one record stops and the emitter has to split.
TEST(PipeCatalogue, SubDataBufferRangeRidesInTheUnionBox) {
MGPSubData record{};
record.Level = 3;
record.RegionCount = 2;
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 4096, 65536));
EXPECT_EQ(record.UnionBox.X, 4096);
EXPECT_EQ(record.UnionBox.W, 65536u);
EXPECT_EQ(record.UnionBox.Y, 0);
EXPECT_EQ(record.UnionBox.Z, 0);
EXPECT_EQ(record.UnionBox.H, 1u);
EXPECT_EQ(record.UnionBox.D, 1u);
EXPECT_EQ(record.Level, 0);
EXPECT_EQ(record.RegionCount, 0u);
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 4096u);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 65536u);
// The largest range one record expresses...
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 0x7FFFFFFFull, 0xFFFFFFFFull));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
// ...and beyond it the emitter splits: refused, record untouched.
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0x80000000ull, 1));
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0, 0x100000000ull));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
}
+25
View File
@@ -45,6 +45,31 @@ endif()
include(GoogleTest)
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
TransformFeedbackLifetimeIdTest
TransformFeedbackLifetimeIdTest.cpp
)
target_include_directories(TransformFeedbackLifetimeIdTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TransformFeedbackLifetimeIdTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(TransformFeedbackLifetimeIdTest PRIVATE /Zc:preprocessor)
endif()
gtest_discover_tests(TransformFeedbackLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
RenderStateTest
RenderStateTest.cpp
@@ -32,6 +32,7 @@
#include "Includes.h"
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
using namespace MobileGL;
@@ -138,3 +139,20 @@ TEST(ObjectLifetimeIdTest, LiveVertexArrayObjectsHaveDistinctLifetimeIds) {
TEST(ObjectLifetimeIdTest, LiveBufferObjectsHaveDistinctLifetimeIds) {
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::BufferObject>("BufferObject");
}
// The renderbuffer had no lifetime id at all until plan B §11 P0 gave it one: it is
// the one FBO attachment source whose identity a backend twin registry can only have
// keyed on the heap address or the GL name, both of which recycle.
TEST(ObjectLifetimeIdTest, RenderbufferObjectAtARecycledAddressCarriesAFreshLifetimeId) {
using MG_State::GLState::RenderbufferObject;
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<RenderbufferObject>("RenderbufferObject");
if (reuseCount == 0) {
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
"construct/destroy rounds, so the recycled-address case was never exercised";
}
RecordProperty("address_reuses_observed", reuseCount);
}
TEST(ObjectLifetimeIdTest, LiveRenderbufferObjectsHaveDistinctLifetimeIds) {
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::RenderbufferObject>("RenderbufferObject");
}
@@ -0,0 +1,143 @@
// MobileGL - MobileGL/MG_Test/State/TransformFeedbackLifetimeIdTest.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
//
// D21 (plan B v2 §4.7.3): DirectVulkan hands every transform feedback object one of sixteen
// counter-buffer groups, and the group carries that span's resume offset. The map was keyed on
// the GL NAME, which glGenTransformFeedbacks recycles the moment the object is deleted, so an
// object created on a recycled name was served the DEAD object's group together with its
// m_xfbCountersValid / m_xfbLastSeenGeneration entries.
//
// The transform feedback object is a plain struct inside a map rather than a heap object, so the
// reuse to defend against is the NAME's, not an address's - which is why these cases live here
// and not in ObjectLifetimeIdTest.cpp with the heap-allocated object types. Keeping them in
// their own translation unit also keeps the D21 commit textually independent of the rest of the
// branch, which plan B §10.4-5 asks for so it can be cherry-picked to dev on its own.
//
// The second case pins the OTHER half of the backend contract: a bounded slot table has to be
// able to tell an object whose span is still open (and may yet resume) from one whose span is
// closed or whose object is gone.
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
namespace {
MG_State::GLState::GLContext& FreshContext() {
MobileGL::Initialize();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
return *MG_State::pGLContext;
}
} // namespace
TEST(TransformFeedbackLifetimeIdTest, AnObjectAtARecycledNameCarriesAFreshLifetimeId) {
auto& context = FreshContext();
// Before anything is bound. The default object exists from the start of the context, and the
// identity has to exist with it: a backend reading 0 here would match every FREE slot in its
// table without ever claiming one, which is the same bug this id was added to remove.
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), 0u)
<< "the default transform feedback object has no identity until something binds it";
Vector<Uint> names;
context.GenTransformFeedbackNames(1, names);
ASSERT_EQ(names.size(), 1u);
const Uint name = names[0];
ASSERT_NE(name, 0u);
context.BindTransformFeedbackObject(name);
const Uint64 firstId = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_NE(firstId, 0u) << "a live transform feedback object answered to id 0, which is the value a "
"zero-initialised backend slot already carries";
// The default object is a different object and must not share the id.
context.BindTransformFeedbackObject(0);
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), firstId)
<< "the default transform feedback object shares an identity with a generated one";
// Deleting while bound reverts to the default object (GL 4.6 core 13.2.1), which is the
// shape the backend sees; delete from there anyway so the test does not depend on it.
context.BindTransformFeedbackObject(name);
context.MarkTransformFeedbackObjectForDeletion(name);
Vector<Uint> reborn;
context.GenTransformFeedbackNames(1, reborn);
ASSERT_EQ(reborn.size(), 1u);
if (reborn[0] != name) {
GTEST_SKIP() << "inconclusive, not proven: the name generator did not hand the deleted name back, so "
"the recycled-name case was never exercised";
}
context.BindTransformFeedbackObject(reborn[0]);
EXPECT_NE(context.GetBoundTransformFeedbackLifetimeId(), firstId)
<< "a transform feedback object created on a recycled name reports the DEAD object's lifetime id - "
"DirectVulkan would hand it the dead span's counter slot, and with it that span's resume state";
}
// The predicate DirectVulkan's slot table asks before it takes a group over. The case that
// matters is the middle one: object A is PAUSED and another object is bound and capturing, so A
// looks completely idle to a least-recently-used rule while being exactly the object whose
// counters must survive.
TEST(TransformFeedbackLifetimeIdTest, APausedSpanStaysOpenWhileAnotherObjectCaptures) {
auto& context = FreshContext();
Vector<Uint> names;
context.GenTransformFeedbackNames(2, names);
ASSERT_EQ(names.size(), 2u);
const Uint nameA = names[0];
const Uint nameB = names[1];
context.BindTransformFeedbackObject(nameA);
const Uint64 idA = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idA)) << "an object that never began a span reads as open";
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// Pausing is what makes interleaving legal (ARB_transform_feedback2); the span is still open.
context.SetTransformFeedbackPaused(true);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// Now the shape the slot table sees: B is bound and capturing, A is paused and untouched.
context.BindTransformFeedbackObject(nameB);
const Uint64 idB = context.GetBoundTransformFeedbackLifetimeId();
EXPECT_NE(idB, idA);
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA))
<< "a paused span stopped reading as open the moment another object was bound - a backend "
"reclaiming slots by 'is this owner still going' would take A's counters away";
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idB));
context.EndTransformFeedback();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idB)) << "a closed span still reads as open";
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idA));
// A closes its own span; its slot becomes reclaimable.
context.BindTransformFeedbackObject(nameA);
context.EndTransformFeedback();
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idA));
// A deleted object can never resume, so its identity must not hold a slot either.
context.BindTransformFeedbackObject(nameB);
context.BeginTransformFeedback(GL_POINTS, nullptr);
EXPECT_TRUE(context.HasOpenTransformFeedbackSpan(idB));
context.MarkTransformFeedbackObjectForDeletion(nameB);
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(idB))
<< "the identity of a deleted transform feedback object still claims an open span, so its counter "
"group would be pinned for the life of the context";
// Identities the context never issued, and the free-slot sentinel, are not open spans.
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(0));
EXPECT_FALSE(context.HasOpenTransformFeedbackSpan(~0ull));
}
+19
View File
@@ -35,6 +35,25 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# The MGPipe boundary counters: the enable latch, the byte/call/gate arithmetic, the
# payload histogram's bucketing and the two report formats. No GL context, no driver.
add_executable(
PipeStatsTest
PipeStatsTest.cpp
)
target_include_directories(PipeStatsTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
PipeStatsTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeStatsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+287
View File
@@ -0,0 +1,287 @@
// MobileGL - MobileGL/MG_Test/Util/PipeStatsTest.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 MGPipe boundary counters (plan B section 11 P0, corollary in section 2.3.1).
// No GL context and no driver: the module is arithmetic over a fixed set of counters,
// which is exactly what has to be pinned before anyone reads a number off a device.
#include <gtest/gtest.h>
#include <Config.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdio>
#include <fstream>
#include <sstream>
namespace {
namespace PS = MobileGL::MG_Util::PipeStats;
using MobileGL::String;
using MobileGL::Uint32;
using MobileGL::Uint64;
class PipeStatsTest : public ::testing::Test {
protected:
void SetUp() override {
PS::ResetForTesting();
PS::SetEnabledForTesting(true);
}
void TearDown() override {
PS::SetEnabledForTesting(false);
PS::ResetForTesting();
}
};
// The off latch is the whole cost argument: every counting site in the two backends is
// written as `if (Enabled()) ...`, so a false latch has to mean "nothing is counted".
TEST_F(PipeStatsTest, EnabledLatchIsTheOnlyGate) {
PS::SetEnabledForTesting(false);
EXPECT_FALSE(PS::Enabled());
PS::SetEnabledForTesting(true);
EXPECT_TRUE(PS::Enabled());
}
TEST_F(PipeStatsTest, ByteClassesAccumulateIndependently) {
PS::AddBytes(PS::ByteClass::StageBuffer, 100);
PS::AddBytes(PS::ByteClass::StageBuffer, 40);
PS::AddBytes(PS::ByteClass::StageTexture, 7);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageBuffer), 140u);
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageBuffer), 140u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 7u);
// Every other class untouched, the residual-value-block placeholder included.
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboGlobal), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboNamed), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageIndirectCmd), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::ResidualValueBlock), 0u);
}
// The frame accumulator is what feeds TracyPlot; the run total is what feeds the JSON
// dump. A present must clear the first and keep the second.
TEST_F(PipeStatsTest, PresentClearsTheFrameButKeepsTheTotal) {
PS::AddBytes(PS::ByteClass::StageTexture, 512);
PS::AddCalls(PS::CallClass::Draws, 3);
PS::CountGate(PS::Gate::EsprytRenderState, /*hit=*/true);
PS::OnPresent();
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageTexture), 0u);
EXPECT_EQ(PS::FrameCalls(PS::CallClass::Draws), 0u);
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 512u);
EXPECT_EQ(PS::TotalCalls(PS::CallClass::Draws), 3u);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::EsprytRenderState), 1u);
EXPECT_EQ(PS::FrameCount(), 1u);
}
TEST_F(PipeStatsTest, InitLatchesTheSummaryPeriodFromTheConfigAndNeverKeepsZero) {
// The device retrace harness never reaches the teardown dump, so the summary
// cadence is the only way a short fixture yields numbers at all: it must follow
// MOBILEGL_PIPE_STATS_PERIOD, and a zero must fall back rather than divide.
const Uint32 saved = MobileGL::MG_Config::Features.PipeStatsPeriod;
MobileGL::MG_Config::Features.PipeStatsPeriod = 7;
PS::Init();
EXPECT_EQ(PS::SummaryFramePeriod(), 7u);
MobileGL::MG_Config::Features.PipeStatsPeriod = 0;
PS::Init();
EXPECT_EQ(PS::SummaryFramePeriod(), PS::kDefaultSummaryFramePeriod);
MobileGL::MG_Config::Features.PipeStatsPeriod = saved;
PS::Init();
}
TEST_F(PipeStatsTest, GateHitsAndMissesAreSeparateCounters) {
for (Uint32 i = 0; i < 5; ++i) {
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/true);
}
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/false);
PS::CountGate(PS::Gate::MagmaDrawFastPath, /*hit=*/false);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaPipelineMemo), 5u);
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaPipelineMemo), 1u);
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaDrawFastPath), 0u);
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaDrawFastPath), 1u);
}
// Bucket 0 is "no payload"; bucket n>0 is [2^(n-1), 2^n). The placeholder histogram is
// the SEG_CMD sizing input (section 4.5.7), so its bucketing is pinned now rather than
// when a generator first calls it.
TEST_F(PipeStatsTest, PayloadHistogramBucketsByPowerOfTwo) {
PS::RecordDrawPayloadBytes(0);
PS::RecordDrawPayloadBytes(1); // [1, 2) -> bucket 1
PS::RecordDrawPayloadBytes(2); // [2, 4) -> bucket 2
PS::RecordDrawPayloadBytes(3); // [2, 4) -> bucket 2
PS::RecordDrawPayloadBytes(48); // [32, 64) -> bucket 6
PS::RecordDrawPayloadBytes(64); // [64, 128)-> bucket 7
EXPECT_EQ(PS::TotalPayloadBucket(0), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(1), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(2), 2u);
EXPECT_EQ(PS::TotalPayloadBucket(6), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(7), 1u);
}
// A record far larger than the last bucket must land in the last bucket, not past the
// end of the array.
TEST_F(PipeStatsTest, PayloadHistogramSaturatesInsteadOfOverflowing) {
PS::RecordDrawPayloadBytes(~Uint64{0});
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets - 1), 1u);
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets), 0u);
}
// The summary line's shape is what an operator greps and what the smoke check in this
// package matches, so it is pinned here rather than left to the log reader's memory.
TEST_F(PipeStatsTest, SummaryLineCarriesEveryClassAndGate) {
PS::AddCalls(PS::CallClass::Draws, 4);
PS::AddCalls(PS::CallClass::AccessorCalls, 50);
PS::AddBytes(PS::ByteClass::StageBuffer, 4096);
PS::OnPresent();
const String line = PS::FormatWindowLine();
EXPECT_NE(line.find("MGPipe stats:"), String::npos) << line;
EXPECT_NE(line.find("draws=4"), String::npos) << line;
// 50 accessor calls over 4 draws, two decimals, no <iomanip>.
EXPECT_NE(line.find("acc/draw=12.50"), String::npos) << line;
EXPECT_NE(line.find("buf=4096.00"), String::npos) << line;
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
EXPECT_NE(line.find("="), String::npos);
}
EXPECT_NE(line.find("gates["), String::npos) << line;
EXPECT_NE(line.find("tex[emit="), String::npos) << line;
}
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
// and load-bearing (bytes/f sizes SEG_STAGE), and integer division silently rounds a
// whole unit off each of them. 26 draws over 14 frames is 1.86, not 1.
TEST_F(PipeStatsTest, PerFrameFieldsKeepTwoDecimals) {
PS::AddCalls(PS::CallClass::Draws, 26);
PS::AddBytes(PS::ByteClass::StageBuffer, 1360);
for (Uint32 i = 0; i < 14; ++i) {
PS::OnPresent();
}
const String line = PS::FormatWindowLine();
EXPECT_NE(line.find("draws/f=1.86"), String::npos) << line;
EXPECT_NE(line.find("buf=97.14"), String::npos) << line;
}
// Successive summaries report WINDOWS, not run totals: a run total over a workload that
// changes shape (load, then steady state) averages away the very number section 2.3.1
// wants. Advancing the window is an explicit call, not a side effect of formatting.
TEST_F(PipeStatsTest, SummaryLinesReportDisjointWindows) {
PS::AddCalls(PS::CallClass::Draws, 10);
PS::OnPresent();
const String first = PS::FormatWindowLine();
EXPECT_NE(first.find("draws=10"), String::npos) << first;
PS::AdvanceSummaryWindow();
PS::AddCalls(PS::CallClass::Draws, 3);
PS::OnPresent();
const String second = PS::FormatWindowLine();
EXPECT_NE(second.find("draws=3"), String::npos) << second;
EXPECT_NE(second.find("frames=2"), String::npos) << second;
}
// FormatWindowLine is pure. It used to rewrite the window bases as a side effect of
// formatting, so any second reader - a probe, a test, a second reporting channel -
// silently zeroed the next window.
TEST_F(PipeStatsTest, FormattingTwiceDoesNotConsumeTheWindow) {
PS::AddCalls(PS::CallClass::Draws, 7);
PS::OnPresent();
const String first = PS::FormatWindowLine();
const String second = PS::FormatWindowLine();
EXPECT_EQ(first, second) << first << "\n" << second;
EXPECT_NE(second.find("draws=7"), String::npos) << second;
// ...and advancing explicitly does close it.
PS::AdvanceSummaryWindow();
const String third = PS::FormatWindowLine();
EXPECT_NE(third.find("draws=0"), String::npos) << third;
}
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroDraws) {
PS::AddCalls(PS::CallClass::AccessorCalls, 12);
PS::OnPresent();
const String line = PS::FormatWindowLine();
// No draw in the window means there is no per-draw number - and "0.00" beside a
// non-zero acc= would read as one.
EXPECT_NE(line.find("acc/draw=n/a"), String::npos) << line;
EXPECT_NE(line.find("acc=12"), String::npos) << line;
}
// A window with no Present in it has no per-frame reading at all. This used to divide by
// a faked 1 and print the window TOTALS under a "/f" label: a scenario slice that draws
// 47 times and never presents reported 1,404,550 staged bytes as a per-frame figure,
// which is a 47x overstatement of the SEG_STAGE sizing input this package exists to
// produce.
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroFrames) {
PS::AddCalls(PS::CallClass::Draws, 47);
PS::AddBytes(PS::ByteClass::StageBuffer, 1404550);
const String line = PS::FormatWindowLine();
EXPECT_EQ(PS::FrameCount(), 0u);
EXPECT_NE(line.find("window=0"), String::npos) << line;
EXPECT_NE(line.find("draws/f=n/a"), String::npos) << line;
// The bracket is relabelled rather than divided: totals, and marked as totals.
EXPECT_EQ(line.find("bytes/f["), String::npos) << line;
EXPECT_NE(line.find("bytes[buf=1404550"), String::npos) << line;
}
TEST_F(PipeStatsTest, JsonDumpNamesEveryCounter) {
PS::AddBytes(PS::ByteClass::StageUboNamed, 256);
PS::CountGate(PS::Gate::MagmaDynamicTail, /*hit=*/false);
PS::RecordDrawPayloadBytes(9);
PS::OnPresent();
const String json = PS::FormatJson();
for (Uint32 i = 0; i < static_cast<Uint32>(PS::ByteClass::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::ByteClass>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
for (Uint32 i = 0; i < static_cast<Uint32>(PS::CallClass::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::CallClass>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
const String name = PS::NameOf(static_cast<PS::Gate>(i));
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
}
EXPECT_NE(json.find("\"stage-ubo-named\": 256"), String::npos) << json;
EXPECT_NE(json.find("\"frames\": 1"), String::npos) << json;
EXPECT_NE(json.find("cmd-bytes-per-draw-histogram"), String::npos) << json;
}
// The counter names are the TracyPlot series names and the JSON keys; a rename is a
// breaking change for every recorded baseline, so the whole set is pinned.
TEST_F(PipeStatsTest, CounterNamesAreStable) {
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageBuffer), "stage-buffer");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageTexture), "stage-texture");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboGlobal), "stage-ubo-global");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboNamed), "stage-ubo-named");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageVertexClient), "stage-vertex-client");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndexClient), "stage-index-client");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndirectCmd), "stage-indirect-cmd");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::PersistentMapPush), "persistent-map-push");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::ResidualValueBlock), "residual-value-block");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytUnitBindingsEpoch), "espryt-unit-bindings-epoch");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDrawFastPath), "magma-draw-fastpath");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaPipelineMemo), "magma-pipeline-memo");
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDynamicTail), "magma-dynamic-tail");
}
// A summary is emitted every kSummaryFramePeriod presents. The period is a constant the
// smoke check depends on, so a change to it has to break a test.
TEST_F(PipeStatsTest, SummaryPeriodIsOneHundredAndTwentyFrames) {
EXPECT_EQ(PS::SummaryFramePeriod(), 120u);
for (Uint64 i = 0; i < PS::SummaryFramePeriod(); ++i) {
PS::OnPresent();
}
EXPECT_EQ(PS::FrameCount(), PS::SummaryFramePeriod());
}
} // namespace
+36
View File
@@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.14)
# The MG_Remote wire layer: framing, the SPSC ring, the in-process transport,
# SCM_RIGHTS descriptor passing and the generated control-plane schema. Only
# reachable with MOBILEGL_BUILD_DISAGGREGATED=ON (see MG_Test/CMakeLists.txt).
set(MOBILEGL_WIRE_TESTS
FramingTest
RingTest
InProcessTransportTest
ProtocolSmokeTest
)
if (NOT WIN32)
# SCM_RIGHTS and fork(): POSIX only.
list(APPEND MOBILEGL_WIRE_TESTS FdPassingTest)
endif()
include(GoogleTest)
foreach (test IN LISTS MOBILEGL_WIRE_TESTS)
add_executable(${test} ${test}.cpp)
target_include_directories(${test} PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/flatbuffers/include
)
target_link_libraries(${test} PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
gtest_discover_tests(${test} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
endforeach ()
+305
View File
@@ -0,0 +1,305 @@
// MobileGL - MobileGL/MG_Test/Wire/FdPassingTest.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
// SCM_RIGHTS across a real process boundary: a forked child creates a shared
// segment, fills it, and hands the descriptor over the aux socket; the parent
// adopts it, maps it read-only and compares every byte.
//
// This is the test the earlier branch never had. Its transport hardcoded
// `out->fd = -1` in the offer poll, so its data plane could not move a byte
// between processes - and nothing in its suite noticed, because everything ran
// in one process.
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/ShmSegment.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <string>
#include <sys/socket.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
constexpr std::uint64_t kSegmentSize = 64 * 1024;
std::uint8_t ByteAt(std::uint64_t index) {
return static_cast<std::uint8_t>((index * 31u + 7u) & 0xFFu);
}
// Child-side exit codes, so a failure says where it happened.
enum ChildStatus : int {
kChildOk = 0,
kChildCreateFailed = 2,
kChildMapFailed = 3,
kChildSendFailed = 4,
};
} // namespace
TEST(FdPassingTest, IsSupportedOnThisPlatform) { EXPECT_TRUE(FdPassing::Supported()); }
TEST(FdPassingTest, ChildSharesASegmentThatTheParentMapsAndVerifies) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
const std::string sideband = "SegmentRef{id=7,kind=Stage}";
const pid_t pid = ::fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
// Child. No gtest assertions here: a failed expectation in a forked
// child would report into a copy of the parent's test state.
::close(sockets[0]);
int status = kChildOk;
ShmSegment segment;
if (ShmSegment::Create("fdpass", kSegmentSize, segment) != MOBILEGL_OK) {
status = kChildCreateFailed;
} else if (segment.Map(false) != MOBILEGL_OK) {
status = kChildMapFailed;
} else {
auto* bytes = static_cast<std::uint8_t*>(segment.Data());
for (std::uint64_t i = 0; i < kSegmentSize; ++i) {
bytes[i] = ByteAt(i);
}
const MobileGLByteSpan span{sideband.data(), sideband.size()};
if (FdPassing::SendFd(sockets[1], segment.Fd(), span) != MOBILEGL_OK) {
status = kChildSendFailed;
}
}
::close(sockets[1]);
::_exit(status);
}
// Parent.
::close(sockets[1]);
// A destination smaller than kMaxSidebandBytes is refused BEFORE the
// datagram is consumed, so the descriptor is not lost by a caller that
// guessed the size wrong.
std::vector<std::uint8_t> small(8);
int fd = -1;
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, smallSpan, &required, 5000),
MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, FdPassing::kMaxSidebandBytes);
EXPECT_EQ(fd, -1);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan sidebandSpan{sidebandBuffer.data(), sidebandBuffer.size()};
ASSERT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, sidebandSpan, &sidebandSize, 5000),
MOBILEGL_OK);
ASSERT_GE(fd, 0);
EXPECT_EQ(std::string(reinterpret_cast<const char*>(sidebandBuffer.data()),
static_cast<std::size_t>(sidebandSize)),
sideband);
ShmSegment adopted;
ASSERT_EQ(ShmSegment::Adopt(fd, kSegmentSize, adopted), MOBILEGL_OK);
EXPECT_TRUE(adopted.Valid());
ASSERT_EQ(adopted.Map(true), MOBILEGL_OK);
EXPECT_TRUE(adopted.MappedReadOnly());
const auto* bytes = static_cast<const std::uint8_t*>(adopted.Data());
ASSERT_NE(bytes, nullptr);
std::uint64_t mismatches = 0;
for (std::uint64_t i = 0; i < kSegmentSize; ++i) {
if (bytes[i] != ByteAt(i)) {
++mismatches;
}
}
EXPECT_EQ(mismatches, 0u);
int childStatus = 0;
ASSERT_EQ(::waitpid(pid, &childStatus, 0), pid);
ASSERT_TRUE(WIFEXITED(childStatus));
EXPECT_EQ(WEXITSTATUS(childStatus), kChildOk);
::close(sockets[0]);
}
TEST(FdPassingTest, ReceiveTimesOutWithNoOffer) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
int fd = -1;
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan span{sidebandBuffer.data(), sidebandBuffer.size()};
EXPECT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, span, &sidebandSize, 20), MOBILEGL_ERR_TIMEOUT);
EXPECT_EQ(fd, -1);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, RejectsBadArguments) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
const std::vector<std::uint8_t> tooBig(FdPassing::kMaxSidebandBytes + 1, 0);
const MobileGLByteSpan oversized{tooBig.data(), tooBig.size()};
EXPECT_EQ(FdPassing::SendFd(sockets[1], sockets[0], oversized), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_EQ(FdPassing::SendFd(sockets[1], -1, MobileGLByteSpan{nullptr, 0}),
MOBILEGL_ERR_INVALID_ARGUMENT);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, SegmentWithoutASidebandStillCarriesItsDescriptor) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
ShmSegment segment;
ASSERT_EQ(ShmSegment::Create("nosideband", 4096, segment), MOBILEGL_OK);
ASSERT_EQ(segment.Map(false), MOBILEGL_OK);
static_cast<std::uint8_t*>(segment.Data())[0] = 0xA5;
ASSERT_EQ(FdPassing::SendFd(sockets[1], segment.Fd(), MobileGLByteSpan{nullptr, 0}),
MOBILEGL_OK);
std::vector<std::uint8_t> sidebandBuffer(FdPassing::kMaxSidebandBytes);
int fd = -1;
std::uint64_t sidebandSize = 123;
MobileGLMutableByteSpan span{sidebandBuffer.data(), sidebandBuffer.size()};
ASSERT_EQ(FdPassing::ReceiveFd(sockets[0], &fd, span, &sidebandSize, 5000), MOBILEGL_OK);
EXPECT_EQ(sidebandSize, 0u);
ASSERT_GE(fd, 0);
ShmSegment adopted;
ASSERT_EQ(ShmSegment::Adopt(fd, 4096, adopted), MOBILEGL_OK);
ASSERT_EQ(adopted.Map(true), MOBILEGL_OK);
EXPECT_EQ(static_cast<const std::uint8_t*>(adopted.Data())[0], 0xA5);
::close(sockets[0]);
::close(sockets[1]);
}
// The `spawn` doorbell rides the same kind of socket as the fd channel, so it
// is covered here rather than beside the in-process one.
TEST(FdPassingTest, SocketDoorbellWakesAParkedWaiter) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
// One end each: the waiter reads its own end, the notifier writes the
// other, exactly as the two processes will.
SocketDoorbell waiterBell(sockets[0], kDoorbellWatermarkAdvanced, /*ownsFd=*/false);
SocketDoorbell notifierBell(sockets[1], kDoorbellWatermarkAdvanced, /*ownsFd=*/false);
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> ready{false};
std::atomic<bool> woke{false};
std::thread waiter([&] {
woke.store(waiterBell.Wait(
parked, [&] { return ready.load(std::memory_order_acquire); }, kDefaultSpinUs, 5000));
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ready.store(true, std::memory_order_release);
NotifyIfParked(notifierBell, parked);
waiter.join();
EXPECT_TRUE(woke.load());
EXPECT_EQ(parked.load(), 0u);
::close(sockets[0]);
::close(sockets[1]);
}
TEST(FdPassingTest, SocketDoorbellTimesOutAndRemembersAnEarlyWakeup) {
int sockets[2] = {-1, -1};
ASSERT_EQ(FdPassing::CreateSocketPair(sockets), MOBILEGL_OK);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/false);
SocketDoorbell notifierBell(sockets[1], kDoorbellRingAdvanced, /*ownsFd=*/false);
// Nothing rings: the park has to end on its deadline, not hang.
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(waiterBell.Park(30));
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
// A wakeup that arrives before anyone parks is not lost - it is sitting in
// the socket buffer, so the next Park returns at once.
notifierBell.Notify();
EXPECT_TRUE(waiterBell.Park(1000));
// ...and it was consumed, so the one after that times out again.
EXPECT_FALSE(waiterBell.Park(10));
::close(sockets[0]);
::close(sockets[1]);
}
// A doorbell whose peer has hung up must report that, not keep saying "ready".
// Park used to treat any `poll` return > 0 as a wakeup without ever looking at
// revents, and a closed peer leaves a stream socket permanently poll-ready
// with nothing to read - so Doorbell::Wait re-parked in a tight loop at full
// clock, unbounded when the caller passed kWaitForever. That is the pathology
// the bidirectional doorbell exists to prevent, arrived at from the other
// side.
TEST(FdPassingTest, SocketDoorbellStopsParkingWhenThePeerHangsUp) {
// A SOCK_STREAM pair, not FdPassing::CreateSocketPair's datagram pair:
// measured on Linux, a closed peer makes a stream end report
// POLLIN|POLLHUP with recv()==0, while a datagram end reports no readiness
// at all. The stream shape is what the spawn transport will use, and it is
// the shape that used to spin.
int sockets[2] = {-1, -1};
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), 0);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/true);
ASSERT_EQ(::close(sockets[1]), 0);
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(waiterBell.Park(kWaitForever));
EXPECT_TRUE(waiterBell.Dead());
// Latched: no second syscall storm either.
EXPECT_FALSE(waiterBell.Park(kWaitForever));
// ...and a Wait with no deadline at all gives up instead of re-parking.
std::atomic<std::uint32_t> parked{0};
EXPECT_FALSE(waiterBell.Wait(
parked, [] { return false; }, /*spinUs=*/0, kWaitForever));
EXPECT_EQ(parked.load(), 0u);
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
1000);
}
TEST(FdPassingTest, SocketDoorbellStillDeliversTheLastRingBeforeAHangup) {
int sockets[2] = {-1, -1};
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), 0);
SocketDoorbell waiterBell(sockets[0], kDoorbellRingAdvanced, /*ownsFd=*/true);
SocketDoorbell notifierBell(sockets[1], kDoorbellRingAdvanced, /*ownsFd=*/false);
// Ring, then die. Detecting the hangup must not swallow the wakeup that
// was already queued - the peer's last publish is the one a waiter is
// most likely to be blocked on.
notifierBell.Notify();
ASSERT_EQ(::close(sockets[1]), 0);
EXPECT_TRUE(waiterBell.Park(1000));
EXPECT_TRUE(waiterBell.Dead());
EXPECT_FALSE(waiterBell.Park(kWaitForever));
}
+197
View File
@@ -0,0 +1,197 @@
// MobileGL - MobileGL/MG_Test/Wire/FramingTest.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 control-channel frame codec, and specifically the two contracts the
// earlier branch's codec got wrong: a bad header must be REPORTED (it used to
// turn into a silent permanent hang) and a too-small destination buffer must
// KEEP the message (it used to fail the call and drop it, wedging the stream).
#include <MG_Remote/Transport/Framing.h>
#include <gtest/gtest.h>
#include <cstring>
#include <numeric>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
std::vector<std::uint8_t> Pattern(std::size_t size, std::uint8_t seed) {
std::vector<std::uint8_t> out(size);
for (std::size_t i = 0; i < size; ++i) {
out[i] = static_cast<std::uint8_t>(seed + i * 7u);
}
return out;
}
} // namespace
TEST(FramingTest, RoundTripsTwoMessagesFedOneByteAtATime) {
const std::vector<std::uint8_t> first = Pattern(37, 0x11);
const std::vector<std::uint8_t> second = Pattern(120, 0x83);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, first.data(), first.size()), MOBILEGL_OK);
ASSERT_EQ(AppendFrame(stream, second.data(), second.size()), MOBILEGL_OK);
EXPECT_EQ(stream.size(), 2 * kFrameHeaderSize + first.size() + second.size());
// A stream transport hands over arbitrary fragments; one byte at a time is
// the worst case and must work.
FrameReader reader;
std::vector<std::vector<std::uint8_t>> received;
for (std::uint8_t byte : stream) {
ASSERT_EQ(reader.Feed(&byte, 1), MOBILEGL_OK);
while (reader.HasMessage()) {
std::vector<std::uint8_t> message;
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
received.push_back(std::move(message));
}
}
ASSERT_EQ(received.size(), 2u);
EXPECT_EQ(received[0], first);
EXPECT_EQ(received[1], second);
EXPECT_FALSE(reader.Failed());
EXPECT_EQ(reader.BufferedBytes(), 0u);
}
TEST(FramingTest, MagicIsOnTheWireAsMGLF) {
std::vector<std::uint8_t> stream;
const std::uint8_t payload = 0xAB;
ASSERT_EQ(AppendFrame(stream, &payload, 1), MOBILEGL_OK);
ASSERT_GE(stream.size(), 4u);
EXPECT_EQ(stream[0], 'M');
EXPECT_EQ(stream[1], 'G');
EXPECT_EQ(stream[2], 'L');
EXPECT_EQ(stream[3], 'F');
}
TEST(FramingTest, EmptyPayloadRoundTrips) {
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, nullptr, 0), MOBILEGL_OK);
FrameReader reader;
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
EXPECT_EQ(reader.PendingMessageSize(), 0u);
std::vector<std::uint8_t> message{0xFF};
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
EXPECT_TRUE(message.empty());
}
TEST(FramingTest, BadMagicIsReportedAndLatchesTheReaderDead) {
std::uint8_t header[8] = {};
const std::uint32_t wrongMagic = 0xDEADBEEF;
const std::uint32_t length = 4;
std::memcpy(header + 0, &wrongMagic, sizeof(wrongMagic));
std::memcpy(header + 4, &length, sizeof(length));
FrameReader reader;
// The failure surfaces at Feed time, not as a message that never arrives.
EXPECT_EQ(reader.Feed(header, sizeof(header)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
EXPECT_TRUE(reader.Failed());
EXPECT_FALSE(reader.HasMessage());
// And it stays dead: a desynchronized stream is never re-synchronized by
// feeding it more bytes.
const std::uint8_t more[4] = {1, 2, 3, 4};
EXPECT_EQ(reader.Feed(more, sizeof(more)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
std::vector<std::uint8_t> message;
EXPECT_EQ(reader.TakeMessage(message), MOBILEGL_ERR_PROTOCOL_MISMATCH);
}
TEST(FramingTest, OversizedLengthIsRejectedBeforeAnyAllocation) {
std::uint8_t header[8] = {};
const std::uint32_t magic = kFrameMagic;
const std::uint32_t length = static_cast<std::uint32_t>(kMaxFramePayloadSize) + 1;
std::memcpy(header + 0, &magic, sizeof(magic));
std::memcpy(header + 4, &length, sizeof(length));
FrameReader reader;
EXPECT_EQ(reader.Feed(header, sizeof(header)), MOBILEGL_ERR_PROTOCOL_MISMATCH);
EXPECT_TRUE(reader.Failed());
}
TEST(FramingTest, SendRefusesAPayloadOverTheCap) {
std::vector<std::uint8_t> stream;
const std::uint8_t dummy = 0;
// The size check happens before the payload is touched, so no 64MiB
// allocation is needed to cover it.
EXPECT_EQ(AppendFrame(stream, &dummy, kMaxFramePayloadSize + 1), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_TRUE(stream.empty());
}
TEST(FramingTest, BufferTooSmallReportsTheSizeAndKeepsTheMessage) {
const std::vector<std::uint8_t> payload = Pattern(200, 0x5A);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
FrameReader reader;
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> small(8);
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(reader.TakeMessage(smallSpan, &required), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, payload.size());
// Still there. This is the whole point: the old transport dropped it here
// and the stream never recovered.
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> big(required);
std::uint64_t got = 0;
MobileGLMutableByteSpan bigSpan{big.data(), big.size()};
ASSERT_EQ(reader.TakeMessage(bigSpan, &got), MOBILEGL_OK);
EXPECT_EQ(got, payload.size());
EXPECT_EQ(big, payload);
EXPECT_FALSE(reader.HasMessage());
}
TEST(FramingTest, TakeWithNoCompleteMessageDoesNotBlockOrCorrupt) {
const std::vector<std::uint8_t> payload = Pattern(64, 0x22);
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
FrameReader reader;
// Header plus half the payload.
ASSERT_EQ(reader.Feed(stream.data(), kFrameHeaderSize + 32), MOBILEGL_OK);
EXPECT_FALSE(reader.HasMessage());
EXPECT_EQ(reader.PendingMessageSize(), 0u);
std::vector<std::uint8_t> message;
EXPECT_EQ(reader.TakeMessage(message), MOBILEGL_ERR_TIMEOUT);
ASSERT_EQ(reader.Feed(stream.data() + kFrameHeaderSize + 32,
stream.size() - kFrameHeaderSize - 32),
MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
EXPECT_EQ(message, payload);
}
TEST(FramingTest, ManyMessagesCompactTheBufferInsteadOfGrowing) {
// Drives the reader past its compaction threshold so the "consumed bytes
// are reclaimed" path is actually taken.
const std::vector<std::uint8_t> payload = Pattern(1024, 0x07);
FrameReader reader;
for (int i = 0; i < 300; ++i) {
std::vector<std::uint8_t> stream;
ASSERT_EQ(AppendFrame(stream, payload.data(), payload.size()), MOBILEGL_OK);
ASSERT_EQ(reader.Feed(stream.data(), stream.size()), MOBILEGL_OK);
ASSERT_TRUE(reader.HasMessage());
std::vector<std::uint8_t> message;
ASSERT_EQ(reader.TakeMessage(message), MOBILEGL_OK);
ASSERT_EQ(message, payload);
}
EXPECT_EQ(reader.BufferedBytes(), 0u);
}
@@ -0,0 +1,398 @@
// MobileGL - MobileGL/MG_Test/Wire/InProcessTransportTest.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 `inproc` transport: message queues in both directions, the
// buffer-too-small contract, shutdown semantics, descriptor hand-off, and the
// condvar doorbells the rings park on.
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/InProcessTransport.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
using namespace MobileGL::MG_Remote::Transport;
namespace {
MobileGLByteSpan Span(const std::string& text) {
return MobileGLByteSpan{text.data(), text.size()};
}
std::string Receive(ITransport& transport, std::uint32_t timeoutMs = 1000) {
std::vector<std::uint8_t> buffer(4096);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
const MobileGLResult result = transport.ReceiveFrame(span, &size, timeoutMs);
if (result != MOBILEGL_OK) {
return std::string("<result=") + std::to_string(static_cast<int>(result)) + ">";
}
return std::string(reinterpret_cast<const char*>(buffer.data()),
static_cast<std::size_t>(size));
}
} // namespace
TEST(InProcessTransportTest, CarriesFramesInBothDirections) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
ASSERT_TRUE(client && server);
EXPECT_EQ(client->Role(), TransportRole::InProcess);
const std::string hello = "Hello{abiMajor=1}";
const std::string welcome = "Welcome{serverPid=42}";
ASSERT_EQ(client->SendFrame(Span(hello)), MOBILEGL_OK);
EXPECT_EQ(server->PeekFrameSize(), hello.size());
// A message goes to the PEER's inbox, never back to the sender.
EXPECT_EQ(client->PeekFrameSize(), 0u);
EXPECT_EQ(Receive(*server), hello);
ASSERT_EQ(server->SendFrame(Span(welcome)), MOBILEGL_OK);
EXPECT_EQ(Receive(*client), welcome);
}
TEST(InProcessTransportTest, PreservesOrder) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
for (int i = 0; i < 64; ++i) {
const std::string message = "msg-" + std::to_string(i);
ASSERT_EQ(client->SendFrame(Span(message)), MOBILEGL_OK);
}
for (int i = 0; i < 64; ++i) {
EXPECT_EQ(Receive(*server), "msg-" + std::to_string(i));
}
}
TEST(InProcessTransportTest, BufferTooSmallKeepsTheMessage) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
const std::string message(300, 'x');
ASSERT_EQ(client->SendFrame(Span(message)), MOBILEGL_OK);
std::vector<std::uint8_t> small(16);
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(server->ReceiveFrame(smallSpan, &required, 0), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, message.size());
// Still queued - the caller just retries with the size it was told.
EXPECT_EQ(server->PeekFrameSize(), message.size());
EXPECT_EQ(Receive(*server), message);
}
TEST(InProcessTransportTest, PollAndTimeoutDoNotBlockForever) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, 0), MOBILEGL_ERR_TIMEOUT);
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(server->ReceiveFrame(span, &size, 30), MOBILEGL_ERR_TIMEOUT);
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
}
TEST(InProcessTransportTest, ShutdownDrainsBeforeItCloses) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
const std::string last = "Fatal{code=DeviceLost}";
ASSERT_EQ(client->SendFrame(Span(last)), MOBILEGL_OK);
client->Shutdown();
// A peer that shuts down right after sending must not lose its last
// message - that is usually the one that says why it is going away.
EXPECT_EQ(Receive(*server), last);
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, 100), MOBILEGL_ERR_TRANSPORT_CLOSED);
EXPECT_EQ(server->SendFrame(Span(last)), MOBILEGL_ERR_TRANSPORT_CLOSED);
}
TEST(InProcessTransportTest, BlockedReceiverWakesOnSendAndOnShutdown) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<bool> got{false};
std::thread reader([&] {
got.store(Receive(*server, kWaitForever) == "wake");
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ASSERT_EQ(client->SendFrame(Span(std::string("wake"))), MOBILEGL_OK);
reader.join();
EXPECT_TRUE(got.load());
std::thread closer([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
client->Shutdown();
});
std::vector<std::uint8_t> buffer(64);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
EXPECT_EQ(server->ReceiveFrame(span, &size, kWaitForever), MOBILEGL_ERR_TRANSPORT_CLOSED);
closer.join();
}
TEST(InProcessTransportTest, RefusesAPayloadOverTheFrameCap) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// Not allocated: the cap is checked before the bytes are touched. Keeping
// the same limit as the socket transports means nothing passes CI here and
// then fails after the switch to `spawn`.
const std::uint8_t dummy = 0;
MobileGLByteSpan huge{&dummy, 64ull * 1024 * 1024 + 1};
EXPECT_EQ(client->SendFrame(huge), MOBILEGL_ERR_INVALID_ARGUMENT);
}
#if !defined(_WIN32)
TEST(InProcessTransportTest, HandsOverADescriptorAndItsSideband) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
int pipeFds[2] = {-1, -1};
ASSERT_EQ(::pipe(pipeFds), 0);
const std::string sideband = "SegmentRef{id=1,kind=Cmd}";
ASSERT_EQ(client->ShareFd(pipeFds[0], Span(sideband)), MOBILEGL_OK);
// Symmetric with the SCM_RIGHTS path: a short sideband buffer is refused
// before anything is consumed, so the descriptor is never dropped.
std::vector<std::uint8_t> small(8);
int fd = -1;
std::uint64_t required = 0;
MobileGLMutableByteSpan smallSpan{small.data(), small.size()};
EXPECT_EQ(server->ReceiveFd(&fd, smallSpan, &required, 0), MOBILEGL_ERR_BUFFER_TOO_SMALL);
EXPECT_EQ(required, FdPassing::kMaxSidebandBytes);
EXPECT_EQ(fd, -1);
std::vector<std::uint8_t> big(FdPassing::kMaxSidebandBytes);
std::uint64_t sidebandSize = 0;
MobileGLMutableByteSpan bigSpan{big.data(), big.size()};
ASSERT_EQ(server->ReceiveFd(&fd, bigSpan, &sidebandSize, 100), MOBILEGL_OK);
ASSERT_GE(fd, 0);
EXPECT_EQ(std::string(reinterpret_cast<const char*>(big.data()),
static_cast<std::size_t>(sidebandSize)),
sideband);
// Same open file description, independent descriptor.
const char payload[] = "bytes";
ASSERT_EQ(::write(pipeFds[1], payload, sizeof(payload)), static_cast<ssize_t>(sizeof(payload)));
char readBack[sizeof(payload)] = {};
ASSERT_EQ(::read(fd, readBack, sizeof(readBack)), static_cast<ssize_t>(sizeof(payload)));
EXPECT_STREQ(readBack, payload);
::close(fd);
::close(pipeFds[0]);
::close(pipeFds[1]);
}
TEST(InProcessTransportTest, AFrameWakeupIsNotEatenByAWaiterOnDescriptors) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// Two readers on the SAME endpoint, blocked on two different predicates.
// With one condition_variable per direction and notify_one, the SendFrame
// below could be delivered to the descriptor waiter, which re-tests its
// own predicate and goes back to sleep - and the message then sits
// undelivered until some unrelated later event. ITransport narrows the
// contract to one dedicated reader thread, but that is a comment, and the
// first caller that splits its reader should not have to discover this.
std::atomic<bool> fdWaiterStarted{false};
std::thread fdWaiter([&] {
std::vector<std::uint8_t> sideband(FdPassing::kMaxSidebandBytes);
MobileGLMutableByteSpan span{sideband.data(), sideband.size()};
int fd = -1;
std::uint64_t size = 0;
fdWaiterStarted.store(true);
// Never offered a descriptor: this one ends on the Shutdown below.
EXPECT_EQ(client->ReceiveFd(&fd, span, &size, kWaitForever),
MOBILEGL_ERR_TRANSPORT_CLOSED);
EXPECT_EQ(fd, -1);
});
while (!fdWaiterStarted.load()) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::atomic<bool> frameWaiterStarted{false};
std::string got;
std::thread frameWaiter([&] {
frameWaiterStarted.store(true);
got = Receive(*client, 4000);
});
while (!frameWaiterStarted.load()) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string message = "wake the right waiter";
const auto start = std::chrono::steady_clock::now();
ASSERT_EQ(server->SendFrame(Span(message)), MOBILEGL_OK);
frameWaiter.join();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count();
EXPECT_EQ(got, message);
// Not "eventually, when the receive timed out and re-checked".
EXPECT_LT(elapsedMs, 2000);
client->Shutdown();
fdWaiter.join();
}
#endif
TEST(InProcessTransportTest, DoorbellWakesAParkedWaiter) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
// producerParked / consumerParked live in RingControl; here a standalone
// flag stands in for one.
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> ready{false};
std::atomic<bool> woke{false};
std::thread waiter([&] {
woke.store(client->SelfDoorbell().Wait(
parked, [&] { return ready.load(std::memory_order_acquire); }, kDefaultSpinUs, 5000));
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ready.store(true, std::memory_order_release);
// The peer only rings when the waiter says it parked, which is what makes
// the common (spin-only) case free.
NotifyIfParked(server->PeerDoorbell(), parked);
waiter.join();
EXPECT_TRUE(woke.load());
EXPECT_EQ(parked.load(), 0u);
}
TEST(InProcessTransportTest, DoorbellReturnsImmediatelyWhenAlreadyReady) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<std::uint32_t> parked{0};
// No notification is sent at all: a condition that is already true must
// never park, or the lost-wakeup window would be reachable.
EXPECT_TRUE(client->SelfDoorbell().Wait(
parked, [] { return true; }, kDefaultSpinUs, 0));
EXPECT_EQ(parked.load(), 0u);
}
TEST(InProcessTransportTest, DoorbellTimesOutWhenNothingHappens) {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
InProcessTransport::CreatePair(client, server);
std::atomic<std::uint32_t> parked{0};
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(client->SelfDoorbell().Wait(
parked, [] { return false; }, kDefaultSpinUs, 30));
EXPECT_GE(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start)
.count(),
20);
EXPECT_EQ(parked.load(), 0u);
}
// The design's own steady state: the consumer spun, set consumerParked and blocked
// with NO deadline. Shutdown has to bring that thread back, and a single Notify
// cannot - Doorbell::Wait consumes it, re-tests a condition that is still false,
// and with kWaitForever parks again. Only a bell that reports Dead() ends the
// loop, which is what InProcessChannel::Close rings now.
//
// A regression here is a HANG, so the join is bounded: the waiter owns its state
// through a shared_ptr and is detached on timeout, and the test fails red after
// five seconds instead of wedging the CI job.
TEST(InProcessTransportTest, ShutdownUnparksAWaiterWithNoDeadline) {
struct Shared {
std::unique_ptr<InProcessTransport> client;
std::unique_ptr<InProcessTransport> server;
std::atomic<std::uint32_t> parked{0};
std::atomic<bool> returned{false};
std::atomic<bool> woke{true};
};
auto shared = std::make_shared<Shared>();
InProcessTransport::CreatePair(shared->client, shared->server);
std::thread waiter([shared] {
shared->woke.store(shared->server->SelfDoorbell().Wait(
shared->parked, [] { return false; }, kDefaultSpinUs, kWaitForever));
shared->returned.store(true, std::memory_order_release);
});
// Past the spin and announced as parked; a little longer and it is inside
// Park. (A Kill that lands before the Park is handled too - Park returns at
// once on a dead bell - but the case under test is the parked one.)
while (shared->parked.load() == 0) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ASSERT_FALSE(shared->returned.load());
shared->client->Shutdown();
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!shared->returned.load(std::memory_order_acquire) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (!shared->returned.load(std::memory_order_acquire)) {
waiter.detach();
FAIL() << "Shutdown did not unpark a waiter with no deadline within 5 s: the inproc doorbell "
"has no death state, so the waiter consumed the ring and parked again";
}
waiter.join();
// No wakeup was consumed - the bell died - and the park flag is clear.
EXPECT_FALSE(shared->woke.load());
EXPECT_TRUE(shared->server->SelfDoorbell().Dead());
EXPECT_TRUE(shared->client->SelfDoorbell().Dead());
EXPECT_EQ(shared->parked.load(), 0u);
// Sticky: a wait with no deadline that ARRIVES after the Shutdown returns at
// once rather than parking, so a late thread cannot hang either.
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(shared->server->SelfDoorbell().Wait(
shared->parked, [] { return false; }, 0, kWaitForever));
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start)
.count(),
1000);
EXPECT_EQ(shared->parked.load(), 0u);
}
+149
View File
@@ -0,0 +1,149 @@
// MobileGL - MobileGL/MG_Test/Wire/ProtocolSmokeTest.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 committed control-plane schema: encode/decode a handshake through the
// generated header, and pin the union tag values, which are wire numbers that
// may only ever be appended to.
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Transport/Framing.h>
#include <MG_Remote/Transport/InProcessTransport.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
using namespace MobileGL::Wire;
namespace Transport = MobileGL::MG_Remote::Transport;
namespace {
std::vector<std::uint8_t> BuildHello() {
::flatbuffers::FlatBufferBuilder builder(1024);
const std::vector<std::uint8_t> config{1, 2, 3, 4};
auto hello = CreateHelloDirect(builder, MOBILEGL_PROTOCOL_ABI_MAJOR,
MOBILEGL_PROTOCOL_ABI_MINOR, "mobilegl-test-build",
/*backendType=*/2, /*pid=*/4242, &config);
auto envelope = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
FinishCtrlEnvelopeBuffer(builder, envelope);
const std::uint8_t* begin = builder.GetBufferPointer();
return std::vector<std::uint8_t>(begin, begin + builder.GetSize());
}
} // namespace
TEST(ProtocolSmokeTest, HelloRoundTrips) {
const std::vector<std::uint8_t> buffer = BuildHello();
// Every message from the peer is verified before a single field is read:
// the control plane is parsed from another process's memory.
::flatbuffers::Verifier verifier(buffer.data(), buffer.size());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
ASSERT_TRUE(CtrlEnvelopeBufferHasIdentifier(buffer.data()));
const CtrlEnvelope* envelope = GetCtrlEnvelope(buffer.data());
ASSERT_NE(envelope, nullptr);
ASSERT_EQ(envelope->msg_type(), CtrlMsg::Hello);
const Hello* hello = envelope->msg_as_Hello();
ASSERT_NE(hello, nullptr);
EXPECT_EQ(hello->abiMajor(), static_cast<std::uint32_t>(MOBILEGL_PROTOCOL_ABI_MAJOR));
EXPECT_EQ(hello->abiMinor(), static_cast<std::uint32_t>(MOBILEGL_PROTOCOL_ABI_MINOR));
ASSERT_NE(hello->buildFingerprint(), nullptr);
EXPECT_EQ(hello->buildFingerprint()->str(), "mobilegl-test-build");
EXPECT_EQ(hello->backendType(), 2u);
EXPECT_EQ(hello->pid(), 4242u);
ASSERT_NE(hello->configBlob(), nullptr);
ASSERT_EQ(hello->configBlob()->size(), 4u);
EXPECT_EQ(hello->configBlob()->Get(3), 4u);
// A message of the wrong kind reads back as null rather than as garbage.
EXPECT_EQ(envelope->msg_as_Welcome(), nullptr);
}
TEST(ProtocolSmokeTest, WelcomeCarriesTheFourSegmentAnnouncements) {
::flatbuffers::FlatBufferBuilder builder(1024);
auto cmd = CreateSegmentRefDirect(builder, 1, SegmentKind::Cmd, 8ull * 1024 * 1024, "cmd");
auto stage = CreateSegmentRefDirect(builder, 2, SegmentKind::Stage, 32ull * 1024 * 1024, "stage");
auto reply = CreateSegmentRefDirect(builder, 3, SegmentKind::Reply, 8ull * 1024 * 1024, "reply");
auto event = CreateSegmentRefDirect(builder, 4, SegmentKind::Event, 256ull * 1024, "event");
auto welcome = CreateWelcome(builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
/*serverPid=*/99, cmd, stage, reply, event);
auto envelope = CreateCtrlEnvelope(builder, CtrlMsg::Welcome, welcome.Union());
FinishCtrlEnvelopeBuffer(builder, envelope);
::flatbuffers::Verifier verifier(builder.GetBufferPointer(), builder.GetSize());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const Welcome* parsed = GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_Welcome();
ASSERT_NE(parsed, nullptr);
EXPECT_EQ(parsed->serverPid(), 99u);
ASSERT_NE(parsed->cmdRing(), nullptr);
EXPECT_EQ(parsed->cmdRing()->kind(), SegmentKind::Cmd);
EXPECT_EQ(parsed->cmdRing()->sizeBytes(), 8ull * 1024 * 1024);
ASSERT_NE(parsed->stageRing(), nullptr);
EXPECT_EQ(parsed->stageRing()->sizeBytes(), 32ull * 1024 * 1024);
ASSERT_NE(parsed->eventRing(), nullptr);
EXPECT_EQ(parsed->eventRing()->sizeBytes(), 256ull * 1024);
}
TEST(ProtocolSmokeTest, UnionTagsAreFrozenWireValues) {
// Appending to CtrlMsg is a compatible change; reordering it is not. If
// this test has to be edited, the schema change was a wire break.
EXPECT_EQ(static_cast<int>(CtrlMsg::NONE), 0);
EXPECT_EQ(static_cast<int>(CtrlMsg::Hello), 1);
EXPECT_EQ(static_cast<int>(CtrlMsg::Welcome), 2);
EXPECT_EQ(static_cast<int>(CtrlMsg::CapsSnapshot), 3);
EXPECT_EQ(static_cast<int>(CtrlMsg::SurfaceOp), 4);
EXPECT_EQ(static_cast<int>(CtrlMsg::SurfaceReply), 5);
EXPECT_EQ(static_cast<int>(CtrlMsg::ResyncRequest), 6);
EXPECT_EQ(static_cast<int>(CtrlMsg::ResyncDone), 7);
EXPECT_EQ(static_cast<int>(CtrlMsg::AuxRequest), 8);
EXPECT_EQ(static_cast<int>(CtrlMsg::Fatal), 9);
EXPECT_EQ(static_cast<int>(CtrlMsg::LogLine), 10);
EXPECT_EQ(static_cast<int>(SegmentKind::Cmd), 1);
EXPECT_EQ(static_cast<int>(SegmentKind::Adopt), 6);
EXPECT_EQ(static_cast<int>(LogLevel::Error), 3);
EXPECT_EQ(static_cast<int>(FatalCode::ProtocolCorruption), 1);
}
TEST(ProtocolSmokeTest, TruncatedMessageFailsVerificationInsteadOfReadingGarbage) {
std::vector<std::uint8_t> buffer = BuildHello();
ASSERT_GT(buffer.size(), 8u);
buffer.resize(buffer.size() / 2);
::flatbuffers::Verifier verifier(buffer.data(), buffer.size());
EXPECT_FALSE(VerifyCtrlEnvelopeBuffer(verifier));
}
TEST(ProtocolSmokeTest, TravelsAcrossTheTransportUnchanged) {
std::unique_ptr<Transport::InProcessTransport> client;
std::unique_ptr<Transport::InProcessTransport> server;
Transport::InProcessTransport::CreatePair(client, server);
const std::vector<std::uint8_t> sent = BuildHello();
ASSERT_EQ(client->SendFrame(MobileGLByteSpan{sent.data(), sent.size()}), MOBILEGL_OK);
const std::uint64_t pending = server->PeekFrameSize();
ASSERT_EQ(pending, sent.size());
std::vector<std::uint8_t> received(pending);
std::uint64_t size = 0;
MobileGLMutableByteSpan span{received.data(), received.size()};
ASSERT_EQ(server->ReceiveFrame(span, &size, 1000), MOBILEGL_OK);
ASSERT_EQ(size, sent.size());
::flatbuffers::Verifier verifier(received.data(), received.size());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const Hello* hello = GetCtrlEnvelope(received.data())->msg_as_Hello();
ASSERT_NE(hello, nullptr);
EXPECT_EQ(hello->pid(), 4242u);
}
+519
View File
@@ -0,0 +1,519 @@
// MobileGL - MobileGL/MG_Test/Wire/RingTest.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 SEG_CMD/SEG_STAGE SPSC ring: layout of the shared control page, cursor
// invariants, wrap-around, backpressure, the generation bump after a hard
// drain, and a real two-thread producer/consumer run.
#include <MG_Remote/Transport/Doorbell.h>
#include <MG_Remote/Transport/Ring.h>
#include <gtest/gtest.h>
#include <atomic>
#include <cstring>
#include <thread>
#include <vector>
using namespace MobileGL::MG_Remote::Transport;
namespace {
// A ring plus its control page, sized like a small SEG_CMD.
class RingFixture {
public:
explicit RingFixture(std::uint64_t capacity, RingCursorSet cursors = RingCursorSet::Cmd)
: m_bytes(static_cast<std::size_t>(capacity)), m_capacity(capacity) {
InitRingControl(m_control);
m_producer = RingProducer(&m_control, m_bytes.data(), capacity, cursors);
m_consumer = RingConsumer(&m_control, m_bytes.data(), capacity, cursors);
m_cursors = cursors;
}
RingControl& Control() { return m_control; }
RingProducer& Producer() { return m_producer; }
RingConsumer& Consumer() { return m_consumer; }
std::uint64_t Capacity() const { return m_capacity; }
bool Invariants() const { return RingCursorsValid(m_control, m_cursors, m_capacity); }
// Writes one record whose payload is `size` bytes of a recognisable
// pattern seeded by `seed`.
bool WriteRecord(std::uint16_t kind, std::uint64_t size, std::uint8_t seed) {
void* payload = m_producer.Reserve(kind, kRecNone, size);
if (payload == nullptr) {
return false;
}
auto* bytes = static_cast<std::uint8_t*>(payload);
for (std::uint64_t i = 0; i < size; ++i) {
bytes[i] = static_cast<std::uint8_t>(seed + i);
}
m_producer.Publish();
return true;
}
static bool CheckPattern(const RingRecordView& view, std::uint64_t size, std::uint8_t seed) {
const auto* bytes = static_cast<const std::uint8_t*>(view.payload);
for (std::uint64_t i = 0; i < size; ++i) {
if (bytes[i] != static_cast<std::uint8_t>(seed + i)) {
return false;
}
}
return true;
}
private:
alignas(4096) RingControl m_control{};
std::vector<std::uint8_t> m_bytes;
RingProducer m_producer;
RingConsumer m_consumer;
std::uint64_t m_capacity;
RingCursorSet m_cursors = RingCursorSet::Cmd;
};
} // namespace
TEST(RingTest, ControlPageLayoutIsTheSharedContract) {
// The page is mapped by two processes; its size and alignment are wire
// contract, not an implementation detail.
EXPECT_EQ(sizeof(RingControl), 4096u);
EXPECT_EQ(alignof(RingControl), 4096u);
EXPECT_EQ(sizeof(RingRecordHeader), 8u);
alignas(4096) RingControl control{};
InitRingControl(control);
// Zero is reserved for "uninitialized" on both generations.
EXPECT_EQ(control.serverEpoch.load(), 1u);
EXPECT_EQ(control.ringGeneration.load(), 1u);
EXPECT_EQ(control.cmdHead.load(), 0u);
EXPECT_EQ(control.stageHead.load(), 0u);
EXPECT_EQ(control.consumerParked.load(), 0u);
EXPECT_EQ(control.producerParked.load(), 0u);
EXPECT_EQ(control.eventRingFull.load(), 0u);
EXPECT_EQ(control.eventDropped.load(), 0u);
// Each contended group on its own cache line.
const auto offset = [&control](const void* member) {
return reinterpret_cast<const std::uint8_t*>(member) -
reinterpret_cast<const std::uint8_t*>(&control);
};
EXPECT_EQ(offset(&control.cmdHead) % 64, 0);
EXPECT_EQ(offset(&control.cmdAppliedTail) % 64, 0);
EXPECT_EQ(offset(&control.stageHead) % 64, 0);
EXPECT_EQ(offset(&control.stageAppliedTail) % 64, 0);
EXPECT_EQ(offset(&control.appliedSeq) % 64, 0);
EXPECT_EQ(offset(&control.serverEpoch) % 64, 0);
// cmdHead and cmdAppliedTail are written by different processes: they must
// not share a line.
EXPECT_NE(offset(&control.cmdHead) / 64, offset(&control.cmdAppliedTail) / 64);
}
TEST(RingTest, RejectsANonPowerOfTwoCapacity) {
alignas(4096) RingControl control{};
InitRingControl(control);
std::vector<std::uint8_t> bytes(1000);
RingProducer producer(&control, bytes.data(), 1000, RingCursorSet::Cmd);
EXPECT_FALSE(producer.Valid());
EXPECT_EQ(producer.Reserve(1, kRecNone, 8), nullptr);
}
TEST(RingTest, RejectsACapacityTheRecordHeaderCannotDescribe) {
alignas(4096) RingControl control{};
InitRingControl(control);
// 4 GiB is a legal power of two, but RingRecordHeader::size is 32 bits and
// both a record's size and a wrap filler's size are bounded only by the
// capacity: they would be truncated on the way in and then bounds-checked
// in their truncated form on the way out. Nothing is mapped here - the
// constructor rejects before it ever touches the base pointer.
std::uint8_t dummy = 0;
constexpr std::uint64_t kFourGiB = 4ull * 1024 * 1024 * 1024;
EXPECT_GT(kFourGiB, kMaxRingCapacity);
RingProducer producer(&control, &dummy, kFourGiB, RingCursorSet::Cmd);
EXPECT_FALSE(producer.Valid());
RingConsumer consumer(&control, &dummy, kFourGiB, RingCursorSet::Cmd);
EXPECT_FALSE(consumer.Valid());
// The largest ring the header CAN describe stays accepted.
RingProducer biggest(&control, &dummy, 1ull << 31, RingCursorSet::Cmd);
EXPECT_TRUE(biggest.Valid());
}
TEST(RingTest, RoundTripsRecordsInOrder) {
RingFixture ring(4096);
ASSERT_TRUE(ring.WriteRecord(1, 16, 0x10));
ASSERT_TRUE(ring.WriteRecord(2, 24, 0x20));
EXPECT_TRUE(ring.Invariants());
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt));
EXPECT_FALSE(corrupt);
EXPECT_EQ(view.kind, 1u);
EXPECT_EQ(view.payloadSize, 16u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 16, 0x10));
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt));
EXPECT_EQ(view.kind, 2u);
EXPECT_EQ(view.payloadSize, 24u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 24, 0x20));
EXPECT_FALSE(ring.Consumer().Pop(view, &corrupt));
ring.Consumer().PublishRetired();
EXPECT_TRUE(ring.Invariants());
EXPECT_EQ(ring.Control().cmdAppliedTail.load(), ring.Control().cmdHead.load());
EXPECT_EQ(ring.Control().cmdRetiredTail.load(), ring.Control().cmdHead.load());
}
TEST(RingTest, PayloadIsPaddedToTheRecordAlignment) {
RingFixture ring(4096);
ASSERT_TRUE(ring.WriteRecord(7, 3, 0x77));
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
// 8 (header) + 3 rounded up to 16 -> 8 bytes of payload space.
EXPECT_EQ(view.payloadSize, 8u);
EXPECT_TRUE(RingFixture::CheckPattern(view, 3, 0x77));
}
TEST(RingTest, WrapsWithoutSplittingARecord) {
// Small ring, records that do not divide it evenly, so the wrap boundary
// lands mid-record and the pad path is exercised many times.
RingFixture ring(256);
std::uint8_t seed = 0;
for (int i = 0; i < 200; ++i) {
const std::uint64_t size = 24 + (i % 5) * 8;
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(1 + (i % 3)), size, seed))
<< "record " << i;
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt)) << "record " << i;
ASSERT_FALSE(corrupt);
EXPECT_EQ(view.kind, static_cast<std::uint16_t>(1 + (i % 3)));
// Contiguity: the payload never straddles the end of the mapping.
EXPECT_TRUE(RingFixture::CheckPattern(view, size, seed)) << "record " << i;
ring.Consumer().PublishRetired();
ASSERT_TRUE(ring.Invariants());
seed = static_cast<std::uint8_t>(seed + 13);
}
// Cursors are monotonic byte counts, so they are far past the capacity.
EXPECT_GT(ring.Control().cmdHead.load(), ring.Capacity());
}
TEST(RingTest, FullRingRefusesAndRecoversWhenTheConsumerRetires) {
RingFixture ring(256);
int written = 0;
while (ring.WriteRecord(1, 24, static_cast<std::uint8_t>(written))) {
++written;
ASSERT_LT(written, 100);
}
EXPECT_GT(written, 0);
// Backpressure, not corruption.
EXPECT_TRUE(ring.Invariants());
EXPECT_LT(ring.Producer().FreeBytes(), 32u);
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
// Applied alone does not free a slot that may still be borrowed by the GPU
// timeline: reclaim follows the retired cursor.
ring.Consumer().PublishApplied();
EXPECT_EQ(ring.Producer().FreeBytes(), 0u);
ring.Consumer().PublishRetired();
EXPECT_GT(ring.Producer().FreeBytes(), 0u);
EXPECT_TRUE(ring.WriteRecord(1, 24, 0xEE));
}
TEST(RingTest, RecordLargerThanTheRingIsRefused) {
RingFixture ring(256);
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 4096), nullptr);
EXPECT_TRUE(ring.Invariants());
}
TEST(RingTest, RecordLargerThanHalfTheRingIsRefused) {
// 256-byte ring: the bound is 128 bytes of header + payload.
RingFixture ring(256);
EXPECT_EQ(ring.Producer().MaxRecordBytes(), 128u);
// 8 + 240 = 248: fits the whole ring, does not fit half of it.
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 240), nullptr);
// 8 + 128 = 136: one step over the bound, refused the same way...
EXPECT_EQ(ring.Producer().Reserve(1, kRecNone, 128), nullptr);
// ...and 8 + 120 = 128, exactly the bound, is accepted.
EXPECT_NE(ring.Producer().Reserve(1, kRecNone, 120), nullptr);
EXPECT_TRUE(ring.Invariants());
}
// The scenario that motivated the bound, as the negative control. Whether a record
// can be placed must not depend on where the head happens to be. With "total <=
// capacity" as the only rule, a 248-byte record is accepted at head offset 0 of an
// empty 256-byte ring and refused forever at head offset 16 of the same empty
// ring - it would need a 240-byte wrap pad plus itself, 488 bytes - while
// FreeBytes() reports 256 the whole time, so a producer waiting for FreeBytes()
// >= 248 spins on nullptr with nothing logged. Both answers have to be the same
// refusal, and it has to be the loud one.
TEST(RingTest, RecordPlaceabilityDoesNotDependOnTheHeadOffset) {
RingFixture atOffsetZero(256);
void* atZero = atOffsetZero.Producer().Reserve(1, kRecNone, 240);
RingFixture atOffsetSixteen(256);
ASSERT_TRUE(atOffsetSixteen.WriteRecord(1, 8, 0x01)); // 8 + 8 = 16 bytes
RingRecordView view{};
ASSERT_TRUE(atOffsetSixteen.Consumer().Pop(view));
atOffsetSixteen.Consumer().PublishRetired();
ASSERT_EQ(atOffsetSixteen.Producer().LocalHead(), 16u);
ASSERT_EQ(atOffsetSixteen.Producer().FreeBytes(), 256u);
void* atSixteen = atOffsetSixteen.Producer().Reserve(1, kRecNone, 240);
EXPECT_EQ(atSixteen, nullptr);
EXPECT_EQ(atZero, nullptr)
<< "a 248-byte record was accepted at head offset 0 but is unplaceable at head offset 16 of "
"the same empty ring: the emitter cannot tell a refusal it must chunk from a full ring it "
"must wait on";
EXPECT_TRUE(atOffsetZero.Invariants());
EXPECT_TRUE(atOffsetSixteen.Invariants());
}
// The positive half of the same argument: a record of exactly half the capacity is
// placeable at EVERY head offset of an empty ring, because the wrap pad in front of
// it costs at most total-8 bytes. Walk the head to each 8-byte offset with bare
// header records and reserve the maximal record there.
TEST(RingTest, HalfCapacityRecordFitsAtEveryHeadOffset) {
RingFixture ring(256);
const std::uint64_t mask = ring.Capacity() - 1;
const std::uint64_t maximal = ring.Producer().MaxRecordBytes() - sizeof(RingRecordHeader); // 120
for (std::uint64_t target = 0; target < ring.Capacity(); target += 8) {
// A bare header never straddles the boundary, so no pad appears on the way.
while ((ring.Producer().LocalHead() & mask) != target) {
ASSERT_TRUE(ring.WriteRecord(1, 0, 0));
RingRecordView filler{};
ASSERT_TRUE(ring.Consumer().Pop(filler));
ring.Consumer().PublishRetired();
}
ASSERT_EQ(ring.Producer().FreeBytes(), ring.Capacity()) << "head offset " << target;
void* payload = ring.Producer().Reserve(2, kRecNone, maximal);
ASSERT_NE(payload, nullptr) << "head offset " << target;
ring.Producer().Publish();
RingRecordView view{};
bool corrupt = false;
ASSERT_TRUE(ring.Consumer().Pop(view, &corrupt)) << "head offset " << target;
ASSERT_FALSE(corrupt);
EXPECT_EQ(view.kind, 2u);
EXPECT_EQ(view.payloadSize, maximal);
ring.Consumer().PublishRetired();
ASSERT_TRUE(ring.Invariants()) << "head offset " << target;
}
}
TEST(RingTest, RejectsARingTooSmallForTheSmallestRecord) {
alignas(4096) RingControl control{};
InitRingControl(control);
std::uint8_t bytes[16] = {};
// One header's worth of ring can carry nothing once a record may be at most
// half the ring; two headers' worth carries a bare header.
RingProducer tooSmall(&control, bytes, sizeof(RingRecordHeader), RingCursorSet::Cmd);
EXPECT_FALSE(tooSmall.Valid());
RingProducer smallest(&control, bytes, kMinRingCapacity, RingCursorSet::Cmd);
ASSERT_TRUE(smallest.Valid());
EXPECT_EQ(smallest.MaxRecordBytes(), sizeof(RingRecordHeader));
EXPECT_NE(smallest.Reserve(1, kRecNone, 0), nullptr);
}
TEST(RingTest, HardDrainBumpsTheGenerationOnlyWhenQuiesced) {
RingFixture ring(256);
ASSERT_TRUE(ring.WriteRecord(1, 32, 0x01));
const std::uint32_t before = ring.Control().ringGeneration.load();
// Records still in flight: the drain is refused and nothing changes.
EXPECT_EQ(HardDrainRing(ring.Control(), RingCursorSet::Cmd), MOBILEGL_ERR_INVALID_ARGUMENT);
EXPECT_EQ(ring.Control().ringGeneration.load(), before);
RingRecordView view{};
ASSERT_TRUE(ring.Consumer().Pop(view));
ring.Consumer().PublishRetired();
EXPECT_EQ(HardDrainRing(ring.Control(), RingCursorSet::Cmd), MOBILEGL_OK);
EXPECT_EQ(ring.Control().ringGeneration.load(), before + 1);
// Cursors stay monotonic across the drain - only the generation moves.
EXPECT_EQ(ring.Control().cmdHead.load(), ring.Control().cmdAppliedTail.load());
EXPECT_GT(ring.Control().cmdHead.load(), 0u);
}
TEST(RingTest, CorruptHeaderIsRefusedRatherThanDispatched) {
// SEG_CMD is written by the peer process, so a compile-time size assert on
// the record catalogue proves nothing about what is actually in the
// mapping. Hand-build a ring whose first header is impossible (a size that
// is not a multiple of 8) and check the consumer refuses it instead of
// dispatching into undefined behaviour.
alignas(4096) RingControl control{};
InitRingControl(control);
std::vector<std::uint8_t> bytes(256, 0);
RingRecordHeader bad{};
bad.kind = 5;
bad.flags = kRecNone;
bad.size = 13; // not 8-aligned
std::memcpy(bytes.data(), &bad, sizeof(bad));
control.cmdHead.store(64, std::memory_order_release);
RingConsumer consumer(&control, bytes.data(), bytes.size(), RingCursorSet::Cmd);
RingRecordView view{};
bool corrupt = false;
EXPECT_FALSE(consumer.Pop(view, &corrupt));
EXPECT_TRUE(corrupt);
// A record claiming more bytes than the producer has published is the same
// class of violation and is refused the same way.
bad.size = 128;
std::memcpy(bytes.data(), &bad, sizeof(bad));
RingConsumer second(&control, bytes.data(), bytes.size(), RingCursorSet::Cmd);
corrupt = false;
EXPECT_FALSE(second.Pop(view, &corrupt));
EXPECT_TRUE(corrupt);
}
TEST(RingTest, SpscProducerConsumerThreadsAgreeOnEveryRecord) {
constexpr int kRecords = 20000;
RingFixture ring(4096);
std::atomic<bool> failed{false};
std::atomic<int> consumed{0};
std::thread consumer([&] {
int next = 0;
while (next < kRecords) {
RingRecordView view{};
bool corrupt = false;
if (!ring.Consumer().Pop(view, &corrupt)) {
if (corrupt) {
failed.store(true);
return;
}
std::this_thread::yield();
continue;
}
const std::uint32_t expectedKind = static_cast<std::uint16_t>(1 + (next % 7));
if (view.kind != expectedKind || view.payloadSize < sizeof(std::uint32_t)) {
failed.store(true);
return;
}
std::uint32_t value = 0;
std::memcpy(&value, view.payload, sizeof(value));
if (value != static_cast<std::uint32_t>(next)) {
failed.store(true);
return;
}
++next;
consumed.store(next, std::memory_order_relaxed);
// Retire as we go; a consumer that never retires would deadlock the
// producer, which is exactly the contract being pinned.
ring.Consumer().PublishRetired();
}
});
for (int i = 0; i < kRecords; ++i) {
const std::uint64_t payloadSize = sizeof(std::uint32_t) + (i % 4) * 8;
void* payload = nullptr;
while ((payload = ring.Producer().Reserve(static_cast<std::uint16_t>(1 + (i % 7)),
kRecNone, payloadSize)) == nullptr) {
if (failed.load()) {
break;
}
std::this_thread::yield();
}
if (payload == nullptr) {
break;
}
const std::uint32_t value = static_cast<std::uint32_t>(i);
std::memcpy(payload, &value, sizeof(value));
ring.Producer().Publish();
}
consumer.join();
EXPECT_FALSE(failed.load());
EXPECT_EQ(consumed.load(), kRecords);
EXPECT_TRUE(ring.Invariants());
EXPECT_EQ(ring.Control().cmdRetiredTail.load(), ring.Control().cmdHead.load());
}
// The publish/park protocol end to end, in both directions: publish the
// watermark, THEN NotifyIfParked; park with Doorbell::Wait. A lost wakeup on
// either side shows up as a Wait that times out with work available rather
// than as a hang, so the failure is a red test and not a stuck CI job.
//
// This cannot prove the seq_cst fence pairing (no test can - x86 needs the
// store buffer to hold the release store across the flag read, and it usually
// does not), but it does exercise the exact call order the fences assume, so a
// future edit that rings the bell BEFORE publishing has somewhere to fail.
TEST(RingTest, DoorbellHandoffWakesBothSidesOnEveryPublish) {
// 4 byte payloads: every record is exactly 16 bytes and 4096 is a multiple
// of that, so no wrap filler ever appears and "head != tail" is exactly
// "a record is waiting".
RingFixture ring(4096);
CondVarDoorbell consumerBell;
CondVarDoorbell producerBell;
std::atomic<bool> ok{true};
constexpr int kRecords = 2000;
constexpr std::uint64_t kRecordBytes = 16;
std::thread consumerThread([&] {
int seen = 0;
while (seen < kRecords) {
const bool woke = consumerBell.Wait(
ring.Control().consumerParked,
[&] {
return ring.Control().cmdHead.load(std::memory_order_acquire) !=
ring.Consumer().LocalTail();
},
kDefaultSpinUs, 5000);
if (!woke) {
ok.store(false); // a wakeup was lost, or the producer stalled
return;
}
RingRecordView view{};
bool corrupt = false;
while (ring.Consumer().Pop(view, &corrupt)) {
std::uint32_t value = 0;
std::memcpy(&value, view.payload, sizeof(value));
if (value != static_cast<std::uint32_t>(seen)) {
ok.store(false);
return;
}
++seen;
}
if (corrupt) {
ok.store(false);
return;
}
ring.Consumer().PublishRetired();
NotifyIfParked(producerBell, ring.Control().producerParked);
}
});
for (int i = 0; i < kRecords && ok.load(); ++i) {
void* payload = nullptr;
while ((payload = ring.Producer().Reserve(1, kRecNone, sizeof(std::uint32_t))) == nullptr) {
if (!ok.load()) {
break;
}
if (!producerBell.Wait(
ring.Control().producerParked,
[&] { return ring.Producer().FreeBytes() >= kRecordBytes; }, kDefaultSpinUs,
5000)) {
ok.store(false);
break;
}
}
if (payload == nullptr) {
break;
}
const std::uint32_t value = static_cast<std::uint32_t>(i);
std::memcpy(payload, &value, sizeof(value));
// Publish first, ring second. The other order reopens the lost-wakeup
// window no matter how strong the flag's memory order is.
ring.Producer().Publish();
NotifyIfParked(consumerBell, ring.Control().consumerParked);
}
consumerThread.join();
EXPECT_TRUE(ok.load());
EXPECT_TRUE(ring.Invariants());
}
@@ -1145,6 +1145,8 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxFragmentShaderStorageBlocks = 4;
GLint maxComputeUniformBlocks = 12;
GLint maxComputeWorkGroupInvocations = 128;
GLint maxComputeWorkGroupCount[3] = {65535, 65535, 65535};
GLint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
GLint maxShaderStorageBufferBindings = 8;
GLint maxTextureBufferSize = 65536;
GLint maxUniformBufferBindings = 24;
@@ -1276,6 +1278,17 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &maxCombinedShaderStorageBlocks);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &maxComputeUniformBlocks);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &maxComputeWorkGroupInvocations);
// The per-axis pair beside it, through the indexed query. ES 3.1 core like the
// invocations limit, so it sits inside the same bracketed run: a 3.0 context rejects
// it, the drain below swallows the error and the locals keep the GL 4.3 minimums.
// These are the six backend-owned indexed answers that cross the MGPipe boundary in
// MGPCaps (DynamicBackendParameters::MaxComputeWorkGroupCount/Size).
if (glesFuncs.glGetIntegeri_v) {
for (GLuint axis = 0; axis < 3; ++axis) {
glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, axis, &maxComputeWorkGroupCount[axis]);
glesFuncs.glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &maxComputeWorkGroupSize[axis]);
}
}
glesFuncs.glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorageBufferBindings);
// GL_MAX_TEXTURE_BUFFER_SIZE is deliberately NOT batched here: like
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT below, the pname only exists once buffer textures do,
@@ -1584,6 +1597,10 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = maxComputeWorkGroupCount[axis];
caps.MaxComputeWorkGroupSize[axis] = maxComputeWorkGroupSize[axis];
}
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
caps.MaxTextureBufferSize = maxTextureBufferSize;
// Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves
@@ -1681,6 +1698,10 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_COUNT: %d %d %d", caps.MaxComputeWorkGroupCount[0],
caps.MaxComputeWorkGroupCount[1], caps.MaxComputeWorkGroupCount[2]);
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_SIZE: %d %d %d", caps.MaxComputeWorkGroupSize[0],
caps.MaxComputeWorkGroupSize[1], caps.MaxComputeWorkGroupSize[2]);
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
// Three distinct states, and the suffix must not conflate them: a driver answer, a floor
// kept because there are no buffer textures to ask about, and a floor kept because the
@@ -1309,6 +1309,11 @@ namespace MobileGL {
Int MaxFragmentShaderStorageBlocks = 4;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE per axis, as the driver answers
// glGetIntegeri_v. Raw: the frontend floors them at the GL minimums itself. The
// initialisers are those minimums, for a context that rejects the query.
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.
@@ -196,6 +196,10 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
caps.MaxComputeUniformBlocks = SaturateToInt(p.limits.maxPerStageDescriptorUniformBuffers);
caps.MaxComputeWorkGroupInvocations = SaturateToInt(p.limits.maxComputeWorkGroupInvocations);
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(p.limits.maxComputeWorkGroupCount[axis]);
caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(p.limits.maxComputeWorkGroupSize[axis]);
}
caps.MaxShaderStorageBufferBindings = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = SaturateToInt(p.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
@@ -333,6 +337,10 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
caps.MaxComputeUniformBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorUniformBuffers);
caps.MaxComputeWorkGroupInvocations = SaturateToInt(properties.limits.maxComputeWorkGroupInvocations);
for (SizeT axis = 0; axis < 3; ++axis) {
caps.MaxComputeWorkGroupCount[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupCount[axis]);
caps.MaxComputeWorkGroupSize[axis] = SaturateToInt(properties.limits.maxComputeWorkGroupSize[axis]);
}
caps.MaxShaderStorageBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
caps.MaxTextureBufferSize = SaturateToInt(properties.limits.maxTexelBufferElements);
caps.TextureBufferOffsetAlignment =
@@ -56,6 +56,11 @@ namespace MobileGL {
Int MaxCombinedShaderStorageBlocks = 32;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount / maxComputeWorkGroupSize per
// axis, saturated to Int like every other limit here. Raw: the frontend floors them
// at the GL minimums itself.
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.
+488
View File
@@ -0,0 +1,488 @@
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.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 "PipeStats.h"
#include <Config.h>
#include <fstream>
// ---------------------------------------------------------------------------------------
// SITE INVENTORY - what these counters DO and DO NOT cover.
//
// This list is the contract. A byte class that reads 0 while a real copy runs uncounted is
// worse than a missing counter, because the zero is then read as an answer, so every path
// that moves bytes and is NOT wired is named here by file and function.
//
// Byte classes
// stage-buffer ESPRYT (DirectGLES Managers.cpp): RespecifyStorageNow's
// glBufferData, FlushPendingRangesNow's three shapes (map-write,
// glBufferSubData, upload-ring stage), and the pool-recycle reseed
// in SyncBufferObject.
// MAGMA (DirectVulkan VkBufferManager.cpp): every host->device copy
// of a buffer object's contents - SwapStorageAndUploadAll, the
// StagedRangeCopy staging fill, the in-place uploads in OnRespecify /
// OnSubData / OnFlushMappedRange, the AcquirePersistentMap seed, the
// AcquireResidentSlice initial upload and the AcquireStreamedSlice
// arena fill.
// NOT covered: bytes an app writes THROUGH a persistent map. Those
// never pass through either backend (D4/D-B4) - see
// persistent-map-push.
// stage-texture ESPRYT (Managers.cpp texture upload): the bytes of whichever of
// the three upload shapes ran (rect list / union box / whole level).
// MAGMA (VkTextureManager.cpp): the packed staging slice of an
// upload batch item set.
// NOT covered: Espryt's compressed-texture path, and both backends'
// readback (device->host) paths, which are a different direction and
// want their own class when the reverse channel of section 7 exists.
// stage-ubo-global ESPRYT (DirectGLES.cpp): the default-uniform-block image, both the
// UBO-ring memcpy and the glBufferSubData fallback.
// MAGMA (UniformManager::ResolveDynamicUboDescriptor): the same
// image, counted after the per-frame slice memo, so a frame that
// re-uses the slice correctly contributes nothing.
// stage-ubo-named DirectVulkan UniformManager::ResolveUniformBufferPayload - the
// bytes Magma repacks into its own UBO ring, counted AFTER the
// zero-copy direct-bind decision (a direct bind repacks nothing).
// Espryt contributes nothing by construction (D-B8).
// stage-vertex-client ESPRYT: BackendVertexArrayObject::SyncClientSideAttributesFor-
// DrawArrays (both the Float64-narrowing and the verbatim shapes)
// and the VBO-backed Float64->Float32 narrowing scratch upload.
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Vertex), which
// is the single chokepoint for the converted-vertex-stream and
// client-array staging.
// stage-index-client ESPRYT: the primitive-restart substitution buffer, and MultiDraw's
// rewritten (rebased) index stream.
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Index).
// stage-indirect-cmd ESPRYT MultiDraw.cpp: the DrawElementsIndirectCommand array staged
// for the indirect tiers, and the compute tier's per-draw info
// array. Kept out of stage-index-client because these are draw
// PARAMETERS - the population that becomes MGPipe command-record
// payload, not resource bytes.
// NOT covered: Magma builds no such array (it issues one vkCmdDraw*
// per sub-draw), so this class is Espryt-only by construction.
// persistent-map-push Not wired in P0: today a persistent map is a permanent address
// space donation (D4/D-B4) that survives the whole monolith track,
// so there is no push to count until the IPC track breaks it.
// residual-value-block Placeholder, always 0 until P2 (plan section 6.3).
//
// Call classes
// draws DirectGLES PrepareForDraw and DirectVulkan SetupDraw's entry. A
// dispatch is not a draw and is not counted.
// accessor-calls STATIC TALLIES at the instrumented entry points, NOT a wrapper
// around all 293 pGLContext-> sites. Each instrumented function adds
// the number of GLContext accessor calls that its OWN body executed
// on the path taken, and each tally sits AFTER the last early return
// that would skip those reads. Covered: PrepareForDraw's own reads,
// SyncRenderState, CaptureDrawTextureSyncKeys/CurrentUnitBindings-
// Epoch, SyncNeccessaryTextures' walk, TrySetupDrawFastPath,
// GetOrCreatePipeline and ApplyDynamicDrawStateTail. NOT covered:
// the reads inside the callees those functions invoke (buffer/VAO/
// FBO/program sync, the pipeline payload builder's ~40 reads on a
// memo miss), and every non-draw entry point. The number is
// therefore a LOWER BOUND on the per-draw accessor count, and it is
// the bound over exactly the six gates section 2.3.1 tabulates.
// texture-* Per (target, level) emission, both backends.
//
// Gates: the six of section 2.3.1, each counted exactly once per probe.
//
// READING acc/draw. The accessor tally covers the instrumented functions wherever they
// run, and three of them (SyncRenderState, the texture-key capture, SyncNeccessaryTextures)
// are also reached from NON-draw call sites - Clear, readbacks, the DSA by-name entry
// points - which the `draws` counter deliberately does not count. So acc/draw is the
// per-draw steady-state number section 2.3.1 asks for only in a DRAW-DOMINATED window; in a
// window dominated by clears and readbacks it is inflated by exactly those non-draw
// probes, and the gate hit/miss pairs are the honest reading there.
// ---------------------------------------------------------------------------------------
namespace MobileGL::MG_Util::PipeStats {
Bool g_pipeStatsEnabled = false;
namespace {
constexpr Uint32 kByteClassCount = static_cast<Uint32>(ByteClass::Count);
constexpr Uint32 kCallClassCount = static_cast<Uint32>(CallClass::Count);
constexpr Uint32 kGateCount = static_cast<Uint32>(Gate::Count);
using Counter = std::atomic<Uint64>;
Counter g_frameBytes[kByteClassCount];
Counter g_totalBytes[kByteClassCount];
Counter g_frameCalls[kCallClassCount];
Counter g_totalCalls[kCallClassCount];
Counter g_frameGateHit[kGateCount];
Counter g_totalGateHit[kGateCount];
Counter g_frameGateMiss[kGateCount];
Counter g_totalGateMiss[kGateCount];
Counter g_totalPayloadBuckets[kPayloadHistogramBuckets];
Counter g_frameCount{0};
// Window bases: the run totals as of the previous summary line. Only ever touched
// from OnPresent()/Shutdown() (the present thread), so plain integers.
Uint64 g_windowBaseBytes[kByteClassCount] = {};
Uint64 g_windowBaseCalls[kCallClassCount] = {};
Uint64 g_windowBaseGateHit[kGateCount] = {};
Uint64 g_windowBaseGateMiss[kGateCount] = {};
Uint64 g_windowBaseFrames = 0;
Bool g_shutdownDone = false;
// Frames per summary line, latched by Init() from MOBILEGL_PIPE_STATS_PERIOD.
Uint64 g_summaryPeriod = kDefaultSummaryFramePeriod;
inline void Bump(Counter& counter, Uint64 amount) {
counter.fetch_add(amount, std::memory_order_relaxed);
}
inline Uint64 Read(const Counter& counter) { return counter.load(std::memory_order_relaxed); }
// Bucket 0 is "0 bytes", bucket n>0 holds [2^(n-1), 2^n). Saturates at the last
// bucket so a pathological record cannot index out of the array.
Uint32 PayloadBucketOf(Uint64 bytes) {
if (bytes == 0) {
return 0;
}
Uint32 bucket = 1;
while (bucket + 1 < kPayloadHistogramBuckets && bytes >= (Uint64{1} << bucket)) {
++bucket;
}
return bucket;
}
// Two decimals without <iomanip>. Every per-frame and per-draw field in the summary
// goes through this: the numbers are small (a per-draw accessor count in the 10-25
// band, a per-frame byte count that sizes SEG_STAGE), so truncating integer division
// loses up to a whole unit on exactly the figures the package exists to produce.
// A zero denominator is "n/a" rather than a division by a faked 1.
String FormatFixed2(Uint64 numerator, Uint64 denominator) {
if (denominator == 0) {
return "n/a";
}
const Uint64 hundredths = (numerator * 100 + denominator / 2) / denominator;
return std::to_string(hundredths / 100) + "." + (hundredths % 100 < 10 ? "0" : "") +
std::to_string(hundredths % 100);
}
const char* const kByteClassNames[kByteClassCount] = {
"stage-buffer", "stage-texture", "stage-ubo-global",
"stage-ubo-named", "stage-vertex-client", "stage-index-client",
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
};
const char* const kCallClassNames[kCallClassCount] = {
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
"tex-upload-jobs",
};
const char* const kGateNames[kGateCount] = {
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
"magma-draw-fastpath", "magma-pipeline-memo", "magma-dynamic-tail",
};
// Tracy needs a stable string literal per series, and a gate is TWO series: plotting
// only the misses (which is what the first cut did) hides the denominator, and a
// gate's whole point is the ratio.
const char* const kGateHitPlotNames[kGateCount] = {
"espryt-render-state-hit", "espryt-texture-sync-list-hit", "espryt-unit-bindings-epoch-hit",
"magma-draw-fastpath-hit", "magma-pipeline-memo-hit", "magma-dynamic-tail-hit",
};
const char* const kGateMissPlotNames[kGateCount] = {
"espryt-render-state-miss", "espryt-texture-sync-list-miss", "espryt-unit-bindings-epoch-miss",
"magma-draw-fastpath-miss", "magma-pipeline-memo-miss", "magma-dynamic-tail-miss",
};
// Short forms, so the per-120-frame line stays one terminal line wide.
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
"idxc", "icmd", "pmap", "resid"};
const char* const kGateShort[kGateCount] = {"ers", "etl", "eub", "mfp", "mpm", "mdt"};
void ResetCounters() {
for (Uint32 i = 0; i < kByteClassCount; ++i) {
g_frameBytes[i].store(0, std::memory_order_relaxed);
g_totalBytes[i].store(0, std::memory_order_relaxed);
g_windowBaseBytes[i] = 0;
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
g_frameCalls[i].store(0, std::memory_order_relaxed);
g_totalCalls[i].store(0, std::memory_order_relaxed);
g_windowBaseCalls[i] = 0;
}
for (Uint32 i = 0; i < kGateCount; ++i) {
g_frameGateHit[i].store(0, std::memory_order_relaxed);
g_totalGateHit[i].store(0, std::memory_order_relaxed);
g_frameGateMiss[i].store(0, std::memory_order_relaxed);
g_totalGateMiss[i].store(0, std::memory_order_relaxed);
g_windowBaseGateHit[i] = 0;
g_windowBaseGateMiss[i] = 0;
}
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
g_totalPayloadBuckets[i].store(0, std::memory_order_relaxed);
}
g_frameCount.store(0, std::memory_order_relaxed);
g_windowBaseFrames = 0;
}
void EmitSummaryLine() {
const String line = FormatWindowLine();
// MGLOG_I on purpose, against the project's usual "MGLOG_D for anything
// non-critical" rule: the line has to survive an INFO build (that is the only
// build a device ever runs), it is emitted at most once per 120 frames, and it
// exists at all only when the operator set MOBILEGL_PIPE_STATS=1. It is an
// opt-in measurement channel, not per-frame noise.
MGLOG_I("%s", line.c_str());
AdvanceSummaryWindow();
}
void WriteJsonDump() {
const String& path = MG_Config::Features.PipeStatsFile;
if (path.empty()) {
return;
}
std::ofstream out(path, std::ios::out | std::ios::trunc);
if (!out) {
MGLOG_W("PipeStats: could not open MOBILEGL_PIPE_STATS_FILE='%s' for writing", path.c_str());
return;
}
out << FormatJson();
out.flush();
if (!out) {
MGLOG_W("PipeStats: failed writing MOBILEGL_PIPE_STATS_FILE='%s'", path.c_str());
return;
}
MGLOG_I("MGPipe stats: wrote JSON dump to %s", path.c_str());
}
} // namespace
void Init() {
ResetCounters();
g_shutdownDone = false;
g_pipeStatsEnabled = MG_Config::Features.PipeStats;
g_summaryPeriod = MG_Config::Features.PipeStatsPeriod == 0
? kDefaultSummaryFramePeriod
: static_cast<Uint64>(MG_Config::Features.PipeStatsPeriod);
if (g_pipeStatsEnabled) {
MGLOG_I("MGPipe stats: counters ON (MOBILEGL_PIPE_STATS), summary every %llu frames%s%s",
static_cast<unsigned long long>(g_summaryPeriod),
MG_Config::Features.PipeStatsFile.empty() ? "" : ", JSON dump to ",
MG_Config::Features.PipeStatsFile.c_str());
}
}
Uint64 SummaryFramePeriod() { return g_summaryPeriod; }
void Shutdown() {
if (!g_pipeStatsEnabled || g_shutdownDone) {
return;
}
g_shutdownDone = true;
EmitSummaryLine();
WriteJsonDump();
}
void AddBytes(ByteClass byteClass, Uint64 bytes) {
const Uint32 index = static_cast<Uint32>(byteClass);
Bump(g_frameBytes[index], bytes);
Bump(g_totalBytes[index], bytes);
}
void AddCalls(CallClass callClass, Uint64 count) {
const Uint32 index = static_cast<Uint32>(callClass);
Bump(g_frameCalls[index], count);
Bump(g_totalCalls[index], count);
}
void CountGate(Gate gate, Bool hit) {
const Uint32 index = static_cast<Uint32>(gate);
if (hit) {
Bump(g_frameGateHit[index], 1);
Bump(g_totalGateHit[index], 1);
} else {
Bump(g_frameGateMiss[index], 1);
Bump(g_totalGateMiss[index], 1);
}
}
void RecordDrawPayloadBytes(Uint64 bytes) { Bump(g_totalPayloadBuckets[PayloadBucketOf(bytes)], 1); }
void OnPresent() {
// Every frame accumulator is EXCHANGED for zero, and the exchanged value is what gets
// plotted. A read followed by a store(0) would lose any Bump that lands in between -
// buffer and texture staging reach these counters from more than one thread - from
// the plot AND from every frame; an exchange hands every add to exactly one frame.
// Without Tracy the value is taken and dropped: the clear is still the point.
//
// One plot per counter, the frame's value. Tracy keeps the series by name, and the
// names are the static literals above, which is what TracyPlot requires. A gate is
// two series - hits and misses - because the ratio is the deliverable and a miss
// count alone cannot be read.
//
// The payload histogram is deliberately NOT plotted: it is a run-total distribution
// over draws (section 4.5.7), not a per-frame scalar, and Tracy has no histogram
// series. It reaches the operator through the JSON dump.
const auto take = [](Counter& counter) { return counter.exchange(0, std::memory_order_relaxed); };
for (Uint32 i = 0; i < kByteClassCount; ++i) {
const Uint64 value = take(g_frameBytes[i]);
(void)value;
#ifdef TRACY_ENABLE
TracyPlot(kByteClassNames[i], static_cast<Int64>(value));
#endif
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
const Uint64 value = take(g_frameCalls[i]);
(void)value;
#ifdef TRACY_ENABLE
TracyPlot(kCallClassNames[i], static_cast<Int64>(value));
#endif
}
for (Uint32 i = 0; i < kGateCount; ++i) {
const Uint64 hits = take(g_frameGateHit[i]);
const Uint64 misses = take(g_frameGateMiss[i]);
(void)hits;
(void)misses;
#ifdef TRACY_ENABLE
TracyPlot(kGateHitPlotNames[i], static_cast<Int64>(hits));
TracyPlot(kGateMissPlotNames[i], static_cast<Int64>(misses));
#endif
}
const Uint64 frames = g_frameCount.fetch_add(1, std::memory_order_relaxed) + 1;
if (frames % g_summaryPeriod == 0) {
EmitSummaryLine();
}
}
Uint64 FrameBytes(ByteClass byteClass) { return Read(g_frameBytes[static_cast<Uint32>(byteClass)]); }
Uint64 TotalBytes(ByteClass byteClass) { return Read(g_totalBytes[static_cast<Uint32>(byteClass)]); }
Uint64 FrameCalls(CallClass callClass) { return Read(g_frameCalls[static_cast<Uint32>(callClass)]); }
Uint64 TotalCalls(CallClass callClass) { return Read(g_totalCalls[static_cast<Uint32>(callClass)]); }
Uint64 TotalGateHits(Gate gate) { return Read(g_totalGateHit[static_cast<Uint32>(gate)]); }
Uint64 TotalGateMisses(Gate gate) { return Read(g_totalGateMiss[static_cast<Uint32>(gate)]); }
Uint64 TotalPayloadBucket(Uint32 bucket) {
return bucket < kPayloadHistogramBuckets ? Read(g_totalPayloadBuckets[bucket]) : 0;
}
Uint64 FrameCount() { return Read(g_frameCount); }
const char* NameOf(ByteClass byteClass) { return kByteClassNames[static_cast<Uint32>(byteClass)]; }
const char* NameOf(CallClass callClass) { return kCallClassNames[static_cast<Uint32>(callClass)]; }
const char* NameOf(Gate gate) { return kGateNames[static_cast<Uint32>(gate)]; }
String FormatWindowLine() {
// Window values: everything since the previous summary. A run total over a workload
// whose shape changes (load, then steady state) hides exactly the number P2 wants.
const Uint64 frames = Read(g_frameCount);
const Uint64 windowFrames = frames - g_windowBaseFrames;
// A window with no Present in it (teardown before the first frame, or a slice whose
// whole workload runs off-screen) has NO per-frame reading. Printing the window
// totals under a "/f" label there is how a 47x overstatement of the SEG_STAGE sizing
// input got printed as a per-frame figure; the label changes instead.
const Bool perFrame = windowFrames != 0;
Uint64 bytes[kByteClassCount];
for (Uint32 i = 0; i < kByteClassCount; ++i) {
bytes[i] = Read(g_totalBytes[i]) - g_windowBaseBytes[i];
}
Uint64 calls[kCallClassCount];
for (Uint32 i = 0; i < kCallClassCount; ++i) {
calls[i] = Read(g_totalCalls[i]) - g_windowBaseCalls[i];
}
Uint64 gateHit[kGateCount];
Uint64 gateMiss[kGateCount];
for (Uint32 i = 0; i < kGateCount; ++i) {
gateHit[i] = Read(g_totalGateHit[i]) - g_windowBaseGateHit[i];
gateMiss[i] = Read(g_totalGateMiss[i]) - g_windowBaseGateMiss[i];
}
const Uint64 draws = calls[static_cast<Uint32>(CallClass::Draws)];
const Uint64 accessorCalls = calls[static_cast<Uint32>(CallClass::AccessorCalls)];
String line = "MGPipe stats:";
line += " frames=" + std::to_string(frames);
line += " window=" + std::to_string(windowFrames);
line += " draws=" + std::to_string(draws);
line += " draws/f=" + FormatFixed2(draws, windowFrames);
line += " acc=" + std::to_string(accessorCalls);
// Same rule as the per-frame fields: a window with no draw in it has no per-draw
// number, and "0.00" next to a non-zero acc= is the same lie in a smaller font.
line += " acc/draw=" + FormatFixed2(accessorCalls, draws);
// "bytes/f[...]" only when there IS a frame to divide by; otherwise the bracket is
// labelled "bytes[...]" and carries the window totals verbatim.
line += perFrame ? " bytes/f[" : " bytes[";
for (Uint32 i = 0; i < kByteClassCount; ++i) {
if (i != 0) {
line += " ";
}
line += kByteClassShort[i];
line += "=";
line += perFrame ? FormatFixed2(bytes[i], windowFrames) : std::to_string(bytes[i]);
}
line += "] tex[emit=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadEmissions)]);
line += " box=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadBoxEmissions)]);
line += " rect=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadRectEmissions)]);
line += " jobs=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadJobs)]);
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
if (i != 0) {
line += " ";
}
line += kGateShort[i];
line += "=";
line += std::to_string(gateHit[i]);
line += "/";
line += std::to_string(gateMiss[i]);
}
line += "]";
return line;
}
void AdvanceSummaryWindow() {
for (Uint32 i = 0; i < kByteClassCount; ++i) {
g_windowBaseBytes[i] = Read(g_totalBytes[i]);
}
for (Uint32 i = 0; i < kCallClassCount; ++i) {
g_windowBaseCalls[i] = Read(g_totalCalls[i]);
}
for (Uint32 i = 0; i < kGateCount; ++i) {
g_windowBaseGateHit[i] = Read(g_totalGateHit[i]);
g_windowBaseGateMiss[i] = Read(g_totalGateMiss[i]);
}
g_windowBaseFrames = Read(g_frameCount);
}
String FormatJson() {
String json = "{\n";
json += " \"frames\": " + std::to_string(Read(g_frameCount)) + ",\n";
json += " \"bytes\": {\n";
for (Uint32 i = 0; i < kByteClassCount; ++i) {
json += " \"";
json += kByteClassNames[i];
json += "\": " + std::to_string(Read(g_totalBytes[i]));
json += (i + 1 == kByteClassCount) ? "\n" : ",\n";
}
json += " },\n \"calls\": {\n";
for (Uint32 i = 0; i < kCallClassCount; ++i) {
json += " \"";
json += kCallClassNames[i];
json += "\": " + std::to_string(Read(g_totalCalls[i]));
json += (i + 1 == kCallClassCount) ? "\n" : ",\n";
}
json += " },\n \"gates\": {\n";
for (Uint32 i = 0; i < kGateCount; ++i) {
json += " \"";
json += kGateNames[i];
json += "\": {\"hit\": " + std::to_string(Read(g_totalGateHit[i])) +
", \"miss\": " + std::to_string(Read(g_totalGateMiss[i])) + "}";
json += (i + 1 == kGateCount) ? "\n" : ",\n";
}
json += " },\n \"cmd-bytes-per-draw-histogram\": [";
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
if (i != 0) {
json += ", ";
}
json += std::to_string(Read(g_totalPayloadBuckets[i]));
}
json += "]\n}\n";
return json;
}
void SetEnabledForTesting(Bool enabled) { g_pipeStatsEnabled = enabled; }
void ResetForTesting() { ResetCounters(); }
} // namespace MobileGL::MG_Util::PipeStats
+192
View File
@@ -0,0 +1,192 @@
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.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 boundary counters (plan B section 11 "P0 - hygiene, measurement, gates and
// skeleton", and the corollary in section 2.3.1).
//
// WHAT THIS IS FOR. The disaggregation plan has to size two things it cannot size by
// reading the tree: how many BYTES cross the frontend/backend boundary per frame (that
// sizes SEG_STAGE and the command segment), and how many accessor CALLS and memo-gate
// probes the backends actually execute per draw (that decides whether pushing state is
// cheaper than pulling it at all). Section 2.3.1 makes the second one the load-bearing
// number: the static call-site counts everyone quoted - Espryt 124 / Magma 169 - are NOT
// the dynamic per-draw cost, because every one of those paths is memo-gated, and the real
// steady state is believed to be 10-25 accessor calls per backend per draw. Without a
// dynamic counter the P2 verdict stays a guess.
//
// COST WHEN OFF. g_pipeStatsEnabled is a plain global Bool latched once at Init() from
// MG_Config::Features.PipeStats (MOBILEGL_PIPE_STATS). Every counting site in the two
// backends is written as
//
// if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::Add...(...);
//
// so with the feature off a site costs one load of a hot global plus one never-taken,
// perfectly-predicted branch, and none of the counter state is touched. The counters
// themselves are relaxed atomics rather than plain integers because texture and buffer
// staging can be reached from more than one thread; relaxed adds cost nothing extra on the
// off path, which never reaches them. The off-path cost is not a guess: see the paired
// A/B in the branch's evidence.
//
// WHAT IS COUNTED AND WHAT IS NOT: see the site inventory in PipeStats.cpp. That inventory
// is the contract - it names every path that is NOT wired, because a byte class that reads
// zero while a real copy runs uncounted is worse than a missing counter.
namespace MobileGL::MG_Util::PipeStats {
// Byte classes. Every one of these names a population of bytes that would have to be
// MOVED across the boundary once the backend no longer shares an address space with
// the frontend, which is why they are grouped this way rather than by call site.
enum class ByteClass : Uint32 {
// Buffer object contents flushed to the driver: glBufferData / glBufferSubData /
// map-write ranges / the persistent upload ring (Espryt), and every host->device
// copy of a buffer object's contents (Magma).
StageBuffer = 0,
// Texel bytes handed to glTexSubImage & friends / packed into the Vulkan upload
// staging slice, whichever upload shape was chosen.
StageTexture,
// The default-uniform-block ("global UBO") image, uploaded at most once per program
// per frame.
StageUboGlobal,
// Named uniform-block bytes that a backend has to repack itself, i.e. Magma's UBO
// ring. Espryt binds the frontend buffer straight to the driver and contributes
// nothing here - which is exactly the asymmetry D-B8 is about.
StageUboNamed,
// Client-memory vertex arrays uploaded into a scratch VBO / transient arena slice on
// the draw path.
StageVertexClient,
// Client-memory / rewritten index data staged on the draw path.
StageIndexClient,
// Draw-parameter bytes a backend synthesises and stages for the draw itself: the
// indirect-command array and the compute path's per-draw info array. These are the
// bytes that become MGPipe command-record payload once the boundary is explicit,
// which is why they are not folded into the index class.
StageIndirectCmd,
// Bytes pushed because a persistently mapped range was published to the backend.
PersistentMapPush,
// PLACEHOLDER (plan section 6.3): the residual value block does not exist yet. The
// class is minted now so the counter names never churn; it stays at 0 until P2.
ResidualValueBlock,
Count
};
// Call classes: the dynamic per-draw cost section 2.3.1 says P2 cannot be decided
// without.
enum class CallClass : Uint32 {
// Draws that reached an instrumented backend draw-preparation entry point. The
// denominator for every "per draw" number below.
Draws = 0,
// GLContext accessor calls actually EXECUTED on the instrumented paths. Counted in
// static tallies at the ~10 hot entry points, not by wrapping all 293 call sites -
// see the inventory in PipeStats.cpp for exactly what is and is not in this number.
AccessorCalls,
// Texture upload emissions: one per (upload target, level) that actually shipped
// texels. The eventual resource_subdata record count.
TextureUploadEmissions,
// Emissions that took the union-box shape (one driver upload job).
TextureUploadBoxEmissions,
// Emissions that took the refined rect-list shape (N driver upload jobs). The
// box/rect split is the thing SSIM cannot see and the +6 ms/frame Mali cliff came
// from, so it is counted separately from the byte total.
TextureUploadRectEmissions,
// Driver upload jobs issued by those emissions: 1 per box emission, N per rect-list
// emission.
TextureUploadJobs,
Count
};
// Memo gates. Each is a place where a backend decides "nothing moved, skip the work".
// Hit == the gate short-circuited; Miss == it fell through and did the work. The six
// are exactly the ones section 2.3.1 tabulates.
enum class Gate : Uint32 {
// DirectGLES.cpp SyncRenderState: the render-state-version early-out.
EsprytRenderState = 0,
// DirectGLES.cpp SyncNeccessaryTextures: the six-value sync-list key compare.
EsprytTextureSyncList,
// DirectGLES.cpp CurrentUnitBindingsEpoch: the (context, max unit, bind generation)
// shutter over the unit walk.
EsprytUnitBindingsEpoch,
// VulkanRenderer.cpp TrySetupDrawFastPath: the whole snapshot fast path.
MagmaDrawFastPath,
// VulkanRenderer.cpp GetOrCreatePipeline: the pipeline memo.
MagmaPipelineMemo,
// VulkanRenderer.cpp ApplyDynamicDrawStateTail: the version+extent tail gate.
MagmaDynamicTail,
Count
};
// Per-draw command payload size histogram (plan section 4.5.7: SEG_CMD has to be sized
// off the DISTRIBUTION, not off a per-frame total). PLACEHOLDER in P0: MGPipe emits no
// records yet, so nothing in the backends calls RecordDrawPayloadBytes. The bucketing
// and the reporting are implemented and unit-tested so that the first generator to
// emit records only has to add the one call.
inline constexpr Uint32 kPayloadHistogramBuckets = 24;
// Frames between two summary lines when MOBILEGL_PIPE_STATS=1.
inline constexpr Uint64 kDefaultSummaryFramePeriod = 120;
// The period Init() latched from MOBILEGL_PIPE_STATS_PERIOD (kDefaultSummaryFramePeriod
// when unset); never 0.
Uint64 SummaryFramePeriod();
// The latch. Read directly by Enabled() so the off path is a global load and a
// predicted branch - do not turn this into a function call.
extern Bool g_pipeStatsEnabled;
inline Bool Enabled() { return g_pipeStatsEnabled; }
// Latches g_pipeStatsEnabled from MG_Config::Features.PipeStats and clears every
// counter. Called from MobileGL::Initialize() right after the config load.
void Init();
// Final summary line plus, if MOBILEGL_PIPE_STATS_FILE names a path, the JSON dump.
// Called from MobileGL's teardown. Idempotent.
void Shutdown();
void AddBytes(ByteClass byteClass, Uint64 bytes);
void AddCalls(CallClass callClass, Uint64 count);
void CountGate(Gate gate, Bool hit);
void RecordDrawPayloadBytes(Uint64 bytes);
// Frame boundary: publishes the frame's values to Tracy (when TRACY_ENABLE), folds them
// into the run totals, clears the frame accumulators, and every kSummaryFramePeriod
// frames emits the summary line. Called from each backend's Present().
void OnPresent();
// --- introspection, for the unit test and the JSON dump -------------------------
Uint64 FrameBytes(ByteClass byteClass);
Uint64 TotalBytes(ByteClass byteClass);
Uint64 FrameCalls(CallClass callClass);
Uint64 TotalCalls(CallClass callClass);
Uint64 TotalGateHits(Gate gate);
Uint64 TotalGateMisses(Gate gate);
Uint64 TotalPayloadBucket(Uint32 bucket);
Uint64 FrameCount();
const char* NameOf(ByteClass byteClass);
const char* NameOf(CallClass callClass);
const char* NameOf(Gate gate);
// The compact fixed-format one-liner MGLOG_I prints, covering the CURRENT window (see
// AdvanceSummaryWindow). PURE: calling it twice returns the same text and changes no
// counter, so a probe, a test or a second reporting channel can format the window
// without stealing it from the log.
String FormatWindowLine();
// Closes the current window: the run totals as of now become the base the next
// FormatWindowLine() subtracts. Emitting the line and advancing the window are separate
// on purpose - the pair used to be one function whose name promised a formatter.
void AdvanceSummaryWindow();
// The teardown dump. Run totals only: a per-frame JSON stream is a different tool.
String FormatJson();
// Test hooks, used by no shipping path. Init() clears the counters through an internal
// ResetCounters() rather than by calling ResetForTesting().
void SetEnabledForTesting(Bool enabled);
void ResetForTesting();
} // namespace MobileGL::MG_Util::PipeStats
@@ -234,6 +234,10 @@ target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc)
add_library(trace_replay_runner SHARED
trace_replay_core.cpp
trace_replay_jni.cpp
# P0 spike A. Android-only, and deliberately its own TU: trace_replay_core.cpp is
# shared verbatim with the desktop mobilegl_trace_replay runner
# (tools/trace_replay/CMakeLists.txt), which cannot see <android/log.h>.
spawn_spike.cpp
"${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay/apitrace_fbo_dump.cpp")
target_compile_features(trace_replay_runner PRIVATE cxx_std_17)
@@ -249,4 +253,5 @@ target_link_libraries(trace_replay_runner
retrace_common
image
android
log
dl)
@@ -0,0 +1,227 @@
// P0 spike A - the Android half of the delivery chain (PLAN-B.md §8.1, inheriting
// PLAN.md §11.1-§11.6). See spawn_spike.hpp for what the spike is asking.
//
// Android-only on purpose: this TU is listed only by
// android-plugin/app/src/trace/cpp/CMakeLists.txt. Its sibling trace_replay_core.cpp is
// shared with the DESKTOP mobilegl_trace_replay runner, which has no <android/log.h>,
// so nothing Android-specific may live there.
#include "spawn_spike.hpp"
#if !defined(__ANDROID__)
#error "spawn_spike.cpp is Android-only; do not add it to the desktop trace replay build"
#endif
#include <android/log.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
// execve needs the environment the parent already has: a server process started from
// the app must inherit it, and handing it an empty one would change what is being tested.
extern "C" char** environ;
namespace mobilegl_trace {
namespace {
constexpr const char* kSpikeLogTag = "MobileGLTraceRunner";
std::string ReadWholeFile(const std::string& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
return {};
}
std::ostringstream contents;
contents << input.rdbuf();
std::string text = contents.str();
while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == '\0')) {
text.pop_back();
}
return text;
}
// The domain this process is in. `u:r:untrusted_app:s0:...` is the whole point of the
// spike: an exec that works from an `adb run-as` shell says nothing about whether the
// app itself is allowed to do it, because that shell is a different SELinux domain.
std::string ReadSelfSelinuxContext() {
const std::string context = ReadWholeFile("/proc/self/attr/current");
return context.empty() ? "<unreadable>" : context;
}
// Starts the child with its stdout and stderr redirected into `outputPath`, reports the
// child pid through `childPid` and, when the exec itself was refused, the child's errno
// through `execErrno`. Returns 0, or the errno of a failure that happened before the
// child existed at all.
//
// fork/execve, not posix_spawn: bionic only declares posix_spawn from API 28 while
// MobileGL ships at minSdk 26 (the root CMakeLists.txt pins MOBILEGL_ANDROID_API_LEVEL
// to 26 and refuses to configure lower), so posix_spawn is not available to the shipping
// build and this is the shape the production spawn path has to take. Nothing happens
// between fork and execve except open/dup2/execve/write/_exit, all async-signal-safe,
// because the parent is a multi-threaded JVM process.
int SpawnSpikeChild(const std::string& serverPath,
const std::string& markerPath,
const std::string& outputPath,
pid_t* childPid,
int* execErrno) {
*execErrno = 0;
char* argv[] = {const_cast<char*>(serverPath.c_str()),
const_cast<char*>(markerPath.c_str()), nullptr};
// The errno of a refused exec is the answer this spike is here to bring back, and it
// is raised in a process that cannot return anything: by the time the parent sees a
// wait status the reason has been flattened into an exit code. So the child writes
// the raw errno into a close-on-exec pipe. A successful exec closes the write end for
// free and the parent reads EOF; a refused one leaves the four bytes behind. EACCES
// (SELinux, or a noexec mount) and ENOEXEC (a mangled or non-PIE file) are entirely
// different verdicts for the design and this is the only thing that separates them.
int report[2] = {-1, -1};
if (pipe2(report, O_CLOEXEC) != 0) {
return errno;
}
const pid_t forked = fork();
if (forked < 0) {
const int forkErrno = errno;
close(report[0]);
close(report[1]);
return forkErrno;
}
if (forked == 0) {
close(report[0]);
// Without this the child's output is unobservable: an Android app process has
// stdout on /dev/null, so a printed line would vanish and the spike could not
// tell "ran and printed" apart from "never ran".
const int outputFd = open(outputPath.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (outputFd >= 0) {
dup2(outputFd, STDOUT_FILENO);
dup2(outputFd, STDERR_FILENO);
if (outputFd != STDOUT_FILENO && outputFd != STDERR_FILENO) {
close(outputFd);
}
}
execve(serverPath.c_str(), argv, environ);
const int failure = errno;
// Only reached when the exec was refused - the one outcome this spike is about.
const ssize_t written = write(report[1], &failure, sizeof(failure));
static_cast<void>(written);
// 127 is the shell's convention for "could not exec" and is distinguishable from
// every status the stub itself can return.
_exit(127);
}
close(report[1]);
int failure = 0;
ssize_t got = 0;
// Blocks until the child either execs (the write end closes, read returns 0) or
// reports why it could not.
while ((got = read(report[0], &failure, sizeof(failure))) < 0 && errno == EINTR) {
}
close(report[0]);
if (got == static_cast<ssize_t>(sizeof(failure))) {
*execErrno = failure;
}
*childPid = forked;
return 0;
}
} // namespace
SpawnSpikeResult RunSpawnSpike(const SpawnSpikeRequest& request) {
SpawnSpikeResult result;
result.parentSelinuxContext = ReadSelfSelinuxContext();
if (request.serverPath.empty() || request.markerPath.empty()) {
result.message = "spike-spawn: serverPath and markerPath are both required";
return result;
}
// A stale marker from a previous run would otherwise be read back as this run's
// proof. Remove it first, so "the marker exists" can only mean the child wrote it.
unlink(request.markerPath.c_str());
const std::string childOutputPath = request.markerPath + ".stdout";
unlink(childOutputPath.c_str());
struct stat serverStat {};
if (stat(request.serverPath.c_str(), &serverStat) != 0) {
result.spawnErrno = errno;
result.message = "spike-spawn: " + request.serverPath + " does not exist: " +
std::strerror(errno);
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s", result.message.c_str());
return result;
}
pid_t childPid = -1;
int execErrno = 0;
const int spawnStatus = SpawnSpikeChild(request.serverPath, request.markerPath,
childOutputPath, &childPid, &execErrno);
if (spawnStatus != 0) {
result.spawnErrno = spawnStatus;
result.message = "spike-spawn: could not start " + request.serverPath +
": spawnErrno=" + std::to_string(spawnStatus) + " (" +
std::strerror(spawnStatus) + ")";
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s (parentSelinux=%s)",
result.message.c_str(), result.parentSelinuxContext.c_str());
return result;
}
result.spawned = true;
result.childPid = static_cast<int>(childPid);
result.execErrno = execErrno;
int waitStatus = 0;
while (waitpid(childPid, &waitStatus, 0) < 0) {
if (errno != EINTR) {
result.message = "spike-spawn: waitpid failed: " + std::string(std::strerror(errno));
__android_log_print(ANDROID_LOG_ERROR, kSpikeLogTag, "%s", result.message.c_str());
return result;
}
}
result.waitStatus = waitStatus;
if (WIFEXITED(waitStatus)) {
result.exitCode = WEXITSTATUS(waitStatus);
}
if (WIFSIGNALED(waitStatus)) {
result.termSignal = WTERMSIG(waitStatus);
}
result.markerContent = ReadWholeFile(request.markerPath);
result.childOutput = ReadWholeFile(childOutputPath);
result.succeeded =
result.execErrno == 0 && result.exitCode == 0 && !result.markerContent.empty();
std::ostringstream message;
message << "spike-spawn: " << (result.succeeded ? "OK" : "FAILED")
<< " server=" << request.serverPath
<< " pid=" << result.childPid
<< " exit=" << result.exitCode
<< " signal=" << result.termSignal
// Always printed, including on the success path, so a reader never has to
// guess whether the field was collected or merely absent.
<< " execErrno=" << result.execErrno
<< " (" << (result.execErrno == 0 ? "exec succeeded"
: std::strerror(result.execErrno)) << ")"
<< " parentSelinux=" << result.parentSelinuxContext
<< " marker=[" << result.markerContent << "]"
<< " childStdout=[" << result.childOutput << "]";
result.message = message.str();
__android_log_print(result.succeeded ? ANDROID_LOG_INFO : ANDROID_LOG_ERROR, kSpikeLogTag,
"%s", result.message.c_str());
// The Activity is normally gone as soon as the run finishes, so the verdict also goes
// to a file next to the marker; that is what a device lane copies out.
std::ofstream report(request.markerPath + ".report", std::ios::trunc);
if (report) {
report << result.message << "\n";
}
return result;
}
} // namespace mobilegl_trace
@@ -0,0 +1,60 @@
#pragma once
// ---------------------------------------------------------------------------
// P0 spike A: exec a second packaged native executable from this process.
//
// Answers one question and nothing else: can an ordinary Android application
// process (untrusted_app, NOT an `adb run-as` shell, which runs in a different
// SELinux domain and would prove nothing) exec a binary that was shipped inside
// its own APK as lib/<abi>/lib*.so? The disaggregated design needs a server
// process on Android and this is its only supported delivery route (PLAN-B.md
// §8.1, inheriting PLAN.md §11.1-§11.6).
//
// This lives beside trace_replay_core.hpp rather than inside it because
// trace_replay_core.cpp is ALSO compiled by the desktop mobilegl_trace_replay
// runner (tools/trace_replay/CMakeLists.txt names it directly), where <android/log.h>
// does not exist. The spike is Android-only, so it gets an Android-only TU;
// spawn_spike.cpp is listed only by the trace APK's CMakeLists.
//
// Nothing in the replay path calls this; it runs only when the trace Activity is
// launched with the `mobilegl_spike_spawn` intent extra.
// ---------------------------------------------------------------------------
#include <string>
namespace mobilegl_trace {
struct SpawnSpikeRequest {
// Absolute path of the executable, normally
// getApplicationInfo().nativeLibraryDir + "/libMobileGLServer.so".
std::string serverPath;
// Marker file the child is asked to write, passed to it as argv[1]. The child's
// stdout and stderr are captured next to it, with ".stdout" appended.
std::string markerPath;
};
struct SpawnSpikeResult {
bool spawned = false;
// Exec'd, waited for, exited 0, and the marker file came back non-empty.
bool succeeded = false;
// errno of the pre-fork or fork failure - the parent could not even try.
int spawnErrno = 0;
// errno of a REFUSED execve, carried out of the child over a close-on-exec pipe.
// This is the one datum the spike exists to produce: EACCES (SELinux or the mount's
// noexec) and ENOEXEC (the packager mangled the file) are different verdicts, and
// the exit status alone cannot tell them apart.
int execErrno = 0;
int childPid = -1;
int waitStatus = -1;
int exitCode = -1;
int termSignal = -1;
// /proc/self/attr/current of THIS process - the domain the exec was attempted from.
std::string parentSelinuxContext;
std::string markerContent;
std::string childOutput;
std::string message;
};
SpawnSpikeResult RunSpawnSpike(const SpawnSpikeRequest& request);
} // namespace mobilegl_trace
@@ -0,0 +1,74 @@
#pragma once
// The generic environment passthrough of the retrace lane, split out of
// trace_replay_jni.cpp and trace_replay_core.cpp so a host-side test can pin it.
//
// One intent extra (`--es mobilegl_env "K=V;K=V"`) carries every MOBILEGL_* knob that has
// no dedicated flag, which is what PLAN-B.md §11 P0 needs when it adds MOBILEGL_PIPE_*.
// That makes this hand-rolled two-level parse the single point where the whole batch can
// be silently misread, and the only lane that exercises it end to end runs on a device -
// hence tools/trace_replay/trace_env_overrides_test.cpp, which every desktop configure
// that builds the replay runner runs at build time.
#include <cstddef>
#include <string>
#include <vector>
namespace mobilegl_trace {
// Splits `A;B;C` into its entries, dropping empty ones. Also used for the texture and
// FBO dump lists, whose entries carry their own ',' and ':' separators. A value that
// itself contains ';' therefore cannot be expressed - that is the format's limit, not a
// bug to work around here.
inline std::vector<std::string> SplitSemicolonList(const std::string& value) {
std::vector<std::string> values;
std::size_t begin = 0;
while (begin < value.size()) {
const std::size_t end = value.find(';', begin);
const std::string entry = value.substr(begin, end - begin);
if (!entry.empty()) {
values.push_back(entry);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
return values;
}
enum class EnvOverrideAction {
// Nothing to do: the entry is empty, or names an empty key.
Ignore,
// setenv(key, value, 1). `K=` is a Set of the empty string, deliberately distinct
// from Unset: a knob read with getenv() != nullptr treats them differently.
Set,
// unsetenv(key). An entry with no '=' means this, and it is the only way for a
// caller to clear a variable the per-knob marshalling above it already set.
Unset,
};
// Classifies one `KEY=VALUE` / `KEY` entry. The first '=' separates; later ones belong to
// the value, so `KEY=a=b` sets KEY to `a=b`.
inline EnvOverrideAction ParseEnvOverride(const std::string& entry,
std::string* key,
std::string* value) {
key->clear();
value->clear();
const std::size_t separator = entry.find('=');
if (separator == std::string::npos) {
if (entry.empty()) {
return EnvOverrideAction::Ignore;
}
*key = entry;
return EnvOverrideAction::Unset;
}
if (separator == 0) {
return EnvOverrideAction::Ignore;
}
*key = entry.substr(0, separator);
*value = entry.substr(separator + 1);
return EnvOverrideAction::Set;
}
} // namespace mobilegl_trace
@@ -4,6 +4,7 @@
#include "apitrace_exit.hpp"
#include "png.h"
#include "trace_benchmark.hpp"
#include "trace_env_overrides.hpp"
#include <algorithm>
#include <cerrno>
@@ -130,6 +131,32 @@ std::string JsonEscape(const std::string& value) {
return out.str();
}
// Generic environment passthrough. One intent extra carries `K=V;K=V`, so a new
// MOBILEGL_* knob costs nothing in the five files between the CI script and this
// setenv - the per-knob plumbing above is what this replaces going forward
// (PLAN-B.md §11 P0, which adds a batch of MOBILEGL_PIPE_* switches).
//
// Applied last, immediately before the library is loaded: it is the escape hatch, so it
// has to be able to override the fields marshalled above, and MobileGL's ConfigLoader
// reads the environment during dlopen. The decision of what each entry means lives in
// trace_env_overrides.hpp so a host-side test can pin it; this is only the setenv.
void ApplyEnvOverrides(const std::vector<std::string>& entries) {
for (const std::string& entry : entries) {
std::string key;
std::string value;
switch (ParseEnvOverride(entry, &key, &value)) {
case EnvOverrideAction::Set:
setenv(key.c_str(), value.c_str(), 1);
break;
case EnvOverrideAction::Unset:
unsetenv(key.c_str());
break;
case EnvOverrideAction::Ignore:
break;
}
}
}
bool LoadMobileGL(const Request& request, std::string& error) {
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
@@ -207,6 +234,8 @@ bool LoadMobileGL(const Request& request, std::string& error) {
setenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D", dumpPoints.c_str(), 1);
}
ApplyEnvOverrides(request.envOverrides);
void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL);
if (handle == nullptr) {
const char* dlError = dlerror();
@@ -63,6 +63,11 @@ struct Request {
bool deriveNumSubgroups = false;
bool iterationRPFixBarrier = false;
int holdMs = 0;
// Generic environment passthrough, each entry `KEY=VALUE` (an entry with no '='
// unsets KEY). Applied last, right before libMobileGL.so is loaded, so a knob that
// has no dedicated field above can still be forwarded from the CI script without
// touching this struct again.
std::vector<std::string> envOverrides;
};
struct Result {
@@ -1,5 +1,8 @@
#include "trace_replay_core.hpp"
#include "spawn_spike.hpp"
#include "trace_env_overrides.hpp"
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <jni.h>
@@ -26,22 +29,7 @@ std::string ToString(JNIEnv* env, jstring value) {
return out;
}
std::vector<std::string> SplitSemicolonList(const std::string& value) {
std::vector<std::string> values;
std::size_t begin = 0;
while (begin < value.size()) {
const std::size_t end = value.find(';', begin);
const std::string entry = value.substr(begin, end - begin);
if (!entry.empty()) {
values.push_back(entry);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
return values;
}
using mobilegl_trace::SplitSemicolonList;
jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) {
jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult");
@@ -129,7 +117,8 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean benchmarkMode,
jint benchmarkTailFrames,
jboolean benchmarkFinish,
jstring benchmarkResultPath) {
jstring benchmarkResultPath,
jstring envOverrides) {
mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath);
request.goldenPath = ToString(env, goldenPath);
@@ -166,6 +155,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
: mobilegl_trace::kDefaultBenchmarkTailFrames;
request.benchmarkFinish = benchmarkFinish == JNI_TRUE;
request.benchmarkResultPath = ToString(env, benchmarkResultPath);
request.envOverrides = SplitSemicolonList(ToString(env, envOverrides));
ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height);
@@ -196,3 +186,20 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
}
return MakeResult(env, result);
}
// P0 spike A: exec the packaged MobileGLServer stub from this app process and report what
// happened. Deliberately a separate entry point rather than another parameter on the
// replay call - it shares nothing with a replay, and the trace lane must be able to run
// it without a trace, a golden or a surface.
extern "C" JNIEXPORT jstring JNICALL
Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunSpawnSpike(JNIEnv* env,
jclass,
jstring serverPath,
jstring markerPath) {
mobilegl_trace::SpawnSpikeRequest request;
request.serverPath = ToString(env, serverPath);
request.markerPath = ToString(env, markerPath);
const mobilegl_trace::SpawnSpikeResult result = mobilegl_trace::RunSpawnSpike(request);
return env->NewStringUTF(result.message.c_str());
}
@@ -54,6 +54,14 @@ public final class TraceReplayActivity extends Activity {
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
));
// P0 spike A: when asked, exec the packaged server stub out of nativeLibraryDir
// instead of replaying anything. This mode needs no trace and no render surface.
String spikeLibrary = spawnSpikeLibrary(intent);
if (spikeLibrary != null) {
runSpawnSpike(spikeLibrary);
return;
}
SurfaceHolder holder = surfaceView.getHolder();
if (request.width > 0 && request.height > 0) {
holder.setFixedSize(request.width, request.height);
@@ -123,7 +131,8 @@ public final class TraceReplayActivity extends Activity {
request.benchmark,
request.benchmarkTailFrames,
request.benchmarkFinish,
request.benchmarkResultPath
request.benchmarkResultPath,
request.envOverrides
);
Log.i(TAG, result.toString());
TraceReplayResult finalResult = result;
@@ -163,9 +172,57 @@ public final class TraceReplayActivity extends Activity {
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath
String benchmarkResultPath,
String envOverrides
);
// ---------------------------------------------------------------------------
// P0 spike A: prove an APK can ship a second native executable and exec it.
//
// The exec has to happen here, in the application's own process: an `adb shell
// run-as` invocation runs in a different SELinux domain, so it can succeed while
// the real app is denied. The child reports the domain it ended up in, and the
// parent reports the domain it spawned from, so the log line stands on its own.
// ---------------------------------------------------------------------------
private static final String EXTRA_SPAWN_SPIKE = "mobilegl_spike_spawn";
private static final String DEFAULT_SPAWN_SPIKE_LIBRARY = "libMobileGLServer.so";
private static String spawnSpikeLibrary(Intent intent) {
if (!intent.hasExtra(EXTRA_SPAWN_SPIKE)) {
return null;
}
// Accepts --ez (boolean, arrives as a null string) and --es with either a truthy
// marker or the library file name to exec.
String value = intent.getStringExtra(EXTRA_SPAWN_SPIKE);
if (value == null || value.isEmpty() || "1".equals(value) || "true".equals(value)) {
return DEFAULT_SPAWN_SPIKE_LIBRARY;
}
return value;
}
private void runSpawnSpike(String libraryName) {
// The surface callbacks fire regardless; this keeps them from starting a replay
// underneath the spike.
started = true;
File outputDir = new File(request.outputDir);
String serverPath = new File(getApplicationInfo().nativeLibraryDir, libraryName)
.getAbsolutePath();
String markerPath = new File(outputDir, "spike-spawn.txt").getAbsolutePath();
statusView.setText("Running spawn spike\n" + serverPath);
new Thread(() -> {
outputDir.mkdirs();
String message = nativeRunSpawnSpike(serverPath, markerPath);
Log.i(TAG, message);
runOnUiThread(() -> {
statusView.setText(message);
finish();
});
}, "MobileGLSpawnSpike").start();
}
private static native String nativeRunSpawnSpike(String serverPath, String markerPath);
private static final class TraceReplayRequest {
final String tracePath;
final String goldenPath;
@@ -199,6 +256,9 @@ public final class TraceReplayActivity extends Activity {
final int benchmarkTailFrames;
final boolean benchmarkFinish;
final String benchmarkResultPath;
// Generic environment passthrough, `K=V;K=V`. A future MOBILEGL_* knob needs no
// new intent extra, no new JNI parameter and no new field beside this one.
final String envOverrides;
private TraceReplayRequest(
String tracePath,
@@ -229,7 +289,8 @@ public final class TraceReplayActivity extends Activity {
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath
String benchmarkResultPath,
String envOverrides
) {
this.tracePath = tracePath;
this.goldenPath = goldenPath;
@@ -260,6 +321,7 @@ public final class TraceReplayActivity extends Activity {
this.benchmarkTailFrames = benchmarkTailFrames;
this.benchmarkFinish = benchmarkFinish;
this.benchmarkResultPath = benchmarkResultPath;
this.envOverrides = envOverrides;
}
static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) {
@@ -296,7 +358,8 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("benchmark", false),
intent.getIntExtra("benchmark_tail_frames", 200),
intent.getBooleanExtra("benchmark_finish", true),
benchmarkResultPath
benchmarkResultPath,
readString(intent, "mobilegl_env", "")
);
}
+14
View File
@@ -32,6 +32,7 @@ Usage:
[--avoid-angle-llvmpipe-explicit-lod-bias] \
[--coherent-as-flush] \
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
[--env "K=V;K=V"] \
[--benchmark] \
[--benchmark-tail-frames N] \
[--benchmark-finish 0|1] \
@@ -60,6 +61,11 @@ copies benchmark.json (per-frame times plus mean/median/p95) out of the app, and
"passed" only means the replay reached the end of the trace without an error.
Pass --reuse-fixture to skip re-extracting and re-pushing the trace, for repeat
runs of a case whose fixture is already in /data/local/tmp.
Pass --env "K=V;K=V" (or set MOBILEGL_TRACE_ENV) to hand arbitrary environment
variables to the replay process. They are applied last, immediately before
libMobileGL.so is loaded, so they override every flag above; an entry with no "="
unsets the variable instead. This is the generic passthrough: a MOBILEGL_* knob
that has no flag of its own needs no plumbing to be forwarded.
EOF
}
@@ -119,6 +125,7 @@ avoid_angle_llvmpipe_sampler_mipmap_min_filter=0
avoid_angle_llvmpipe_explicit_lod_bias=0
coherent_as_flush=0
texture_2d_dumps=""
env_overrides="${MOBILEGL_TRACE_ENV:-}"
benchmark=0
benchmark_tail_frames=200
benchmark_finish=1
@@ -164,6 +171,7 @@ while [ "$#" -gt 0 ]; do
;;
--coherent-as-flush) coherent_as_flush=1; shift 1 ;;
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
--env) env_overrides="$(next_arg "$@")"; shift 2 ;;
--benchmark) benchmark=1; shift 1 ;;
--benchmark-tail-frames) benchmark_tail_frames="$(next_arg "$@")"; shift 2 ;;
--benchmark-finish) benchmark_finish="$(next_arg "$@")"; shift 2 ;;
@@ -400,6 +408,12 @@ run_retrace() {
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
if [ -n "${env_overrides}" ]; then
# adb joins the argv with spaces and hands the result to the device shell, so a value
# holding the ';' that separates entries would otherwise be read there as a command
# separator. The single quotes make it one token again.
set -- "$@" --es mobilegl_env "'${env_overrides}'"
fi
if [ "${benchmark}" -eq 1 ]; then
set -- "$@" --ez benchmark true
set -- "$@" --ei benchmark_tail_frames "${benchmark_tail_frames}"
+17
View File
@@ -5,6 +5,16 @@ def mobileGlLogActiveLevel = {
}
def standalonePluginBuild = rootProject.name == 'MobileGLPlugin'
// P0 spike A opt-in. OFF by default: the spike binary is dead weight in every trace APK
// that is not running the spike, and a flavour that silently carries an extra executable
// is exactly the kind of thing nobody notices until it ships. Turn it on for the spike
// build only, with either
// ./gradlew :app:assembleTraceDebug -Pmobilegl.buildServerSpike=ON
// or MOBILEGL_BUILD_SERVER_SPIKE=ON in the environment.
def mobileGlBuildServerSpike = {
(rootProject.findProperty('mobilegl.buildServerSpike') ?: System.getenv('MOBILEGL_BUILD_SERVER_SPIKE') ?: 'OFF') as String
}
android {
namespace 'top.mobilegl.mobilegl'
compileSdk 34
@@ -53,6 +63,13 @@ android {
externalNativeBuild {
cmake {
arguments '-DMOBILEGL_TRACE_ANGLE_VARIANTS=ON'
// P0 spike A: the second native executable that proves an APK
// can ship one and exec it from nativeLibraryDir. Off unless
// asked for, and only offered here - the shipping plugin
// flavour cannot turn it on at all, so no released artifact can
// grow a binary nothing loads. See mobileGlBuildServerSpike
// above for the two ways to enable it.
arguments "-DMOBILEGL_BUILD_SERVER_SPIKE=${mobileGlBuildServerSpike()}"
}
}
}
+601
View File
@@ -0,0 +1,601 @@
# MGPipe 设计与架构
> 本文描述**已决定**的设计。每条决定附一行理由;数字凡有实测的取实测(见 `MEASUREMENTS.md`)。落地状态以 `feat/disaggregated@458ccde1` 为准:标注"P0 已落地"的是树里的代码,其余是后续阶段要实现的形状(阶段号见 `ROADMAP.md`)。
## 1. 边界
### 1.1 一句话
`MG_Backend` 已经是一台贴着目标 API 的状态机(Espryt 有逐字节的渲染状态镜像、6 个 twin registry、三条 persistent ringMagma 有 `SetupDrawSnapshot`、pipeline memo、5 个 `Vk*Manager`)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化**推**过去,后端不再拉 `MG_State::pGLContext`。server 进程因此只装 `MG_Backend` + MGPipe 对象表,不链接 `MG_State``MG_Impl`、glslang。
接口不是从 gallium 自顶向下设计的,而是从两个后端自己维护的关键结构反推出来的:`SetupDrawSnapshot` 的字段并集 → `set_*` 组;`DrawTextureSyncKeys``set_sampler_views`+`create_sampler_view`+`set_texture_params``ResolvedDrawBuffers`/`ResolvedVertexBindings` → vertex elements 三件;`g_syncedRenderStateParameters` → render-state CSO`UnpackStagingBlock``MGPSubData` 的 region 形状;`BufferBackendOps`7 个 hook,注释自称 `pipe_context` 类比)→ `resource_*` 全族。gallium 是目的地(词汇可读、可迁移),不是推导前提;与 gallium 的十条偏离见 §3.5。
### 1.2 两张函数指针表
`MGPipeScreen`share-group 作用域:caps、resource、persistent map、fence)与 `MGPipeContext`(其余全部:query 命名空间、CSO、`set_*`、对象操作、verb),由 `PipeCalls.def` 经 G1 生成(`MG_Pipe/generated/PipeTables.inc`)。**P0 已落地。**
- 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(`gBackendFunctionsTable`);**null 项已经表示"未实现,前端回退"**,正好就是"这个子系统还没迁移,继续拉取";`MG_Test` 已用替换整张表的方式 mock 后端。
- 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flow`pGLContext` 是进程全局,share group 全库无人读取)。
- EGL 生命周期 8 项与 caps 面留在 `pActiveBackendObject` 的虚函数上(罕见路径)。
### 1.3 三种形态,一份后端
| 形态 | 表里装的是什么 | 用途 |
|---|---|---|
| `monolith`(默认) | backend 自己的函数;`MGPipeCallbacks` 是对 `MG_State` 的直调;`MGHostSpan.Ptr` 指向 client shadow(零新增拷贝) | 出货 |
| `inproc` | 发射器 → 同进程第二个线程上的 applier | CI 形态;同时就是 monolith 的**渲染线程**(把 `PrepareForDraw` 与驱动调用搬离 GL 线程,是本项目手上最大的单一 CPU 杠杆) |
| `spawn` | 发射器 → SPSC shm ring → 另一个进程的 applier → 同一批 backend 函数 | 两进程出货形态 |
唯一 hook 点是 `MG_Backend::Init()``MG_Backend/Init.cpp`)里一个 `#if MOBILEGL_BUILD_DISAGGREGATED` 分支:`MG_Config::Transport != Monolith` 时装 `MG_Remote::BackendObject_Remote`,否则走今天的 `switch`。下游 `MG_Impl` 的边界调用点零 `#ifdef`。(分支在 P5 落地;P0 的 `Init.cpp` 尚未含它。)
## 2. 对象模型
### 2.1 句柄 = `{slot, gen}`P0 已落地,`MG_Pipe/MGPipeHandles.h`
- 8 字节 POD,按值走寄存器对;**client 铸造,server 永不返回句柄** → 整份目录零创建 round trip(对 gallium 的偏离 D1)。
- slot 稠密、**按 kind 分配**free list + 高水位),server 对象表是数组而非哈希表。与 `IndexGenerator` 无关——后者的 LIFO 名字复用正是句柄要关掉的问题。
- `gen` 只在 slot 复用时 ++,不在 respecify 时 ++;同一 slot 复用 2³² 次才回绕(1000 fps 逐帧复用约 50 天),debug 分配器断言回绕。
- kind`Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context`
- 保留句柄:`{0,0}` = null`{0,1}` of `Framebuffer` = 默认帧缓冲(退役 Espryt 四处 `pDefaultFramebufferInfo->defaultFBO` 身份比较);`ShaderCso` slot 空间的高 1/16 保留给 program pipeline 合成体(`MobileGL/MG_Pipe/MGPipeHandles.h:88-90`)。
- GL name 只以 `GlNameForDiag` 出现在 `MGPResourceDesc` 里,永不做身份、永不进 memo 键或 content hash`GetLifetimeId()` 留在 client 作 tracker 自己的身份,client 维护 `lifetimeId → slot`
### 2.2 两种世代,严格分开
| | 拥有者 | 回答 | 过线 |
|---|---|---|---|
| `MGPipeHandle::Gen` | client | "还是同一个 GL 对象吗?" | 是 |
| `MGGen``g_bufferMutationEpoch``m_textureImageEpoch``m_cacheStructureEpoch` 等 12 个后端纪元) | server | "我自己是否重铸了驱动对象?" | **永不**server→client 只以纹理拉取请求出现(§8.4) |
规范:任何 MGPipe 调用不得要求 client 提供或知晓 `MGGen`;反过来,client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明——过线时要么加宽、要么与 `{slot, gen}` 同行。
### 2.3 CSO 与可变对象
| 类别 | 形态 | 对应后端已有缓存 |
|---|---|---|
| `VertexElementsCso` | create/bind/delete | `VertexInputStateFactory::m_cache` |
| `SamplerCso` | create/delete + `bind_sampler_states` | `VkSamplerManager::m_samplers``BackendSamplerObject` |
| `SamplerViewCso` | create/delete + `set_sampler_views` | `TextureResource::{perMipViews,…}``SyncTextureViewToBackend` |
| `ShaderCso` | create/bind/delete + server 侧惰性特化 | `ProgramFactory::m_cache``BackendProgramObjectImpl` |
| `RenderStateCso` | create/bind/delete,身份 = pipeline 子集 | Espryt 值镜像;Magma `ComputePipelineStateHash` |
| Buffer / Texture / Renderbuffer | create / respecify / subdata / destroy | 各自 twin |
| Framebuffer / Xfb | per-context 身份 + `set_*` payload | `BackendFramebufferObject``m_xfbCounterSlotByObject` |
CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
## 3. 调用目录(P0 已落地)
### 3.1 单一真相源
`MobileGL/MG_Pipe/PipeCalls.def`:一行一个调用 `X(Name, PayloadStruct, Class, Flags)`。**线上 opcode 就是行在文件里的位置**(1-based),所以目录必须是唯一记录的集合,新调用只能**追加**到文件末尾、退役的调用保留槽位。`MGP_CALL_LIST_DOCUMENTED_COUNT = 71``MobileGL/MG_Pipe/PipeCalls.def:69`)由 `MG_Test/Pipe/PipeCatalogueTest.cpp` 钉住。
七个生成器(`scripts/gen_pipe.py`,产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`):
| | 产物 | 内容 |
|---|---|---|
| G1 | `PipeTables.inc` | 两张函数指针表 |
| G2 | `PipeThunks.inc` | monolith 直调 thunk `MGP_<Name>()``MG_Impl` 的约 93 个 `gBackendFunctionsTable.GL.*` 站点逐名改到它上面 |
| G3 | `PipeWire.inc` | wire 记录 + 每种一条尺寸 `static_assert` + applier 分发前的运行期边界检查 → `Fatal{ProtocolCorruption}` |
| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表来自 `PipeFields.def`;浮点按位比较,NaN patch level 不会误报) |
| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id61 个)与逐 verb 世代 poison |
| G6 | `PipeCoverage.inc` | 477 行后端读点清单 → MGPipe 调用的映射(`Coverage.def` 手工维护一半):299 → 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED** |
| G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2 |
### 3.2 分组与计数
| Class | 条 | 内容 |
|---|---|---|
| `kScreen` | 11 | `GetCaps`(R)、`ResourceCreate/Respecify/Destroy``MapPersistent`(R,O)/`UnmapPersistent`(O)、`FenceCreate/Status(R)/Wait(R)/Destroy`、追加的 `FenceWaitServer``glWaitSync`GPU 侧等待) |
| `kCtxQuery` | 8 | `QueryCreate/Begin/End/Available(R)/Result(R)/Destroy`、追加的 `QueryTimestamp`(R)`glGetInteger64v(GL_TIMESTAMP)`)与 `QueryCounter``glQueryCounter` |
| `kCtxCso` | 13 | create/delete × {render state, vertex elements, sampler, sampler view, shader} + bind × {render state, vertex elements, shader}sampler 与 sampler view 的绑定是下一组的批量调用 |
| `kCtxState` | 17 | `SetDynamicState`(B)、`SetFramebufferState``SetVertexBuffers`(V)、`SetIndexBuffer``SetIndirectBuffers``SetSamplerViews`(V)、`BindSamplerStates`(V)、`SetShaderImages`(V)、`SetShaderBuffers`(V,H)、`SetStreamOutputTargets`(V)、`SetGlobalConstants`(B)、`SetVertexAttribDefaults`(V)、`SetPixelPackState``SetPatchState``SetDrawProgram``SetDispatchProgram`、迁移期临时的 `SetResidualValueState`(B) |
| `kCtxObject` | 9 | 按资源寻址:`SetTextureParams``ResourceSubData`(B,V)、`BufferSubDataResident`(B,O)、`ResourceSubDataComplete``ResourceFlushRange``ResourceReadback`(R)、`ResourceCopyRegion``GenerateMipmap``GetTextureImage`(R) |
| `kCtxVerb` | 13 | 按上下文寻址:`Blit``Clear``ReadPixels`(R)、`DrawVbo`(H,V)、`LaunchGrid``MemoryBarrier``Begin/End/Pause/ResumeStreamOutput``Flush``Present``SetSwapInterval`(O) |
Flags`kNeedsAck`(调用方等 server 确认;目录里目前无条目携带,见 §8.3)、`kHasBlob`(B)、`kVarTail`(V)、`kHostSpan`(H)、`kReplySlot`(R,答进 `MGPReplySlot`,永不阻塞)、`kOptional`(O,后端表里可为 null:Magma 故意不注册 `BufferSubDataResident``SetSwapInterval`)。
- 今天 20 个 draw 入口塌成 `DrawVbo` 一条,`MGPDrawRange[]` 就是 `MultiDraw*` 族今天的形状;`Clear` 一条判别式合并 `glClear` + 4 个 `glClearBuffer*` + 4 个 `glClearNamedFramebuffer*`
- `SetSamplerViews` / `BindSamplerStates` **没有 stage 维度**:MobileGL 的纹理单元空间是合并的(`TextureState::m_textureUnits` 是 192 个单元的一个数组,每 stage 32 只是广告数字),同一单元可被两个 stage 采样;stage 只在目标 API 需要时由 server 从反射归档推导。
- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 FBO attachment / image 单元 / `glCopyImageSubData` 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数,且 `RequireImageBindableStorage` 需要在前端参数版本不动时强制重同步。
- `SetIndexBuffer` 独立于 VAO 配置版本(D5):索引 slot 重绑不移动 VAO config version。
- `SetGlobalConstants` 只覆盖默认 uniform blockD6):`globalUboScratch` 是 link phase B 的 CPU 数组,没有 GL name、没有 `BufferObject`
**显式不移植**`GetIntegeri_v`/`GetInteger64i_v`/`GetProgramiv`(后两项 P0 已从 `GLFunctionsTable` 删除,`50815a23`;唯一属于后端的带下标答案 `GL_MAX_COMPUTE_WORK_GROUP_COUNT/SIZE``MGPCaps``e8ee7b1a``GL_COMPUTE_WORK_GROUP_SIZE` 是前端反射查询)、`ShaderStorageBlockBinding`(折进反射归档)、`set_pixel_unpack_state`(不存在:前端已在 `glTexImage` 时解析压缩格式、强制默认 unpack)、压缩格式概念、`pipe_transfer`
### 3.3 能力位(`MGPCapBit`
`kCapViewportArray``kCapFloat64VertexAttrib``kCapResidentSubData``kCapCpuXfbPrimitiveAccounting``kCapTimerQuery``kCapOcclusionQuery``kCapXfbPrimitivesQuery``kCapNeedsHostIndexBytes`server 做 restart 重写 / multi-draw 展平,split 下开启索引宿主镜像,§10.3)、`kCapNeedsHostUboBytes`server 把具名 UBO 打进自己的 ring,需要 `SetShaderBuffers` 的 host payload)。`CallMask` 取代"槽位是否为 null"这个隐式能力探测。
不存在 `kCapPrimitiveRestart` / `kCapMultiDraw*` 一类"归属开关"D-B7):`ResolveTierForBatch` 逐 batch 用 `programReadsDrawID`(转译后 ESSL 的性质,只存在于 server)选档,两个后端都做 restart 重写,所以这类归属不可用 cap 表达。规则一句话:**multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。**
一个待转显式能力位的现有陷阱:`GL_Drawing.cpp``EndTransformFeedback` 槽位的非空当作"后端按 GL 顶点序捕获"来跳过 `FixupGsStripCaptureOrder`。MGPipe 下改为显式 `kCapDriverOrderedXfbCapture` 一类的位(P8/P9)。
`MGPCaps` = `DynamicBackendParameters`(整块包含,~90 个标量含六个 compute 限制)+ `CallMask` + 两个 blobformat 能力表、renderer 字符串),握手后一次快照,取代 40 个 `pActiveBackendObject->` 站点与 89 个 caps 读点。
## 4. 记录与 payload 约定(P0 已落地,`MG_Pipe/MGPipeTypes.h`
- 每个 payload 是平坦 POD、显式 padding、`static_assert` 平凡可复制与**精确尺寸**;**永不含指针**。
- `MGPBlobRef{Offset, Size, Seg}`24 B)指向 blob 区:monolith 下 `Seg == kMGHostSpanSegNone`、Offset 是调用方 staging arena 内地址;split 下 Seg 命名传输段。
- `MGHostSpan`32 B`MG_Pipe/MGPipeHostSpan.h`)是整份接口里**唯一形状随传输而变**的东西:monolith 下 `Ptr` 指向 shadow 或应用内存;split 下 `Ptr == nullptr`、字节在 `Seg/Offset` 命名的 `SEG_STAGE`,或 `Seg == kMGHostSpanSegFromServerIndexMirror``MobileGL/MG_Pipe/MGPipeHostSpan.h:26`)表示"字节已在你那边的索引镜像里"。`MGPipeHostBytes()` 是一次可预测分支;split 解析器 `gMGPipeSegmentResolver``MG_Remote` 安装。它只进变长尾(`DrawVbo` 的用户索引、`SetShaderBuffers` 的具名 UBO 字节),永不内联进定长 payload——VBO 路径(MC/Sodium 的全部 draw)不为它付字节。
- 变长记录(`kVarTail`)= 定长前缀 + 自描述长度的内联尾巴;`kHasBlob` 记录额外校验 `BlobRef` 落在其声明的段内。运行期边界纪律:`SEG_CMD` 是对端并发写入的区域,`static_assert` 管不到运行期损坏,违反一律 `Fatal{ProtocolCorruption}`
- wire 记录头 `MGPWireRecHeader{Op:u16, Flags:u16, Size:u32}`(8 B),Size 含头、8 字节倍数;**没有逐记录序号字段**——seq 就是记录序数(producer `m_emitSeq++` / consumer `m_applySeq++`)。
- **分块上界 = ring 容量的一半**`RingProducer::MaxRecordBytes()`):这是每个 head 偏移都能放下的最大记录(wrap pad 最多花 total8 字节),超过它的 payload(大 `ResourceSubData``CreateShaderState` 归档)由发射器切成多条;ring 对更大的记录直接拒绝(nullptr + `MGLOG_E`)而不是让 producer 等一个永远不够的空闲量。
- `MGPSubData` 的 buffer 半边:`Target == Buffer` 时没有 level 与 box,目的字节范围搭在 `UnionBox.X`offset)与 `UnionBox.W`size)上,`MGPipeSetSubDataBufferRange()` 是唯一拼写;单条记录上限 offset 2³¹−1 / size 2³²−1,越界由发射器拆分。
### 4.1 关键 payload
| payload | 尺寸 | 要点 |
|---|---|---|
| `MGPResourceDesc` | 88 | buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;`BindMask``ELEMENT_ARRAY` 位是索引镜像的开关;`ImageBindableHint` 预防性分配 image-bindable 存储;`ViewOf` 是纹理视图的存储属主(server 侧 keep-alive);`BufferForTexBuffer/BufOffset/BufSize` 实时解析(`kMGPipeWholeBuffer = ~0`)。Renderbuffer 保持独立类(自己的 format-capability target、`ComponentSizes`、twin |
| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / **12** / 32 | §5.3 |
| `MGPVertexElements` | 40 | blob 同时带解析后的 `VertexAttribute[]` **和** `VertexBufferBindingPoint[]`,缺一不可(pointer 调用的 stride 0 = element sizebinding 模型的 stride 0 = 每顶点读同一 element);`IsLong``Type == Float64` 分开携带;仅供查询的 `LegacyStride/LegacyPointer` 留在 client |
| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`**(三种 border color 表示永远都被数值填满,没有它后端无法在 `Iiv`/`fv``VkBorderColor` 家族间选择) |
| `MGPSamplerView` / `MGPTextureParams` | 36 / 32 | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上 |
| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + 四个状态字节,§7 |
| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**(按结构消灭 read-buffer-shared-FBO 缺陷类);`MGPSurface::InternalFormat` 内联(四个跨对象 mask 推送时零查表);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器 |
| `MGPSubData` / `MGPSubRegion` | 72 / 40 | §6 |
| `MGPDrawInfo` / `MGPDrawRange` / `MGPDrawIndirect` | **56** / 12 / 40 | `Flags` 门控 `MinIndex/MaxIndex`(只在 client-memory 数组路径算)与 `XfbCpuCapturedVertices`(只在 XFB scatter 路径读)——不是每 draw 都算;`NumDraws``MGPDrawRange` 在变长尾;用户索引的 `MGHostSpan` 只在 `kDrawHasUserIndices` 时进变长尾;indirect 的 `DrawCount` 由 client 解析,server 永不读 indirect 命令块来数 draw |
| `MGPShaderBuffers` / `MGPBufferRange` | 32 / 24 | range 不内联 host span`kCapNeedsHostUboBytes` 下 Uniform 类带第二个变长尾 `MGHostSpan[HostSpanCount]`,与 range 数组下标对齐 |
| `MGPPixelPackState` | 28 | 只有 PACK 方向(D5 |
| `MGPPatchState` | 40 | 同时是 shader variant 输入 |
| `MGPClear` | 48 | Whole / Color / Depth / Stencil / DepthStencil 判别式 |
| `MGPGlobalConstants` | 40 | `(ShaderCso, Version)` 键控,每 program 每帧至多一次 |
| `MGPSubDataComplete` | 24 | 纹理拉取的正向终止符,可携带零个 region |
| `ResidualValueBlock` | **1248** | 迁移期 Track V 载体,§9.4 |
每条 `kVarTail``set_*``SetVertexBuffers``SetSamplerViews``BindSamplerStates``SetShaderImages``SetShaderBuffers``SetStreamOutputTargets`)都带 `ContentHash`——与 `MGPFramebufferState` 同一模式,hash 未变就不发(§5.4)。
## 5. 前端 state tracker`MG_Impl/Pipe/Tracker`P2 起)
### 5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里
Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住(Espryt 代码自己标它为最热路径),per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium `st_validate_state`
八个 validate 入口,由 `PipeCalls.def``kCtxVerb`/`kCtxObject` 条目生成:`ValidateForDraw`20 个 draw 入口)、`ValidateForDispatch``ValidateForClear``ValidateForBlitOrCopy``ValidateForTextureOp`GenerateMipmap / CopyTex* / BindImageTexture)、`ValidateForReadback``ValidateForXfbSpan``ValidateForQuery`。八个而不是四个,因为 `MG_Impl` 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、query/sync)很多自己就读 `pGLContext`
**只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。
### 5.2 dirty 位:值类零新增记账,对象类新增 5 个聚合世代
| dirty 位 | 类 | 快门来源 |
|---|---|---|
| `NEW_RENDER_STATE` / `NEW_PIPELINE_STATE` | 值 | `m_version` / `m_pipelineStateVersion` |
| `NEW_PIXEL_PACK``NEW_PATCH_STATE``BitwiseEqual`NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS``NEW_VERTEX_ELEMENTS`VAO config version | 值 | 既有计数器 |
| `NEW_SHADER``NEW_SHADER_BINDINGS``NEW_GLOBAL_CONSTANTS` | 值 | link/image-unit/backend-state/block-binding/uniform-write-set/UBO-content 版本 |
| `NEW_VERTEX_BUFFERS` | 对象 | **`VertexArrayState::m_anyVaoAttributeGeneration`**(新增)→ 命中后走 32 属性前缀 |
| `NEW_INDEX_BUFFER` | 对象 | 索引 slot 版本 + 绑定对象 `{slot,gen}` |
| `NEW_FRAMEBUFFER` | 对象 | **`FramebufferState::m_anyAttachmentGeneration`**(新增)+ 对象/slot 版本 → 重算 `ContentHash` |
| `NEW_SAMPLER_VIEWS``NEW_SAMPLERS``NEW_SHADER_IMAGES` | 对象 | **`TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration`**(新增)+ bind/sampling-resolution generation → 走 `GetMaxTouchedUnit()` 前缀、重算集合 hash |
| `NEW_CONST_BUFFERS` / `NEW_SHADER_BUFFERS` / `NEW_SO_TARGETS` | 对象 | **`BufferState::m_anyBufferChangeGeneration`**(新增)→ 走 `GetTouchedBindPointCount()` 前缀 |
五个聚合世代全部落在既有 bump 点上(约 20 行),把对象类组的快门从"每 validate 走查 192 单元 / 84×4 绑定点 / 32 属性 / 40 attachment"降成一次 `Uint64` 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了",这正是 Magma 不得不用有损 `sampledContentSum` 的原因)。
完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举 `MG_Impl/GLImpl` 里每个 mutator → 必须 bump 的聚合世代,CI 重生成 + `git diff --exit-code`,未映射即失败(P1 起成为门)。**实测规模**:926 次 mutator 调用落在 73 个不同 mutator 上,其中 92 次(7 个 mutator,绝大多数 `RecordError`)位于同函数内也会到达后端的"即时发布点",其余 834 次由紧随其后的 verb 发布——映射表是 73 条目的问题。
三个回绕 `Uint16` 在 tracker 边界加宽(`m_lastPushed[]` 是 tracker 自己的字段,不改 `MG_State`);回绕在 tracker 本地无害(多一次重推,永不漏推),且被集合 hash 抑制器吞掉。
### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集,动态状态单独走(D-B1)
```
create_render_state(cso, MGPBlobRef pipelineSubsetChunks) // 只带 pipeline 子集
bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B
set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化 chunk
```
- 整块的理由:`RenderStateParameters` 是平凡可复制 POD,Espryt 自己 `static_assert` 并做 head/blend/tail 三段 memcmp**字段顺序承重**(`ScissorBoxWrittenMask``ClipDistanceEnabledMask` 故意放在 tail 段);拆成 blend/depth-stencil/rasterizer 三个 CSO 要手工维护 ~150 字段划分表且无绊线。
- 子集身份的理由:整块内容寻址会让 `glViewport`/`glScissor`/`glBlendColor`/`glClearColor` 每次铸造新 CSO、冲掉 server 的 pipeline memo——`RenderState.h` 记录的那次回归。`RenderState.cpp` 里 viewport/scissor/line-width 族只 `++m_version``SET_CAPABILITY` 与 pipeline 相关 setter 才 `BumpVersions()`
- 动态子集:viewport、scissor、depth range、blend color、line width、polygon offset、stencil ref/write mask、clear 值、sample coverage、hints、point-size 族。
- 划分只写在一处:`MG_Pipe/MGPipeRenderStateSpans.{h,cpp}`P2)的 chunk 表 + `MGPipeComputePipelineSubsetHash()`,从 Magma 的 `ComputePipelineStateHash` 搬来,client 与两个后端共用;G7 的 `MG_Test` 遍历每个 `RenderState` public setter,断言 `pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变`
- server 侧:每 context 一份 working `RenderStateParameters`1168 B),`bind``set_dynamic_state` 各把自己的 chunk 散射进去。**Espryt 的 `SyncRenderState`693 行)拿到的仍是 `const RenderStateParameters&`,单 `Uint16` 早退、三段 memcmp 一行不动**Magma 的 pipeline memo 键是 `cso.slot`,动态尾巴仍走 `ApplyDynamicDrawStateTail`。Espryt 的 head/blend/tail 划分(驱动侧增量)与 pipeline/dynamic 划分(线上与身份)是两回事,并存、各有绊线。
- client 取值顺序:`m_pipelineStateVersion` 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 pipeline 子集算 xxHash~25-30 字,Magma 今天就在算)→ CSO map 探测 → 命中发 12 B bind,未命中发变化 chunk 的 create 再 bind`m_version` 变而子集未变 → 只发 `set_dynamic_state`~200 B)。
- `FramebufferSrgb``DepthClamp` 今天**没有存储**`glEnable` 被静默吞掉且不报错,六个后端读点恒为 false);chunk 表冻结前要补真存储并把 `FramebufferSrgb` 划进 pipeline 半边(它改变 attachment/blend 的解释)——待拍板,见 `ROADMAP.md`
### 5.4 验证不变式、合并与抑制器
规范(D-B3):**一条 verb 的全部 `set_*`/`bind_*` 必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态惰性特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,`set_*` 之间没有顺序要求。** 推荐实现顺序(framebuffer → program → 纹理/sampler/image/buffer/global constants → render state/dynamic → vertex elements/buffers/index/attrib defaults → patch/XFB → verb)只是代码组织,不是契约。退役 Espryt 的 fragColor 重推导 workaround、`g_broadcastMemo*``ImageUnitFormatsStillMatch` 的机制是惰性特化,不是调用顺序。
`create_shader_state` 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server——monolith 拿不到的异步收益。
四条合并规则:整块结构优于逐字段;高水位标记(`GetTouchedBindPointCount``GetMaxTouchedUnit`)留在 tracker 走查里,直接就是 `count` 实参;只发 program 解析过的集合(`uniformSamplerOrImageUnitIndex`);**集合 hash 抑制器**——每条 `kVarTail` `set_*` 在 client 算已解析集合的 xxHash,未变不发。最后一条是从后端搬到 client 的 ~175 行去抖(`UnitBindingsSnapshot`/`PairingsIntact`/`g_fboTextureSyncList` 族)的载体:`GetTextureBindGeneration()` 在冗余重绑时也 bump(MC 26.2 每次纹理单元切换都重绑同一个 sampler),没有抑制器每个 batch 都会重发一条几百字节的变长记录并冲掉 server 的两个 memo。
索引绑定范围在 validate 时刻实时解析(`glBindBufferBase` 之后再 `glBufferData` 是普通应用代码)。
### 5.5 sampler view 在 client 侧解析
GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 sampler uniform 类型、mipmap 完备性(`IsMipmapCompleteForFilter``SamplesAsIncompleteTexture`)与 `IsUndefinedDefaultTexture`。gallium 的"每槽一个 view"就是解析后的形态,解析留在 client 并带自己的 memo(~40 行搬迁)。两处后端特定后处理留在 server、作用于已解析集合:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。
### 5.6 生命周期、共享组、composite program
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义;`resource_destroy` 在析构时发。三条顺序约束由 payload 表达:view 先于存储属主销毁(`ViewOf` + server keep-alive)、FBO attachment 钉住纹理(surface handle 隐含 keep-alive)、buffer texture 钉住 buffer`BufferForTexBuffer`,范围实时解析)。
- 共享组:v1 一个 screen、一个 context、一条 flow`eglMakeCurrent` 是 flow 所有权转移,在既有 `EGLOperationMutex` 下发射(顺手让 `ReleaseThread``SwapInterval` 也取该锁)。
- program pipeline 合成体:`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handleslot 从 `ShaderCso` 保留高位段分配,pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。
### 5.7 emulation 的归属
规则:**驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。** 只有三个"读前端字节的纯 CPU 变换"下放到 client。
| emulation | 归属 | 过线的是什么 |
|---|---|---|
| client 顶点数组(`(first+count-1)*stride+elementSize` | client | 字节(`MGHostSpan`),永不是指针 |
| 最大索引扫描(`TryComputeMaxIndexFromHostBytes`,唯一无界的应用指针读,只有 client 同时持有两个数组) | client | `MGPDrawInfo::MinIndex/MaxIndex`flag 门控,`~0` = 未知) |
| client 索引数组 | client | 变长尾里的 `MGHostSpan` |
| `*IndirectCount` 计数解析(从 parameter buffer 的 shadow 读实际 draw 数) | client | 解析后的 `MGPDrawRange[]`(几十字节) |
| primitive-restart 重写(整 EBO 重写,`kMaxRestartRewriteBytes` = 64 MiB | **server** | 零线上流量:从索引宿主镜像读(§10.3) |
| multi-draw 五档分档 + 展平(`ResolveTierForBatch`CPU 展平是回退) | **server** | 同上 |
| viewport-array N 遍回放 | server | 无新增:16 组 viewport/scissor/depth-range 已在渲染状态里 |
| fp64 顶点窄化 | server | 原始字节;`IsLong``Type` 分开过线 |
| image-bindable 存储加宽/拆分 | server | 正向 `ImageBindableHint`;反向纹理拉取 + 终止符 |
| 生成 mipmap 的前端存储 | 拆开:client 分配 level 存储,server 生成 | `MGPMipPlan``OnMipLevelsGenerated` 只带形状不带字节 |
| CopyImage shadow 镜像 | client | 只回"拷贝成功",删掉一整条 server→client 字节通道 |
| XFB CPU 图元计数 | client | `XfbCpuCapturedVertices`flag 门控)+ `EndStreamOutput``MGPXfbAccounting` |
| XFB scatter 的 read-modify-write | client | §8.5 |
| 压缩纹理 / pixel unpack 规整 | client | 无 |
**陈旧索引纪律是逐站点表,不是一条笼统规则**(client 侧扫描/解析之前要做的 reconcile 必须逐字复现 monolith 的集合):
| client 侧动作 | 必须做的 reconcile |
|---|---|
| client 顶点数组范围计算 + 暂存 | 无(应用内存,无 GPU 写者) |
| 最大索引扫描(EBO 源) | `SyncPersistentMappedRange()` **+** `SyncGpuWrites()` |
| 最大索引扫描(client 指针源) | 无 |
| `*IndirectCount` 计数解析 | **只** `SyncPersistentMappedRange()`,不加 `SyncGpuWrites()`——monolith 今天就只做这一个,加了会给 Create/Flywheel 的每 batch 平白加一次 publish-and-wait |
| server 侧 restart 重写 / multi-draw 展平 | server 从镜像读;GPU 写者可见性由 `OnGpuWritten` 收窄集在 server 本地判定 |
前两条 client reconcile 的形态:publish → 等 `appliedSeq` → 排空事件 → 再碰 shadow。门:`ClientArrayAfterComputeWriteScenario`(去掉等待必须看到几何缺失);`create-indirect` fixture 上 `roundtrips-per-frame` 必须读零(P8)。
## 6. 纹理 subdata 与 dirty 归属
- `glTexSubImage*` 根本不调后端表:全部纹理上传由 Espryt 在 sync 时刻按**累积**区域做,那里跑 `MipmapStorage` 的 96-rect 级联合并与 `summedArea*4 >= unionArea*3` 的 union-box 回退,并在 unpack ring 可用时刻意塌成一个 box——Mali 按**作业数**给上传计价,~100 个精灵 rect 对一个 union box 实测 +6 ms/frame。逐 `glTexSubImage` 发一条记录会精确复现那个形状。
- 因此:client 在自己的 `MipmapStorage` rect 模型里累积,在**下一个 validate / flush 点**把合并后的形状作为**一条** `ResourceSubData` 发出。`MOBILEGL_PIPE_STATS` 单列逐帧发射次数与上传作业数(`TextureUploadEmissions/Box/Rect/Jobs`)。
- **同时携带 union box 与 region 列表,由 server 选上传形状**:决策留在付 GPU 代价的那一侧。实测(`MEASUREMENTS.md`):vanilla 世界同样 185 次发射,Espryt 的整 box 路径每帧 635 KB 纹素、Magma 的 rect 路径 40 KB16×。
- `MGPSubRegion` 显式携带 `SrcRowStride/SrcSliceStride``MGPSubData::SourceIsVerbatimLevelShadow` 显式携带原来由 `uploadData == mipData` 指针比较回答的问题:"这批字节是未经转换的 level shadow 吗"。split 下 client 既不发整 level 也不在 server 留整 level 镜像,指针比较不成立;Espryt 的上传路径改为从描述符取步长,`UNPACK_ROW_LENGTH``SrcRowStride/bpp` 设。形状照抄已存在的 `UnpackStagingBlock`(ring 路径本来就紧密重打包、不发 `glPixelStorei`)。
- **dirty 归属反转**client 保留 rect 模型、维护一份发射游标、发射后清自己的标志,server 从不碰 client 的标志。安全,因为 `MG_Impl` 里没有任何 `IsStorageDirty/GetStorageDirtyRects/GetStorageDirtyRegion` 调用点(前端从不读自己的 dirty 状态)。逐 level "server 权威位"与纹理 ack 协议因此不必存在。
- 发射游标按**存储属主**键控 `(storageOwnerHandle, ownerUploadTarget, ownerLevel)``TextureObjectView` 把 dirty 查询/清除全部转发给属主并做索引重映射,view 与属主共用同一份 dirty 状态。门:通过 view 上传、经属主采样(及反向),跨 draw 边界各一次。
- 后端真正在 shadow 里写字节的两处——CPU 回退生成 mipRGB16F/RGB32F)与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。
- Unpack PBO 完全在 client 解析;压缩纹理永不到达后端;`glCopyTexSubImage*``glClearTexImage` 整体留在 client(今天就是纯前端操作:借一次 `ReadPixels` 进 CPU scratch 再写 shadow),拆分后恰好是一次阻塞 ReadPixels round trip,脏区按普通 subdata 下发。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 clientSPIRV-Cross`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex``uniformBlockBinding``shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings``xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture``computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples``uniformOffsets``XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts``ResourceReflection``XfbVarying``LinkArtifacts``SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),更新 7 个 includer,加 CI `-H` 闭包断言。同批抽取 `MG_Pipe/MGPipeValueTypes.h``MAX_DRAW_BUFFERS``PerBufferBlendState``StencilFaceState``PixelStoreParameters``RenderStateParameters``SamplerParameters``BorderColorForm``VertexAttribute``VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h``RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。
- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。
- Magma 的两个内部 shaderblit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 `MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。
## 8. 反向通道
### 8.1 `MGPipeCallbacks`P0 已落地,`MobileGL/MG_Pipe/MGPipeCallbacks.h:27-51`
十个具名回调 + 一个正向终止符(`ResourceSubDataComplete`),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 `SEG_EVENT` 上的记录。
| 回调 | 取代 |
|---|---|
| `OnGlError(code)` | 6 处 `RecordError`**必须对命令流有序**,否则 `glGetError` 答错(`glGetError` 本身永远本地) |
| `OnGpuWritten(res, ranges[])` | 6 处 `MarkGpuWritten`client 在每个 draw/dispatch 发射点**保守自建** pending 集,这是**收窄**通道 |
| `OnBufferWriteback(res, offset, bytes)` | PBO 回读、XFB 捕获;**按操作级批处理**(今天两处逐行循环绝不能变成每扫描线一次 IPC);必须与 epoch bump 有序 |
| `OnTextureWriteback(res, box, bytes)` | CPU 回退生成 mip 的纹素(唯一生产者) |
| `OnTexturePullRequest(res, target, firstLevel, levelCount, pullSerial)` | §8.4 |
| `OnMipLevelsGenerated(res, base, count)` | 只带形状:monolith 的 `EnsureGenerateMipmapStorageAllocated` 也只 `AllocateStorage` + `MarkStorageDirty(false)` 不填内容,split 行为一致 |
| `OnSurfaceChanged(info)` | `SwapchainObject``pDefaultFramebufferInfo` 的分层倒置;client 自己合成 default-FB 对象 |
| `OnCapsInvalidated()` | 2 处 `InvalidateCompileEnv` |
| `OnLog(level, text)` | ≤WARN 有损,≥ERROR 无损 + 速率限制 |
| `OnXfbScatterReady(scratch, packedStride, vertices)` | §8.5 |
95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账(零消息);后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendResource` 删除(server 拥有资源表);`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payloadD-B8)。
### 8.2 有序性是正确性要求
每一次 `WritebackFromBackend` 后面都紧跟 `BumpBufferMutationEpoch()`,否则 server 的 draw-clean memo 会在 epoch 背后变陈旧——split 里这变成反向通道上的排序规则:写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。**
### 8.3 错误、ack 与日志
- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*``MarkStorageDirty`Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**`glRenderbufferStorage*` 不 ack41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。目录里目前没有条目携带 `kNeedsAck``ResourceRespecify``kNone`),标记随 P3a 的 buffer 路径落地。
- 其余错误一律晚到,走有序的 `OnGlError`
- `OnLog` 分级:≤WARN 有损(覆盖最旧 + `eventDropped` 计数);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed"`MGLOG_E_ONCE` 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。
### 8.4 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6)
server 不保留纹素,三个原因会要求重发已发过的 level:`RequireImageBindableStorage` 的 re-dirty、整格式再生、view 源重铸。四条缓解同时上:
1. **预防主因**client 给纹理打 `everImageBound``ResourceCreate/Respecify` 一直携带 `ImageBindableHint`image-bindable 存储前期分配好。
2. **拉取异步**server 发 `OnTexturePullRequest` 并把 twin 标 not-readyclient 下次 publish 时重发;阻塞的是 `mgl-srv-apply` 线程不是应用线程。
3. **有上限的保留,默认关**`MOBILEGL_PIPE_TEXEL_RETAIN_MB` 默认 0——`MipmapStorage` 保有每 level 完整 CPU 影子,拉取总能被服务,缓存买的是延迟不是正确性。只有实测拉取率非平凡才开。
4. **显式终止符**:拉取是 request/response 对,由 `ResourceSubDataComplete(res, target, firstLevel, levelCount, pullSerial)` 终止,**可携带零个 region**——内容只来自渲染、被 `CanMirrorCopyImageShadow` 拒绝的 copy、或 GPU 侧 mip 生成的 levelclient 根本没有字节;收到零 region 时 server 带着"已分配但为空"的存储继续(正是 monolith 的行为)并记 `MGLOG_W`。没有终止符 apply 线程会永久 park。
门:`TextureRemintPullScenario`(含无解用例,且在终止符落地前必须是红的);拉取次数逐 trace 用例发布。本设计从不声称"零 round trip",它测量并公布。
### 8.5 XFB scatter 搬到 client
Espryt 的 `ScatterCapturedRecords` 是对 client shadow 的 read-modify-write:从应用已有的字节起步,只把捕获到的 varying 补进去(`gl_SkipComponents` 的空洞保留应用原本的内容,`KHR-GL46.transform_feedback.capture_special_interleaved_test` 走到它)。server 没有 `MappedData()`,所以:server 把紧密打包的 scratch 通过 `OnBufferWriteback` 推给 client,用 `OnXfbScatterReady` 告知布局;client 拥有目的 shadow 与反射归档里的 varying/stride,原样跑补丁循环;补好的范围作为普通 `ResourceSubData` 重发并 bump change serial。不新增停顿类。
## 9. 后端状态机改造
### 9.1 原样不动的东西
Espryt:三条 persistent-mapped ring 与 `PersistentRing` 算法、buffer pool、7 条 fallback-repack 路径、`m_backendColorSlots` 置换表、三个 scratch FBO 及驱动侧影子、`PackState`、全部驱动绑定影子、Adreno 禁用属性 SIGSEGV workaround、Mali XFB 捕获丢失 workaround、`ScopedDefaultUnpackState`、SPIRV-Cross 会话与 post-emission ESSL 重写、驱动 POST 自检族、restart 重写与 multi-draw 五档。
Magma`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory``ProgramFactory``UniformManager` 的 ring 与描述符集、五个 `Vk*Manager``FrameContext``SwapchainObject``DynamicStateShadow``VertexInputStateFactory` 的 cache 本体、**D18 的节点式容器纪律**(`m_renderbufferResources`/`m_textureResources` 故意用 `std::unordered_map`,调用方跨查表缓存 `Resource*`postmortem 注释逐字进 review checklist)。
从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。
唯一两处必须真改的 `MG_State` 类型内部用法(都在 Magma):占位纹理(构造真的 `TextureObject2D*` 只为复用 `SyncTextureAndGetDescriptor(ITextureObject&)` 签名,~120 行木偶戏 → ~60 行原生 `VkImage`+view+descriptor34 个 `MOBILEGL_ASSERT(pGLContext)` 里的 9 个随之消失);两个内部 shader 烘焙(§7)。Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。
### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1
```cpp
// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织(~20 KB,字段集全迁移期稳定)
struct PipeInputs {
const RenderStateParameters& GetRenderStateParameters() const; // 阶段 A:类型与后端今天读到的完全一致
// … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 55
#if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
Uint64 m_filledGen[kFieldCount]; // 逐字段"上次填充的 verb 序号"
Uint64 m_currentVerbSerial;
#endif
};
#if MOBILEGL_PIPE_PUSH
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
# define MGB_CTX (::MG_State::pGLContext)
#endif
```
| 阶段 | 改什么 | 证明 |
|---|---|---|
| A 别名 | 机械 `sed``MG_State::pGLContext->``MGB_CTX->`(293 处)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
| B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 |
| C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) |
- 填充点逐 verb 类,不只 `PrepareForDraw`/`SetupDraw` 两处:G5 从 `PipeCalls.def` 生成"每个 `kCtxVerb`/`kCtxObject` 调用可能读哪些字段"的表,在 `MG_Impl` 的 ~93 个边界站点生成 validate/fill 调用。
- poison 是**逐 verb 世代**不是位图:每次 verb 递增 `m_currentVerbSerial`,字段被填时记下序号,读取时断言相等(跨 verb 有效的字段显式标 sticky)。位图看不见"上一个 draw 填过、紧随的 `glTexSubImage` 读到陈旧值"。debug 与 disaggregated 构建里读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}`。纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`
### 9.3 Track V / Track H
- Track V(值类型:`GetRenderStateParameters``GetPixelStoreParameters`、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。
- Track H(对象类型:167 个 `SharedPtr<MG_State…>` 点):真活。
- 读点分类实测(静态):A 探测变化 ~35(12%)、B 翻译输入 ~21674%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"bump 一个版本让 server 自己拉"行不通,值本身必须过去。
### 9.4 残余值块
Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)`payload `ResidualValueBlock{RenderStateParameters, PixelStoreParameters, CapabilityBits, patch 三字段}`。三条纪律:退役是编译错误(`MGL_RESIDUAL_BLOCK_SIZE` 只降不升,`MobileGL/MG_Pipe/MGPipeTypes.h:535`P13 变成 `static_assert(sizeof == 0)`);布局逐成员 `offsetof` 断言且 split 下逐字段序列化(异质 POD 并集的 padding 差异 monolith verify 看不见);只在 P2..P13 存在,`MOBILEGL_PIPE_STATS` 单独计一类字节(`ResidualValueBlock`P0 已占位)。
### 9.5 21 条身份 memo 的重键
统一事实:每个进入 memo 键的版本计数器要么是回绕 `Uint16`,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。`{slot, gen}` + 显式 destroy 让 **11 条直接删除**registry 的同址 `weak_ptr` + GC ×6、`TwinLookupMemo` ×3 + `OwnerEquals``UnitSamplerLookupMemo``WeakPtr` 测试、`SetBackendStateMemo``VkTextureManager::TextureIdentity` 存活探测、`ConvertedVertexStreamKey``sourcePin`……),**2 条** server 删除但去抖搬到 client(§5.4),**7 条重键**成更便宜的比较(`StampSyncedFBO` 四元组 → `ContentHash` + server 私有 `attachmentRemintEpoch``ResolvedTextureBindingMemo` 9 键 → `(shaderCso.slot, viewSetSerial)``SetupDrawSnapshot` 的 ~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty mask`VertexInputStateFactory::ComputeHash` 里的 lifetimeId → `gen` **混进** server 侧每个 content hash),**1 条**(D18)原样不动。两个顺带修掉的潜伏 bug 已先独立落地:`m_xfbCounterSlotByObject` 用裸 GL name 做键(`bd2b4158`)、`RenderbufferObject``GetLifetimeId()``9c7339b2`)。
### 9.6 A/B 与口径收窄
`MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)在 P3a/P4a 期间保留 registry / `TwinLookupMemo` 实现活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役(各阶段 +1 天维护)。
P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX``MOBILEGL_PIPE_PUSH``MOBILEGL_PIPE_LEGACY_MEMOS`**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()``MG_State` include**D-B5verify 构建永不出货);三道纯度门在非 verify 构建上转绿。
## 10. server 侧
### 10.1 对象表与 applier
- `MG_Remote/Server/PipeObjectTables`:按 kind 的 slot 数组,不是对象图;server 不持有任何 buffer 的完整副本、不持有纹素、不持有前端对象图。
- `PipeApplier`:解码 → 更新对象表与 `PipeInputs` → 调后端函数指针。debug 断言:任何传输下都不得有 `SharedPtr` 或裸前端指针跨过 applier 边界。`InProcessTransport` 走与 spawn **完全相同**的 G3 编解码路径,只在门铃/拷贝机制上不同。
- 每 context 一份 working `RenderStateParameters`(§5.3)。
### 10.2 monolith 侧的净收益
即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;`SwapchainObject``MG_Impl` 的分层倒置消失;两个潜伏 bug 已修;一次 glslang 编译离开启动路径;`inproc` = 渲染线程;`MG_Test` 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是**增加**的(约 +6,650 手写 + 4,000 生成,对 ~372 行真删除),所以 monolith 论据是逐线程 CPU 数字(§13.2-④),不是删除行数。
### 10.3 索引宿主镜像(`MG_Remote/Server/IndexHostMirror`P8
- 覆盖:`BindMask & ELEMENT_ARRAY` 的资源,且仅当 `kCapNeedsHostIndexBytes`split 且 server 需要索引字节做 restart 重写 / multi-draw 展平)。
- 由 server 本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护:零额外线上流量、零 round trip。GPU 写者对镜像的影响由 `OnGpuWritten` 收窄集在 server 本地判定。
- 预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),逐帧发布 `index-mirror-bytes`;超预算时该 buffer 退化为逐 draw 经 `MGHostSpan` 传送(`Seg` 指向 `SEG_STAGE`),计入 `index-bytes-shipped`
- 必须是它:`kMaxRestartRewriteBytes` = 64 MiB 是默认 `SEG_STAGE` 的两倍,`kMaxFlattenedIndices` = 1<<24 同量级,逐 draw 塞进 32 MiB 的段既不可行也无必要。它是本设计里唯一的"数据副本"。
## 11. 传输与数据面(骨架 P0 已落地,`MobileGL/MG_Remote/`
### 11.1 段
| 段 | 拥有者 | 默认 | 内容 |
|---|---|---|---|
| `SEG_CMD` | clientserver 只读) | 8 MiB2 的幂 | `RingControl`4 KiB 页)+ POD 记录 + ≤4 KiB 内联负载 |
| `SEG_STAGE` | client | 32 MiB,上限实测定 | bulk 字节:buffer sub-data、纹理紧密重打包区域、UBO scratch、client 顶点/索引/indirect 数组、multi-draw 参数块、具名 UBO host payload、persistent-map 脏块 |
| `SEG_REPLY` | serverclient 只读) | 8 MiB4 KiB slot | readback 像素、buffer writeback |
| `SEG_EVENT` | server | 256 KiB SPSC ring | 十个回调的事件 + `EvQueryResult/EvFenceSignaled/EvReadbackDone` |
| `SEG_SHADOW[n]` | client | 每对象,≥256 KiB shadowPhase 2 | 零拷贝 buffer/texture shadow |
| `SEG_ADOPT[n]` | serverclient RW | 每 buffer,≥16 MiB adopted storeP11 | 应用直写 GPU 内存 |
创建(`ShmSegment`):Android `ASharedMemory_create`API 26libc 的 `memfd_create` wrapper 是 API 30);桌面 Linux `syscall(SYS_memfd_create)`;其他 POSIX `shm_open`+`shm_unlink`Windows `CreateFileMappingW`(`Local\`)。传递:POSIX `SCM_RIGHTS``FdPassing`,专用 `AF_UNIX SOCK_DGRAM` socketpair——消息边界保住 ancillary data 与 payload 不被拆开,sideband ≤256 B);Windows 段名走 `SegmentRef`。fd 传递在第一个 transport commit 里实现——没有它数据面在唯一重要的平台上一字节过不去。
不进 `SEG_STAGE` 的:restart 重写的整 EBO 与 multi-draw 展平的索引流(走索引镜像)。`SEG_SHADOW` 块的退休规则:释放的块进 pending 链表,`appliedSeq`(借入 GPU 时间线的 slot 用 `retiredSeq`)越过最后一条引用它的记录后才归还 arena。
### 11.2 `RingControl``Ring.h`
一页 4 KiB,每个争用组各占一条 cache line:`SEG_CMD` 游标三元组 `cmdHead / cmdAppliedTail / cmdRetiredTail``SEG_STAGE` 独立三元组(`stageHead / stageAppliedTail / stageRetiredTail`——"`SEG_STAGE` 余量 < 1/4"是 publish 触发器,占用率不能从另一个 ring 算出,且 stage slot 的退休条件不同);三个严格区分的水位 `appliedSeq`(释放 `*AppliedTail`/ `submittedSeq`(释放 staging/ `retiredSeq` + `completedFrameSerial`(释放 `*RetiredTail``SEG_ADOPT`+ `presentAckSerial``serverEpoch`context 丢失 / server 重启 ++)、`ringGeneration`(硬 drain 后 ++,作废缓存 offset)、`consumerParked`/`producerParked``eventRingFull``eventDropped`。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次,那种 slot 只能在 `completedFrameSerial` 之后回收。游标是单调字节计数、2 的幂掩码、永不重置。
记录头 `RingRecordHeader{kind, flags, size}`kind 0 保留给 wrap 填充;`RingProducer::Reserve` 在记录会跨 wrap 边界时自动发 pad 记录,保证每条记录连续;`MaxRecordBytes() == Capacity()/2``RingConsumer::Pop` 拒绝不可能的头(非 8 对齐、小于头、大于已发布)并置 corrupt → `Fatal{ProtocolCorruption}``HardDrainRing` 只在两侧静默且 ring 全空时 bump generation。
### 11.3 双向 doorbell`Doorbell.h`
- client → serverconsumer 自旋 → 置 `consumerParked=1` → 阻塞;producer release-store `cmdHead` 之后仅当 `consumerParked` 时敲(字节码 `0x01`)。
- server → clientclient 在**任何**等待(present credit、`kNeedsAck`、ring/stage 满)先自旋 `MOBILEGL_IPC_SPIN_US`(默认 50 µs)→ 置 `producerParked=1` → 阻塞;server 在 release-store 任何 watermark 之后仅当 `producerParked` 时敲(`0x02`)。没有第二个方向,每处 client 等待都退化成跨进程自旋一条 cache line——手机上一颗大核满频空转一整帧,而全库没有亲和性控制。
- 两个实现,零 futex/eventfd/named-event 平台代码:`CondVarDoorbell``inproc`,带 `Kill()` 死亡态让 `Shutdown` 能 join 一个 parked 的等待者)与 `SocketDoorbell``spawn`,一字节;`SOCK_STREAM` 端在对端关闭时报 `POLLIN|POLLHUP` + `recv()==0`,这是死亡检测)。
- 丢失唤醒窗口由**两个 `seq_cst` fence** 关闭(等待者置标志 → fence → 再测条件;通知者发布 watermark → fence → 读标志),标志本身的访问是 relaxed。`NotifyIfParked` 的前置条件:watermark 已发布。死亡的 doorbell 让 `Wait` 停止重新 park。
### 11.4 控制面(`protocol.fbs``Framing.h``ITransport.h`
- 一份 schema,两种用法:热路径 → FlatBuffers `struct`(定长、无 vtable、只需边界检查)直接进 ring——即 G3 生成的记录,与 `MGPipeTypes.h` 的 POD 逐条 `static_assert` 尺寸/`offsetof` 对齐;罕见/变长/需演进 → `table` 走 CTRL socket。今天 `protocol.fbs` 只含控制面(`MobileGL/MG_Remote/Protocol/protocol.fbs:218-228``CtrlMsg``Hello``Welcome`(四个段的 `SegmentRef`)、`CapsSnapshot``SurfaceOp/SurfaceReply``ResyncRequest/Done``AuxRequest`(外来线程的 fence wait / query result / scalar get)、`Fatal``ProtocolCorruption/RingOverrun/SegmentMismatch/DeviceLost/ServerCrashed/AbiMismatch`)、`LogLine`),`file_identifier "MGLC"`union tag 是 wire 值,只追加。
- `protocol_generated.h` 提交进树,`scripts/gen_protocol.py` 再生成(只用 `MOBILEGL_FLATC_EXECUTABLE` 或从 pinned submodule 在仓库外构建一次的 flatc,不用 PATH 上的),CI `flatc-check``.github/workflows/test.yml:304`)重生成并 diff。**codegen 绝不进默认构建图**;运行时 header-only。
- 封帧 `[u32 'MGLF'][u32 len][payload]`,64 MiB 上限,**读时校验**:坏 magic / 超长长度立即 latch 失败并报 `MOBILEGL_ERR_PROTOCOL_MISMATCH`(不是静默永久挂起);接收缓冲不足**返回所需大小并保留消息**(`MOBILEGL_ERR_BUFFER_TOO_SMALL`)。
- `ITransport``SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown / Role`;热路径完全绕过它。`Shutdown` 拆掉整个连接(两端都不能再发,等待者全部解锁,已排队消息仍可读完)。`WireLog.h` 是唯一的日志入口,让 `Transport/` 的头不 include 前端 umbrella(纯度门 A 断言 `-H` 输出)。
- `mg_protocol_base.h`:纯 C、无依赖的结果码 / span / `ShmRegion` / id 词汇,structSize-first 版本纪律(追加 = minor,改动 = majormajor 不符是结构化失败)。
### 11.5 WAR 危害、拷贝账与背压
- Phase 1(P5P8):GL 调用时刻把字节拷进 ring slotslot 到 `stageAppliedTail` 越过它为止不可变,危害按构造消除;代价一次 memcpy,`Ops_ResidentSubData``StageBlocksIntoUnpackRing` 在 monolith 里已经在付。
- Phase 2shadow-in-shm,零拷贝):≥256 KiB 的 shadow 分配在 `SEG_SHADOW``PipeResource::MapAlignedAllocator` 增加 shm arena,保留 64 B 对齐契约;`MipmapStorage` 的 level vector 同理),`ResourceSubData` 只带 `{seg, offset, size}`。WAR 用 per-shadow 64 KiB 块发送水位:应用写某块而该块上次发送尚未被 `appliedSeq` 覆盖 → 这次写走 `SEG_STAGE`。必须整段 `#if MOBILEGL_BUILD_DISAGGREGATED` 包裹(改容器 allocator 就改了类型,option OFF 时逐字折叠回今天的 allocator)。
| 路径 | monolith | Phase 1 | Phase 2 |
|---|---|---|---|
| `glBufferSubData` → shadow store | 2 | 3 | **2** |
| `glBufferSubData` → adopted storeP11 | 2 | 2 | 2 |
| `glMapBufferRange(WRITE)`+unmap | 3 | 4 | 3 |
| persistent coherent map 推送(§12 | 0 | 1/发射点 | 1/发射点(精确块) |
| `glTexSubImage` | 2 | 2 | 2 |
| 全局 UBO / draw | 1 | 2 | 1 |
| adopted ≥16 MiBP11 T1/T0 | 0 | 0 | 0 |
server 没有第二份 `BufferObject`,所以不存在"staging → server 侧 shadow"这次中间拷贝。字节计数器装在 wire 两侧,验收看总量。
- 分配与背压:逐字移植 `PersistentRing`(单调 head/tail、2 的幂掩码、frame mark)。分配失败升级:扩容(翻倍)→ 对最老未 retire 批次有界等待(默认 50 ms,走 `producerParked` doorbell)→ 硬 `Drain` + `ringGeneration` bump。硬 drain 后恢复便宜:正向流是自洽的推送流,tracker 把全部 dirty 位置为"必须重推",下一个 verb 重发完整 `set_*` 集合,纹理侧由发射游标负责,没有"重发未 apply 对象状态"的特殊协议。`SEG_CMD``SEG_STAGE` 各自独立跑这套升级。
### 11.6 publish、序号与 credit
- 不设"records ≥ 64 KiB"一类阈值(那是一整帧的流水线气泡,且否掉 `inproc` 的全部意义)。规则:每条记录(或每 8–16 条摊销)release-store `cmdHead`,仅当 `consumerParked` 时敲门铃。
- 显式门铃点:`present`、任何 `kNeedsAck` 请求、`eglMakeCurrent``glFlush`(刷出不等待)、`SEG_STAGE` 余量 < 1/4、**轮询类入口**`glClientWaitSync` 任意 timeout、`glGetSynciv(GL_SYNC_STATUS)``glGetQueryObject*(AVAILABLE|NO_WAIT)`——否则 `while (glClientWaitSync(s, FLUSH_COMMANDS_BIT, 0) == TIMEOUT_EXPIRED) {}` 永久自旋);带 `GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish。
- 饥饿升级:同一 handle 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)本地回答"未就绪"而 watermark 毫无移动 → 升级为一次阻塞 round trip。
- `glFinish`/`glFlush` 保持纯 no-op。
- seq = 记录序数;两个互相独立的窗口:字节 credit(两个 ring 各自占用)与 present credit`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT``eglSwapBuffers` 阻塞)。server 不发 credit 消息:对 `RingControl` release storeconsumer 每 64 条记录更新一次 `appliedSeq``producerParked` 时敲反向门铃。
### 11.7 事件回传与溢出
`SEG_EVENT` 承载十个回调加回读完成通知。client 排空点:`glGetError``glGetQueryObject*``glClientWaitSync``glGetSynciv``eglSwapBuffers``glMapBuffer*`/`glGetBufferSubData`/`glCopyBufferSubData`,以及**每一次等待循环的每一轮**。溢出策略(修一个双向死锁:client 卡在 present credit、server apply 线程卡在生产事件):`EvLogLine` ≤WARN 有损;语义承载事件(`EvGpuWritten``EvReadbackDone``EvFenceSignaled`、writeback、pull request、mip、scatter、`EvGlError`、surface、caps、`EvLogLine ≥ERROR`)无损——ring 满时 server 置 `eventRingFull=1`、**在记录边界停止 apply**、敲反向门铃,client 排空后清标志并敲正向门铃;ERROR 速率限制器。故障注入:client 被 credit 阻塞时灌满 `SEG_EVENT`;日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧恢复。server 侧 `MGLOG` 按流顺序 replay 进 client 日志流(复用 `DeferredLogLine` 机制)。
### 11.8 fence 与无 present 负载
- fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位:DirectGLES 的 `g_completedFrameSerial` 只在 `Present()``WaitForFrameSerialCompleted` 里前进,帧中 fence 会退化成帧计数推断——`DirectVulkan.cpp` 写明这是被修掉的 bugMC 1.21.5 的 fence-paced ring 曾因此 native-heap OOM)。规则:`FenceCreate` 转成真实的后端 `FenceSync()`server 用自己已有的逐 fence 轮询在非 present 时刻也推进并发 `EvFenceSignaled`
- 无 present 循环(CTS、回读循环、从不 swap 的集成场景)下 `retiredTail` 会饿死、`SEG_STAGE` 填满、每个用例都跑到硬 drain。规则:DirectGLES 的 server 加**非 present fence tick**——距上次 `Present` 超过 8 ms 或每 4096 条已 apply 记录插一个 `glFenceSync` 并轮询 fence ring;ring 占用率与升级次数进计数器;P8 加一个无 present 的 split 用例。
## 12. persistent map 与 ≥16 MiB 采纳
`AcquirePersistentMap` 是永久的地址空间捐赠(返回 host-visible coherent 指针,成为该 buffer 的唯一真相源;≥16 MiB 可变 store 由 `TryAdoptLargeStorage` 自动走到,实测 MC 26.3 p99 163→21 ms、40→115 fps、省 ~400 MB)。**整个 monolith 改造期一动不动**(D-B4),只有 IPC 那一步会打破它。
三档,由运行时 POST 探针选择(本项目"后端限制一律探针判定、不硬编码驱动名"的既定规则),**spike B 已在两台设备上给出答案**(`MEASUREMENTS.md` §2):
| 档 | 形态 | 实测 |
|---|---|---|
| **T0 — server 导入 client 分配**P11 主攻) | client 分配 `AHardwareBuffer` BLOBsocket 交接;server 以 `VK_ANDROID_external_memory_android_hardware_buffer`Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`Espryt)导入,两侧 persistent+coherent 映射 | **Adreno 830 与 Mali 都是完整读写往返**,含 GPU 访问与两侧字节校验——唯一在两台设备、两个后端上都成立的档 |
| T1 — server 导出自己的映射 | `VK_KHR_external_memory_fd` opaque fdclient `mmap` + 导入 | 只有 Adreno 的 Vulkan 路径可用;Adreno 的 GLES 导入 `glMapBufferRange` 全部 `GL_INVALID_OPERATION`;Mali 不可导出。**每次存储定义一次 round trip**(不是每 store 一次),`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips` |
| T3 — host pointer 导入(`VK_EXT_external_memory_host` | | Adreno 无扩展;Mali 只读(GPU 写对宿主映射不可见) |
| T2 — 拒绝(永久正确回退) | `AcquirePersistentMap` 返回 `nullptr`,前端已在三处容忍 | 此档下 client 侧推送强制 |
`MOBILEGL_IPC_ADOPT_TIER``auto`/0/1/2)做负面对照;与 `MOBILEGL_IPC_RESPAWN` 互斥(被采纳的 store 是 server 拥有的内存)。
**client 侧 persistent map 推送三件套**T2 档强制,P5):
1. 不做 map/unmap 命令对:server 唯一需要知道的是"这个资源现在有没有活的宿主写入者"(`IsBufferDrawClean` 那一行要表达的东西),所以 `ResourceRespecify/SubData` 的 payload 带一个 `hasLiveHostWrites` 位,零新增记录种类。
2. 块粒度脏块推送:tracker 维护 `m_livePersistentMaps`persistent+write+非 FlushExplicit+非 GpuResident),在每个 validate 点对本次操作可达的每个这类 bufferVAO/index/indirect/UBO/SSBO/atomic/XFB target——即后端 20 个 `SyncPersistentMappedRange` 站点的并集)按 `MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(默认 64)切块发送。Phase 1 保守版(整个 mapped span 当脏,按块拆);Phase 2 精确版(shadow-in-shm 的 64 KiB 块脏位,`memcmp` 先行)。P5 验收记录 `persistent-map-push` 字节量;若保守版在 Create/Flywheel fixture 上不可接受,精确版提前——计划里唯一允许因测量改变阶段顺序的地方。
3. 门从第一天就有:`PersistentCoherentMapScenario`map PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)。
`MOBILEGL_COHERENT_AS_FLUSH` 在拆分模式下照常生效:两个带 `coherent_as_flush: true` 的 Create fixture 在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义。
## 13. 回读、roundtrip 清单与验证
### 13.1 稳态零 roundtrip 与不可避免的阻塞点
零 round trip:全部 draw/clear/blit/copy/dispatch/barrier/XFB 跨度/bind/CSO/`set_*`/上传/`present`(单向记录);全部 caps 站点(握手快照);`glGetError`/`glFinish`/`glFlush`(本地 / no-op);fence 与 query 的创建及非阻塞轮询(client 铸造 handle,未命中合法地答"未就绪");`glGetTexImage`DirectGLES,含 GPU 生成的 mip);`glReadPixels` → pack PBOfire-and-forget + client 侧 `MarkGpuWritten`,严格优于 monolith 的无条件停等);`glEndTransformFeedback`(取消无限 fence 等待,对 capture target 置 `MarkGpuWritten`);`eglSwapBuffers`(只查 credit);`*IndirectCount`restart/multi-draw。
不可避免(全部罕见):握手一次;surface 生命周期与首次 `MakeCurrent`+`InitCapabilities` 每 surface 至多一次;`glReadPixels` → 客户内存(像素进 `SEG_REPLY`,逐行写回循环留在 server 内按操作级批成一段);`glGetTexImage`DirectVulkan,对"无 GPU 背书"的 level 回答"请用你自己的 shadow");GPU-write pending 的 buffer 首次 CPU 读(monolith 本来就 `glFinish()`;由 `writableMask``OnGpuWritten` 收窄);`glClientWaitSync(timeout>0)``GL_QUERY_RESULT` 未完成、`glBeginConditionalRender`(谓词只解析一次,之后每个条件 draw 在 client 丢弃,server 永远不需要那个 query);`glBufferStorage` 的 ack`MapPersistent`(仅 T1,每次存储定义一次);纹理拉取(§8.4);client 侧索引扫描当源 EBO 在 pending 集里;ring/stage 耗尽与 present credit(节奏,非语义)。
验收措辞:在全部 40 个 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数与 `index-bytes-shipped`;零 timeout 轮询循环必须在有界时间内退出。
### 13.2 五部分验证门(取代 monolith 的字节一致门)
"改前改后 `nm --defined-only``.text` size 完全相等"的门在本方案里按构造死亡(不存在能让旧字节回来的配置);替换是:
1. **接口纯度三道门**(只跑非 verify 构建):**A 门 include 图**——disaggregated 配置编译 `MG_Backend` 时把 `MG_State/GLState` 从 include 搜索路径移除(`nm --undefined-only` 对"只 include 不调用"是瞎的,而 `RenderState.h → FramebufferObject.h → TextureObject.h` 正是这种耦合),依赖 P0.5;**B 门符号**——`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**C 门未声明**——`grep -c 'pGLContext' MG_Backend/` == 0。外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`,永不是裸前端指针",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。
2. **语义影子比对 `MOBILEGL_PIPE_VERIFY=1`**——决定性的一条:两套状态模型活在同一地址空间,tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 比对器逐字段、每 draw 比对,打印第一个分歧字段与 draw 序号。抓 tracker 忘推的字段、**dirty 位触发得太少**(危险方向)、两条路径变换不一致的值。第三种 CI 模式,40 个 trace + 全部集成测试,~5–10× 慢,永不出货。逐字段而非 `memcmp`padding 会 false-DIFFER)。**保留模式**:消费即清的组(纹理 dirty rect)发射后无法重算,verify 时 tracker 保留清除前的集合并比对发射出去的 `(UnionBox, RegionCount, Regions[])`。**活过 P13**。
3. **行为 A/B**40 个 trace 在 `{monolith-pull, monolith-push, split}` 下 SSIM ≥ 0.99(默认阈值);`ctest -L integration-gpu``DirectGLES.``DirectGLES.Pipe.`/`DirectGLES.Split.`DirectVulkan 同)之间逐名相同;单元测试全绿;CTS 逐后端 conformance 在 0.5 pp 内(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail)NS 不进分母)。`TextureUploadShapeScenario` 把逐纹理逐帧的上传形状(box vs N region、作业数)录金标比对——+6 ms 悬崖由形状相等把关,SSIM 对它完全不敏感。逐名功能基线是"P1 出口的重构后 monolith"P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。
4. **monolith 性能不回归**:两台设备 reboot-clean、同热窗口、配对 A/B`tools/bench.sh` + trace replay `--benchmark` 逐帧 JSON;**指标是逐线程 CPU 时间**p50 与 p99**绝对阈值**——tracker 每 draw 的 ns 公布并设上限(真实拉取基线只有每 draw 6.5–9.3 次 accessor,相对噪声阈值会平凡通过);Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。
5. **覆盖 + poison + 句柄纪律**G6 重生成 0 UNMAPPED`gen_pipe_dirty_surface.py` 重生成 0 未映射 mutator;逐 verb 世代 poisonG7 setter 一致性测试;`ResidualValueBlock``offsetof` 断言与 P13 的 `sizeof == 0`
两条幸存的字节级等式:`MOBILEGL_BUILD_DISAGGREGATED=OFF``nm --defined-only libMobileGL.so | grep MG_Remote` 为空且链接行不增加库;`nm -D libMobileGL.so | grep mobilegl_server_main` 在 RelWithDebInfo 里命中。符号与 `.text` 漂移每阶段作为信息性指标发布。
### 13.3 长期语义门:MGPipe recorder
P13 把 `MG_Test` 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 `MG_State`,是 P13 之后不靠 verify 构建的语义门,也给 `tools/trace_replay` 一种记录**已解析**状态的、比 apitrace 精确得多的录制格式。它只覆盖推送内容,不覆盖后端对它的解释(split-only 的渲染 bug 仍无 server 侧第二意见)。
## 14. Present、线程与帧节奏
- `eglSwapBuffers``present{frameSerial}`swap interval 搭在同一条记录上)→ publish + 敲门铃 → 返回,除非超出 credit。**`present``eglSwapBuffers` 严格 1:1**:两个后端的帧边界排空(Magma 四次 `OnFrameBoundary` 老化、`TryDrainFrameTransients``BeginFrame`Espryt 三个 ring 与 `TrimBufferPool` 的 retire)只在 `Present` 内发生,批量会饿死它们。
- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(可配 1–4):延迟叠加,`端到端 ≈ client credit + server 帧数 + 驱动深度`server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 45 帧(60 Hz 下 6683 ms)。P10/P12 用 `GetGpuTimestampNs``--benchmark` 逐帧 JSON 构建输入延迟直方图,只有实测吞吐收益能抵掉延迟代价才调高。
- Magma 从不注册 `SetSwapInterval` 且偏好 `MAILBOX`/`IMMEDIATE`,IPC credit 是它唯一的显式限帧器;若需要 FIFO 作为独立 `dev` 变更。
- 线程——client**v1 不加线程**,编码在 GL 线程上直接写 ring(前端本就是 per-context 单线程契约);外来线程的 sync/query 读全部从 `RingControl` 无锁回答,必须发射的少数取 `ctrlMutex` 走 CTRL socket 的 `AuxRequest`SPSC ring 不允许第二个 producer);`ShaderCompilePool` 原样在 client;可选 `mgl-client-tx` 凭测量决定。server`mgl-srv-io`asio、封帧、`SCM_RIGHTS`、doorbell、CTRL RPC)、`mgl-srv-apply`**终身持有原生 context**`g_backendContextOwnerThread` 只写一次,`MakeCurrent` 的缓存失效风暴变启动期一次性,每帧 EGL 复核恒真,off-thread 降级消失)、可选 `mgl-srv-dec`
- **核心放置**:拆分的全部性能主张押在两半落在两个都快的核上。全库无亲和性控制,server 是独立进程不继承 launcher 的亲和性。规则:报总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith `PrepareForDraw`);复用 `ShaderCompilePool` 的大核探测把 `mgl-srv-apply` 绑到大核(`MOBILEGL_IPC_SERVER_AFFINITY`,默认 auto,解析出的 mask 打进日志);每阶段报逐线程 CPU 时间。
- 拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool(先于 `glslang::FinalizeProcess()``pGLContext` 析构)→ `MobileGL::Destroy()` → 释放 sync/query handle。
## 15. 进程、EGL 与平台
### 15.1 启动与握手
- server 定位:`MOBILEGL_IPC_SERVER_PATH`(主要)→ `dladdr(&MobileGL::Initialize)` 同目录的 `libMobileGLServer.so`(兜底;不能当主要机制,因为集成测试静态链接 `MobileGL_s`、trace replay 的可执行文件不在库目录)。配套:`MobileGLServer``RUNTIME_OUTPUT_DIRECTORY` 设为 `$<TARGET_FILE_DIR:MobileGL>`,每条新 ctest `ENVIRONMENT``add_trace_replay_test``SPLIT` 分支带 `MOBILEGL_IPC_SERVER_PATH`
- 启动:`socketpair(AF_UNIX, SOCK_STREAM)` + `fork`/`execve`fd 3 = socket。无文件系统 socket 路径、无 abstract namespace、Android 上无 SELinux 争议。
- **子进程强制 monolith**(修无界 fork 链——server stub `dlopen(libMobileGL.so)` 后必然走 `MG_Backend::Init()`,继承的 `MOBILEGL_TRANSPORT=spawn` 会再 spawn):spawn 时构造显式 envp 剔除 `MOBILEGL_TRANSPORT` 与全部 `MOBILEGL_IPC_*``mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`。两条都做。`MG_Test/Wire` 测试:spawn 一个 server,进程树只多出恰好一个子进程。
- `Hello{abi, backendType, buildFingerprint, configBlob}``Welcome{四个段}``configBlob` 转发 client 解析好的 `MG_Config::Features`,两半不可能对 quirk 开关有分歧;`buildFingerprint`git hash + `PipeCalls.def` hash)不匹配 → 握手期 `Fatal{AbiMismatch}`
- `mobilegl_server_main` 声明为 `extern "C" __attribute__((visibility("default")))`:非 Debug 构建设了 hidden visibility,而 FCL/plugin 出货的是 RelWithDebInfo,否则 `dlsym` 在设备上静默失败。
### 15.2 Androidspike A 已证)
- 交付链:APK 唯一可 exec 的位置是 `lib/<abi>/`,打包器只收 `lib*.so`,所以 server 以 `add_executable` + `PREFIX "lib"/SUFFIX ".so"` 构建(真 PIE),并把 `RUNTIME_OUTPUT_DIRECTORY` 指到 AGP 收集原生产物的 `CMAKE_LIBRARY_OUTPUT_DIRECTORY``CMakeLists.txt:784-808``MOBILEGL_BUILD_SERVER_SPIKE`)。**两台设备上都已证明**:从 `TraceReplayActivity` 自身的 `untrusted_app` 进程 `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so`,子进程落在同一域、同一 MLS categoryexit 0,零 avc denial`MEASUREMENTS.md` §1)。
- `fork`+`execve` 而非 `posix_spawn`bionic 从 API 28 才声明后者,minSdk 26`android-plugin/app/src/trace/cpp/spawn_spike.cpp:63-68`)。fork 与 execve 之间只做 async-signal-safe 的 open/dup2/execve/write/_exit(父进程是多线程 JVM)。
- 应用进程的 stdout/stderr 是 `/dev/null`:子进程用 **marker 文件** 证明自己活过,exec 被拒的 errno 经 close-on-exec pipe 回传(EACCES 与 ENOEXEC 是完全不同的判决)。
- 生产 server 主体是 ~30 行 stub`dlopen(libMobileGL.so)``dlsym("mobilegl_server_main")`。一份共享库、两个角色、版本必然匹配(Android 上那份库仍含 glslang/SPIRV-Cross,因为它同时服务 client;B 门检的是 server 侧代码有没有引用它们)。
- minSdk 26 没有公开 NDK API 能扁平化 `ANativeWindow``libbinder_ndk``ASurfaceControl` 都是 API 29)。**P5P11 验证路径无窗口**pbuffer 或 `AImageReader``ANativeWindow`trace replay 默认 pbuffer。**P12 生产路径**Java `Surface`Parcelable)→ Messenger/AIDL → `MobileGLServerService``android:process=":mgl"`)→ JNI `ANativeWindow_fromSurface`FCLauncher 今天在 `egl_bridge.c` 做的那一次调用);仓内先例是 `android:process=":bench"``BenchService`。代价:server 进程多一个 ART~1525 MB)。FCL 把游戏 JVM 跑在主进程,第二个进程必须新建。
- `HeadlessGL` 的 fork 预检会 fork 一个子进程跑完整 EGL bring-up 然后 `_exit`——拆分模式下那个子进程会 spawn 一个孤儿 server。规则:server 的 EOF 检测**即时且无条件退出**(亚秒级);client 的 socket fd 设成 `_exit` 会确定性关闭的形态;就绪握手有界重试。列为 P6 验收。
- 通用 env 透传 `--env K=V``run_android_retrace_local.py` → intent extra `mobilegl_env``trace_replay_core.cpp` 在加载 `libMobileGL.so``setenv`)已接进 retrace 通道,取代逐 knob 加 `--es/--ez`
### 15.3 Linux / Windows / 崩溃
- Linux/X11`Window` 是 XID`nativeToken:u64` 直接送,backend 自己 `XOpenDisplay(getenv("DISPLAY"))`Wayland 维持不支持。WSL/CI 永不开窗:`EGL_PLATFORM=surfaceless` + `EnsureHeadlessPlatform()`
- Windows`HWND``nativeToken`Vulkan 可行,WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only。transport 默认 named pipeasio `windows::stream_handle` 要求 overlapped 句柄,所以用 GUID 命名的 `CreateNamedPipeW(FILE_FLAG_OVERLAPPED)` + `CreateFileW(FILE_FLAG_OVERLAPPED)` 造句柄对再继承给 `CreateProcess`AF_UNIX-everywhere 是可选简化。Windows 机器不是正确性门。macOS 不拆分(`CAMetalLayer` 无跨进程表示)。
- server 死:client 读到 EOF/EPIPE → device-lost 闩锁(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_FALSE`+`EGL_CONTEXT_LOST``glGetGraphicsResetStatus` 返回 `GL_UNKNOWN_CONTEXT_RESET`);`MOBILEGL_IPC_RESPAWN=1` 时重启并全量重推(默认关,静默重启会掩盖 bug)。client 死:server 读到 EOF → 立即销毁原生 context 并退出;`MOBILEGL_IPC_IDLE_EXIT_S`(默认 30)只作最后保险。
## 16. 构建布局
```
MobileGL/MG_Pipe/ 永远进构建(monolith 的架构,不在任何 option 之后) [P0]
MobileGL/MG_Impl/Pipe/ Tracker、SlotAllocator、CsoCache、HostResolve、CompositeResolver [P2+]
MobileGL/MG_Backend/MGPipe/ PipeInputs.h + MGPipeImpl_DirectGLES/DirectVulkan.cpp [P1+]
MobileGL/MG_Remote/ 仅 MOBILEGL_BUILD_DISAGGREGATED
Protocol/ protocol.fbs generated/protocol_generated.h mg_protocol_base.h [P0]
Transport/ ITransport InProcessTransport Framing Ring ShmSegment(+Posix/Win32) FdPassing Doorbell WireLog [P0]
SocketTransport [P6]
Client/ PipeEmitter EmitTables BackendObject_Remote CapsMirror ShadowArena PersistentMapTracker GpuWritePending Surface/{X11,Win32,Android,Headless} [P5+]
Server/ PipeApplier PipeObjectTables IndexHostMirror ServerLoop ReplyPool EventRing ServerMain [P5+]
ServerJni.cpp [P12]
```
- CMake option`CMakeLists.txt:23`):`MOBILEGL_BUILD_DISAGGREGATED`(默认 OFF)追加 `MG_Remote/**``SOURCE_FILES``CMakeLists.txt:454-469`)并定义 `-DMOBILEGL_BUILD_DISAGGREGATED=1`OFF 时 `MG_Config::Transport``constexpr Monolith``Init.cpp` 的分支编译期消失。`3rdparty/flatbuffers/include` 缺失时把 option 强制回 OFF 并 `message(WARNING)``CMakeLists.txt:440-451`)。`MobileGL``MobileGL_s` 都拿到同一份源。`MG_Test/Wire` 只在该 option 下注册(`MobileGL/MG_Test/CMakeLists.txt:93-95`)。
- `MOBILEGL_BUILD_DISAGGREGATED_INPROC`(尚不存在):CI/调试形态,隐含开启前者,额外加角色隔离 shim。MGPipe 让需要角色分身的进程全局从四个(`pGLContext``gBackendFunctionsTable``pActiveBackendObject``pDefaultFramebufferInfo`)降到**两个**pipe 表与 `pActiveBackendObject`):server 角色不再读 `pGLContext`(三道纯度门就是这个断言),`pDefaultFramebufferInfo` 由保留句柄 `{0,1}` + `OnSurfaceChanged` 取代。两个 shim 都不在 GL 热路径的每次访问上——这是 `inproc` 从"成本可疑的实验"变成"可交付形态"的直接原因(Android 上 dlopen 的库无法可靠用 initial-exec TLS`pGLContext->``MG_Impl` 有 1494 处)。
- `MobileGLServer`:桌面 `add_executable` 链接 `MobileGL_s`Android `add_executable` 改名 `lib*.so` 链接共享 `MobileGL`,由 AGP 打进 `jniLibs`
- `MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>`P5 起在 `ConfigLoader.cpp` 解析),免费换来 ctest `ENVIRONMENT` 变体、trace-replay 的 `setenv` 块、FCL 用户可编辑 env、plugin APK 的 V2 开关表、`/data/local/tmp` CTS 路径。
- 测试接线陷阱:ctest `ENVIRONMENT` 是替换而非追加、`;` 必须转义、property 覆盖 job env,必须用 `mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})` 构造;`add_trace_replay_test``SPLIT` 后缀(否则与同 case+backend 重名)并加 `-DTRACE_TRANSPORT=``run_trace_case.cmake` 消费。
- CI`.github/workflows/test.yml:809` `pipe-gates`P0 已落地):`gen_pipe.py` 重生成 + diff`MG_Backend`/`MG_State` 下禁止 stdio 插桩的 grep 门;`gen_pipe_dirty_surface.py --summary`(信息性,P1 成门);`check_doc_citations.py`(警告级,文档定稿后 `--strict`)。独立 job `flatc-check`。后续:`include-graph-check`P0.5)、`monolith-symbol-report`
## 附 A:开关
CMake
| 选项 | 默认 | 状态 |
|---|---|---|
| `MOBILEGL_BUILD_DISAGGREGATED` | OFF | 已落地 |
| `MOBILEGL_BUILD_SERVER_SPIKE` | OFF(仅 Android | 已落地(spike A,非出货) |
| `MOBILEGL_BUILD_DISAGGREGATED_INPROC` | OFF | 计划(P5 |
| `MOBILEGL_PIPE_VERIFY` | OFF | 计划(P1;构建期开关,编译进 `SnapshotFromGLContext()` 与 G4 比对器,P13 后保留) |
| `MOBILEGL_PIPE_LEGACY_MEMOS` | ONP2..P13 | 计划(编译期臂) |
| `MOBILEGL_FLATC_EXECUTABLE` | 空 | 已落地(只服务 `flatc-check` |
| `MOBILEGL_BAKED_INTERNAL_SHADERS` | ONP7+ | 计划 |
运行时,MGPipe`MobileGL/Config.h:319-358``MobileGL/ConfigLoader.cpp:245-256`P0 已落地):
| 变量 | 默认 | 说明 |
|---|---|---|
| `MOBILEGL_PIPE_PUSH` | 0 | 子系统位图(0 = 全 pull),含一位关闭 CSO 内容寻址;十进制或 `0x` |
| `MOBILEGL_PIPE_VERIFY` | 0 | 逐 draw 逐字段影子比对 |
| `MOBILEGL_PIPE_STATS` | 0 | 边界计数器(§附 B) |
| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON | 三态读取,只有显式 falsy 才关 |
| `MOBILEGL_PIPE_TEXEL_RETAIN_MB` | 004096 | 纹理拉取保留 LRU |
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64(0–4096) | 索引宿主镜像预算 |
| `MOBILEGL_PIPE_STATS_PERIOD` | 120(1–10⁶) | 每多少帧一条汇总行 |
| `MOBILEGL_PIPE_STATS_FILE` | 空 | teardown 时的 JSON 转储路径 |
运行时,传输与 IPC(计划,P5+):`MOBILEGL_TRANSPORT`(monolith)、`MOBILEGL_IPC_SERVER_PATH``MOBILEGL_IPC_RING_MB`(8)、`MOBILEGL_IPC_STAGE_MB`(32)、`MOBILEGL_IPC_PRESENT_CREDIT`(1)、`MOBILEGL_IPC_SPIN_US`(50)、`MOBILEGL_IPC_POLL_ESCALATE`(64)、`MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(64)、`MOBILEGL_IPC_ADOPT_TIER`(auto)、`MOBILEGL_IPC_SHADOW_SHM`(1Phase 2 起)、`MOBILEGL_IPC_INLINE_PAYLOADS`(0,负面对照)、`MOBILEGL_IPC_SERVER_AFFINITY`(auto)、`MOBILEGL_IPC_STRICT_ERRORS`(0)、`MOBILEGL_IPC_AUDIT`(0)、`MOBILEGL_IPC_TRACE`(0)、`MOBILEGL_IPC_ATTACH``MOBILEGL_IPC_RESPAWN`(0)、`MOBILEGL_IPC_IDLE_EXIT_S`(30)。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`server 没有 `MG_Impl` 校验器)。既有负面对照开关(`MOBILEGL_ESPRYT_DISABLE_{UBO,UNPACK,UPLOAD}_RING``_INVALIDATE_FLUSH``MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION``MOBILEGL_COHERENT_AS_FLUSH`)全部保留。
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h:46-122`P0 已落地)
关闭时每站点一次全局 load + 一条永不命中的分支。字节类:`stage-buffer``stage-texture``stage-ubo-global``stage-ubo-named`(只有 Magma 贡献,D-B8 的不对称)、`stage-vertex-client``stage-index-client``stage-indirect-cmd`Espryt 独有)、`persistent-map-push`P0 未接线:monolith 期不存在推送)、`residual-value-block`(占位)。调用类:`draws``accessor-calls`(实际执行的 GLContext accessor 次数,在约 10 个热入口做静态计数,是**下界**)、`texture-upload-emissions/box/rect/jobs`。六个 memo 门(`SyncRenderState` 早退、`SyncNeccessaryTextures` 键比较、`CurrentUnitBindingsEpoch` 快门、`TrySetupDrawFastPath`、pipeline memo、`ApplyDynamicDrawStateTail`)各计 hit/miss。每 draw payload 直方图(24 桶)已实现,等第一个发射器接入。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行(`MGLOG_I`),`TRACY_ENABLE` 下逐帧 `TracyPlot`teardown 时可选 JSON。站点清单——哪些路径**没有**接线——写在 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份清单是契约。
+96
View File
@@ -0,0 +1,96 @@
# P0 实测
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74CAdreno 830Android 16`3B159D009VZ00000` = Oppo PLG110MaliAndroid 16ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
## 1. Spike A — 从应用自身进程 exec 第二个原生可执行文件
问题:Android 上能否把 server 以 `lib*.so` 打进 APK,并从应用自己的 `untrusted_app``fork`+`execve` 它(`adb run-as` 跑在别的域,证明不了)。
| 设备 | 结果 |
|---|---|
| Adreno 830 | **OK**。父进程 `u:r:untrusted_app:s0:c173,c257,c512,c768` `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so` → 子进程 pid 31348exit 0;子进程 SELinux `u:r:untrusted_app:s0:c173,c257,c512,c768`(同域同 category);marker 文件、stdout 捕获、报告全在;`execErrno=0`;窗口内**零 avc denial** |
| Mali | **OK**,同形:子进程 pid 28433exit 0`execErrno=0``u:r:untrusted_app:s0:c94,c257,c512,c768`,零 avc denial |
主机侧已证的三条(随 `8a239177`):AGP 会把改名成 `lib*.so``add_executable` 打进 `lib/arm64-v8a/`,前提是把 `RUNTIME_OUTPUT_DIRECTORY` 重定向到 `CMAKE_LIBRARY_OUTPUT_DIRECTORY``posix_spawn` 在 minSdk 26 不可用(bionic API 28 起),出货臂是 `fork`+`execve`;应用进程 stdout/stderr 是 `/dev/null`,子进程用 marker 文件证明自己活过。
- 代码:`tools/spikes/server_stub/main.cpp`stub:打印并写 marker 自己的 pid/uid/SELinux 上下文)、`android-plugin/app/src/trace/cpp/spawn_spike.cpp``RunSpawnSpike`)、`CMakeLists.txt:784-808``MOBILEGL_BUILD_SERVER_SPIKE`)。
- APK`p0-spike-a-android/trace-debug-spike-on.apk`(在 `30d7595b` 构建,与 `7ef7c7e5` 源码相同)。
- ColorOS 陷阱:首次 `adb install` 一个未安装的包会卡在 `com.oplus.appdetail InstallGuideActivity` 确认页,直到点"继续安装"1272×2772 面板上 `input tap 353 2349`);同签名重装静默通过。另一台设备上一个外来签名的 trace APKversionCode 26080769)会让 `install -r``INSTALL_FAILED_UPDATE_INCOMPATIBLE`,需先卸载。
- 42-device.sh 的 env 透传 A/B 腿在该 ROM 上跑不了(`run-as sh -c 'cat > files/…'` 被拒);透传由下面的 stats 基线端到端证明(`--env MOBILEGL_PIPE_STATS=1` 必须在 `mobilegl.log` 里产生 `MGPipe stats` 行)。
## 2. Spike B — 跨进程外部内存分档
问题:`AcquirePersistentMap` 背后的内存能否共享给另一个进程并在那里映射,两个后端各走哪条路。探针 `tools/spikes/extmem_probe/``39f982e6` 源码,arm64`adb shell` = `u:r:shell:s0` 域),4 MiB payload,64 KiB 同判决。每一行都取一次真 GPU 访问(`vkCmdCopyBuffer` + `vkCmdFillBuffer` + host-read barrier)并两侧字节校验才算 OK。
| 路线 | Adreno 830 | Mali |
|---|---|---|
| T1-opaque-fdserver 导出 `VkDeviceMemory` fdclient 裸 `mmap` + 导入) | **OK** 完整往返含 GPU 访问(`/dmabuf:system`dedicatedOnly=1 | UNSUPPORTED`vkCreateBuffer(external)=VK_ERROR_INVALID_EXTERNAL_HANDLE`advertisedExportable=0 |
| T1-dma-buf | UNSUPPORTED`VK_EXT_external_memory_dma_buf` 缺) | UNSUPPORTED |
| T1-gles-memobj-fd`GL_EXT_memory_object_fd` 导入导出的 fd | **FAIL**:导入 + `glBufferStorageMemEXT` 接受(`GL_NO_ERROR`)但每次 `glMapBufferRange``GL_INVALID_OPERATION`persistent 与 plain 都是);`GL_DEVICE_UUID` 不可读 | UNSUPPORTED(扩展字符串缺,入口点可解析) |
| **T0-ahb-blob-transfer**client 分配 `AHardwareBuffer` BLOB → socket 交接 → server Vulkan 导入 + GL 导入) | **OK** 全链:cpu-lock、vk-import+map、GPU copy/fill、GL map persistent+coherent、写回 client 全部字节校验 | **OK** 全链,判决相同(glPersistentCoherent=1gpuRan=1 |
| T3-external-memory-host`VK_EXT_external_memory_host` | UNSUPPORTED(扩展缺) | PARTIAL:导入 + map 往返,但 **GPU 写对宿主映射不可见**(只读档) |
| T3-memfd-cross-process / client-memfd-server-import | UNSUPPORTED | OK / PARTIAL(同样的 GPU 只读 caveat |
**P11 的分档决定**:唯一在两台设备、两个后端上都是完整读写的档是 **T0**——client 分配 `AHardwareBuffer` BLOBserver 以 `VK_ANDROID_external_memory_android_hardware_buffer`Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`Espryt)导入,两侧 persistent+coherent 映射。Adreno 另有 T1Vulkan 路径);Mali 无任何 server 导出路线,host-pointer 导入只读。Caveat:运行域是 `shell` 不是 `untrusted_app`AHB 的 socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,域风险在 memfd/opaque-fd 腿上。
复现:
```sh
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 tools/spikes/extmem_probe/build_android.sh /tmp/extmem-build
S=<serial>; adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe \
&& adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe; echo EXIT=\$?" | tee out-$S.txt
```
判决语义(OK / PARTIAL / FAIL / UNSUPPORTED)与逐腿 trace 格式见 `tools/spikes/extmem_probe/README.md`。主机构建(lavapipe)用来证明探针本身报得对:T1/T3 在 lavapipe 上全 OKT1-gles 在 llvmpipe 上 `GL_OUT_OF_MEMORY` 是 Mesa interop 缺口,不是探针缺陷。
## 3. 边界计数器基线(双设备、双后端、四条 trace)
`MOBILEGL_PIPE_STATS=1` 经 retrace 通道的 `--env` 透传;trace APK 从 `7ef7c7e5` 构建,spike OFF。取每次运行的**最后一个完整 120 帧窗口**。accessor/draw 与 memo 门数字是软件确定的(同一 trace 在两台设备上完全相同:它们数的是代码路径不是硬件),只有墙钟/CPU 时间随设备变。
| trace(窗口内帧数) | 后端 | draws/f | **acc/draw** | buf B/f | tex B/f(发射 box/rect | ubo-global B/f | **ubo-named B/f** | memo 门(hit/miss |
|---|---|---|---|---|---|---|---|---|
| `minecraft-1.21.4-in-world`360 | Espryt | 91.6 | **9.28** | 13.5 K | **635 K**185 box / 0 rect | 16.7 K | 0 | ers 9257/2577etl 10538/1296eub 10720/1114 |
| `minecraft-1.21.4-in-world`360 | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K97 box / 89 rect | 16.7 K | 0 | mfp 0/10994mpm 9240/1754mdt 9120/1874 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world`120memo 冷) | Espryt | 23.2 | 21.04 | 32.6 K | 8.8 K | 1.8 K | 0 | ers 1958/1843etl 722/3079 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world`120memo 冷) | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0vtxc 1.7 K | mpm 1890/895mdt 1573/1212 |
| `improved-transparency-minecraft-26.3`1200 | Espryt | 1320 | **8.44** | 333 K | 0 | 0 | 0 | ers 156925/2791etl 148606/11110eub 148246/11470 |
| `improved-transparency-minecraft-26.3`1200 | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036mpm 134421/2615mdt 156611/1785 |
| `minecraft-1.21.1-neoforge-create-indirect-in-world` | 两者 | — | — | — | — | — | — | 两台设备都失败(§5),且不足 120 帧 |
门缩写:ers = `EsprytRenderState`etl = `EsprytTextureSyncList`eub = `EsprytUnitBindingsEpoch`mfp = `MagmaDrawFastPath`mpm = `MagmaPipelineMemo`mdt = `MagmaDynamicTail``MobileGL/MG_Util/Metrics/PipeStats.h:46-122`)。`accessor-calls` 是约 10 个热入口的静态计数,是每 draw accessor 数的**下界**(站点清单 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`)。
对设计的读法:
- **真机稳态动态 accessor 成本是每 draw 6.59.3 次**(预测区间 1025 的下沿;llvmpipe 的 15.5/20.7 是 memo 冷的)。推送要打败的是 ~8 次 accessor + memo 探测,不是 124/169 的静态调用点数。GO/NO-GO 的 tracker 绝对 ns 上限从这里定。
- **`stage-ubo-named`D-B8**Magma 在 26.3 世界每帧重打包 **331 KB** 具名 UBO 字节,Espryt 直接绑定为 0——host payload 决定的第一个真数字。
- **union box vs region list**vanilla 世界同样 185 次发射,Espryt 的整 box 路径移动 **635 K** 纹素字节/帧,Magma 的 rect 路径 **40 K**16×——"server 选上传形状"这一条的量化依据(Mali 侧的 +6 ms/frame 作业数悬崖在另一个方向)。
复现(一台设备一次;两台必须**串行**,见 §4):
```sh
ANDROID_SERIAL=<serial> MSYS_NO_PATHCONV=1 \
python3 tools/trace_replay/run_android_retrace_local.py \
--case minecraft-1.21.4-in-world --backend DirectGLES \
--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
# 数字在结果目录的 mobilegl.log 里,grep 'MGPipe stats:',取最后一个完整窗口
```
## 4. 桌面数据点与语料事实
- **llvmpipe / lavapipe 动态 accessor**`GuiBatchScenario`14 帧 / 26 drawmemo 冷):Espryt 20.65 / Magma 15.54 次/draw——落在预测区间内,且因场景太短偏高;真机稳态数字见 §3。
- **dirty-surface 面**`python3 scripts/gen_pipe_dirty_surface.py --summary`,本树):`MG_Impl/GLImpl` 41 个文件,926 次 mutator 调用,73 个不同 mutator`RecordError` 一项就占 836 次);92 次(36 个即时发布点、7 个 mutator,绝大多数是 `RecordError`)位于同函数内也到达后端的入口,其余 834 次由紧随的 verb 发布。映射表是 73 条目的问题。
- **读点覆盖**`python3 scripts/gen_pipe.py`):71 条调用(11 screen / 60 context)、63 个 verify payload、61 个 `PipeInputs` 字段;477 行后端读点清单 → 299 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED**。
- **OOM 探测惯用法**41 个 trace fixture 中 0 例——全部语料只有 9 次 `glRenderbufferStorage` 调用散在 5 个 fixture,无一在其后 3 个调用内跟 `glGetError`;语料里真实的成功性检查是 `glCheckFramebufferStatus`。→ `glRenderbufferStorage*` 不 ack。
- **`FramebufferSrgb` / `DepthClamp`**`FramebufferSrgb` 的六个后端读点全部消费一个编译期常量 `false``DepthClamp` 零读点;两者的 `glEnable` 落到 `RenderState.cpp``default:` 分支既不存储也不报 `GL_INVALID_ENUM`41 个 fixture 无一开启任一项(补真存储不会改动任何既有 fixture 的输出)。
- **`GetIntegeri_v` 族**Espryt 实现里是 `GetIntegeri_v` 的 9 个分支 + `GetInteger64i_v` 的 2 个(不是"15 个 case");`GL_COMPUTE_WORK_GROUP_SIZE``GL_Program.cpp``ProgramObject::GetComputeLocalSize` 纯前端回答。
- **payload 尺寸**`MG_Pipe/MGPipeTypes.h``static_assert`arm64 与 x86-64 一致):`MGPDrawInfo` **56**`MGHostSpan` 32、`MGPBindRenderState` **12**`MGPResourceDesc` 88、`MGPFramebufferState` 304、`MGPProgramDesc` 192、`MGPSubData` 72、`MGPPixelPackState` 28、`ResidualValueBlock` **1248**(其中 `RenderStateParameters` 1168)。`SEG_CMD` 按 56 B 固定头定尺:MC 帧 10004000 draw 时每帧 56224 KiB 头字节。
- **persistent map 采纳的既有基线**`dev`MC 26.3Adreno):≥16 MiB 可变 store 定义时采纳为 coherent persistent map 后 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB。P11 的回归上限对着它。
- **Mali 上传作业数悬崖**(Espryt 代码注释记录的既有实测):~100 个精灵 rect 对一个 union box 是 +6 ms/frame。
## 5. Harness 事实与陷阱
1. trace app 从不到达 `MobileGL::DestroyImpl`,所以 `MOBILEGL_PIPE_STATS_FILE` 的 JSON 转储在设备上永远不会写——只有 `mobilegl.log` 里的周期汇总行;短于一个周期的 trace 什么都不产出。`MOBILEGL_PIPE_STATS_PERIOD``458ccde1`)为此而加:需要数字的运行把它设到足够小。
2. `run_android_retrace_local.py` 每棵树共用一个 `.trace-work/android-retrace-result` 根并在每次调用时 `rmtree`,所以两台设备必须从一棵树**串行**跑。
3. `--env` 值里嵌入的 `/data/...` 会被 runner 的 bash.exe 做 MSYS 路径转换(`MSYS2_ARG_CONV_EXCL="/data/*"` 只覆盖开头匹配)——用 `MSYS_NO_PATHCONV=1` 跑。
4. `coherent_as_flush` 管线完好:`--ez coherent_as_flush true``trace_replay_core.cpp``setenv`,独立于 `--env` 透传。
5. **`minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败**Adreno 830Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`MaliSSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。
+57
View File
@@ -0,0 +1,57 @@
# MGPipeMobileGL 前后端拆分
> 状态:**P0 已落地**`feat/disaggregated@458ccde1`,基线 `dev@81b17c0b`)。下一步 P0.5 → P1 → P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`
## 是什么
MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`Espryt = DirectGLES、Magma = DirectVulkan)之间的一份**显式接口**:gallium 形状、句柄寻址、只推不拉。它取代今天后端每 draw 直接读 `MG_State::pGLContext` 的做法,让后端拥有自己的状态机,并在此之上把前后端拆到**两个进程**。
接口本身是可独立交付的产物:即使 IPC 永不上线,`inproc`(同进程第二个 apply 线程)就是 monolith 的渲染线程。
## 架构(一段)
```
应用 GL 调用
→ MG_ImplGL 语义、错误、shadow
→ MG_Impl/Pipe/Tracker:在每条 verb 之前 validate,把变化推成 MGPipe 调用
→ MGPipeScreen / MGPipeContext(两张函数指针表,71 条调用,单一真相源 PipeCalls.def
monolith:直调 backend 函数 split:发射器写 SEG_CMD ring → server applier
→ server 对象表(按 {slot, gen} 句柄索引的数组)+ PipeInputs(后端被推送的状态块)
→ MG_BackendEspryt / Magma),两个后端的 ring / pool / memo / lowering pass 原样不动
← MGPipeCallbacks10 个具名反向回调 + 1 个正向终止符)
```
三种构建/运行形态共用**同一份 backend 实现**`monolith`(默认,接口在进程内直调)、`inproc`(同进程两个线程,CI 形态与渲染线程交付物)、`spawn``fork`+`execve` 出 server 进程,SPSC 共享内存 ring + FlatBuffers 控制面)。
## 文件地图
| 文件 | 内容 |
|---|---|
| `ARCHITECTURE.md` | 已定稿的设计与架构:句柄与世代、调用目录、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程/EGL/平台、构建与纯度门、验证策略 |
| `ROADMAP.md` | P0…P13 阶段表、两条跑道、GO/NO-GO 清单、再基线检查点、仍然开放的问题 |
| `MEASUREMENTS.md` | P0 实测:spike A/B 结论、双设备四条 trace 的边界计数器基线、桌面数据点、语料事实、复现命令 |
代码地图(P0 已落地的部分):
| 路径 | 作用 |
|---|---|
| `MobileGL/MG_Pipe/` | `PipeCalls.def`(目录)、`PipeFields.def`(比对器字段表)、`Coverage.def`(读点覆盖)、`MGPipeTypes.h`payload POD)、`MGPipeHandles.h``MGPipeHostSpan.h``MGPipeCallbacks.h``MGPipe.h``generated/*.inc`G1G7 产物,提交进树) |
| `scripts/gen_pipe.py` | 七个生成器 G1G7`gen_pipe_dirty_surface.py` 前端 mutator 面扫描;`gen_protocol.py` FlatBuffers 头再生成;`check_doc_citations.py` 本目录 `file:line` lint |
| `MobileGL/MG_Remote/` | `Protocol/protocol.fbs`(控制面 schema)、`Transport/``Ring``Doorbell``ShmSegment``FdPassing``Framing``InProcessTransport``ITransport`);仅 `MOBILEGL_BUILD_DISAGGREGATED=ON` 编译 |
| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | 边界计数器(字节 / 动态 accessor 调用 / 六个 memo 门 / 上传形状),`MOBILEGL_PIPE_STATS=1` 开启 |
| `MobileGL/Config.h``MobileGL/ConfigLoader.cpp` | `MOBILEGL_PIPE_*` 八个开关 |
| `tools/spikes/server_stub``android-plugin/app/src/trace/cpp/spawn_spike.cpp` | spike AAndroid 上以 `lib*.so` 打包并从应用进程 exec 第二个原生可执行文件 |
| `tools/spikes/extmem_probe/` | spike B:跨进程外部内存分档探针 |
| `MobileGL/MG_Test/Pipe/``MG_Test/Wire/``MG_Test/Util/PipeStatsTest.cpp` | 目录算术、wire 层五个套件、计数器测试 |
## 术语
- **client / server**:前端进程 / 后端进程;monolith 下是同一进程的两个角色。
- **verb**:会让 server 做事的命令(draw、dispatch、clear、blit、readback、XFB 跨度、query、纹理操作)。推送只发生在 verb 之前的 validate 时刻。
- **CSO**:常量状态对象(render state、vertex elements、sampler、sampler view、shader),client 侧内容寻址,server 侧按句柄缓存。
- **Track V / Track H**:值类读点的迁移(整块 POD 过线)/ 对象类读点的迁移(`SharedPtr<前端对象>` → 句柄)。
- **`MGGen`**server 私有的"我重铸了驱动对象"纪元,永不过线;与句柄里的 client 世代严格分开。
## 历史
本目录此前是一份 328 KB 的实施计划(`PLAN.md`)加 135 KB 的设计竞赛与三视角对抗性评审记录(`REVIEW.md`)。设计已定稿,本次改写只保留设计与架构本身;评审记录、早期草案与修订史留在 git 历史里(`8b31de2f``1794ac94``8349babe``87ee17c6``git show 87ee17c6:docs/Disaggregated/REVIEW.md` 可取回评审记录全文)。更早的一条已放弃分支 `Feat/CS-Delta-IPC` 的逐文件可复用判定见 `8349babe``PLAN.md` §17。
+90
View File
@@ -0,0 +1,90 @@
# MGPipe 路线图
> 状态:P0 已落地(`feat/disaggregated@458ccde1`)。设计见 `ARCHITECTURE.md`,实测见 `MEASUREMENTS.md`。天数是各阶段所含子系统行的求和(低端 / 高端),总计 **267337 人天**(不含 CTS 周转);两个工程师、P7 与 P5/P6/P8 并行约 7–9 个月,真正的约束是两台设备的争用。
## 通用纪律(每个 commit
默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门);**每个门必须能因它存在的理由变红**;Windows 机器不是正确性门;设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议;每阶段出口跑一次五部分门;每阶段性能判据是**逐线程 CPU 时间**。
两条跑道:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止且 monolith 严格好于起点;**IPC 跑道** P5 → P6 → P9 → P10 → P11 → P12。
## 阶段
| 阶段 | 天 | 落地什么 | 验收门 | 依赖 |
|---|---|---|---|---|
| **P0** 卫生、度量、门、骨架 | 9–11 | ✅ 边界计数器(字节 / 动态 accessor / 六个 memo 门 / 上传形状);`PipeCalls.def` 完整目录 + payload POD + 七个生成器 + CI `pipe-gates``gen_pipe_dirty_surface.py``check_doc_citations.py`;八个 `MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架(`SCM_RIGHTS` 第一优先、双 tail 双三元组的 `RingControl`、双向 doorbell、校验型 `Framing``ShmSegment``InProcessTransport`+ `protocol.fbs` + `flatc-check` + `MG_Test/Wire` 五个套件;三个严格 no-op 收益(`GetInteger64i_v`/`GetProgramiv` 退役、`RenderbufferObject::GetLifetimeId()`、D21 XFB 计数槽重键);compute 限制进 `DynamicBackendParameters`spike A、spike Bretrace 通道 `--env` 透传 | ✅ 单元/集成/40 trace 逐名不变;wire 层测试(fd 传递、doorbell、ring、封帧、inproc)绿;两台设备的字节/调用基线在案;spike A/B 出结论;citation lint 绿 | — |
| **P0.5** 值头与制品头抽取 | 69 | `MG_Pipe/MGPipeValueTypes.h``RenderStateParameters``SamplerParameters``PixelStoreParameters``VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言 | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
| **P1** `PipeInputs` 替换与 verify harness | 1013 | `MG_Backend/MGPipe/PipeInputs.h`Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | 1826 | `MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门;`MGPipeRenderStateSpans` + G7 setter 一致性测试;`CsoCache`64 项,键 = pipeline 子集);`create/bind_render_state` + `set_dynamic_state`Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state``set_patch_state``set_vertex_attrib_defaults``set_residual_value_state` + `ResidualValueBlock` 绊线;**第一片 Track H**Espryt 0b`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针);`MOBILEGL_PIPE_LEGACY_MEMOS`;补 `FramebufferSrgb`/`DepthClamp` 存储 | 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照 | P1 |
| **P3a** handle wave 1Espryt):buffer、VAO | 1823 | 7 个 `BufferBackendOps``resource_*``buffer_subdata_resident`(可 null)、`resource_flush_range``resource_readback``map_persistent`(不碰实现);pool 与延迟释放原样搬;vertex elements 三件(两个视图都带);`set_vertex_buffers``baseInstance` 显式字段);`set_index_buffer`Adreno SIGSEGV workaround 保留 | 全套门;buffer/VAO 族场景(`LargeArenaAdoption``StorageBufferRegrow` 发布 `map-persistent-roundtrips``VertexAttribBinding``MultiDraw``PrimitiveRestart`…);Create/rd12/26.3/sodium traceMC 26.3 在 Adreno 上 p99 不变。**再基线检查点 1:超过 27 天必须重定基线** | P2 |
| **P4a** handle wave 2Espryt):FBO / 纹理 / sampler / program 身份与描述符 | 2634 | `set_framebuffer_state`(解析后的 `ReadSurface`、内联格式、`ContentHash``{0,1}`);sampler CSO(含 `borderColorForm`);sampler view + `set_texture_params``set_sampler_views`/`bind_sampler_states`/`set_shader_images`shader CSOSPIR-V + 归档);`set_draw/dispatch_program``set_global_constants``CompositeResolver`;纹理/renderbuffer 的 `resource_*`。emulation 在 split 下显式 Fatal 直到 P8 | 全套门;framebuffer/纹理/program 族场景;**新增"只作 attachment / image 单元 / CopyImage 端点的纹理其 `glTexParameter` 生效"场景(落地前必须红)**;两台设备 `KHR-GL46.direct_state_access.framebuffers*` 与整个 `packed_pixels` 块(~3300 例,句柄复用压力测试)。**再基线检查点 1b:超过 39 天** | P3a |
| **P5** 传输 + inproc applier + 发射表 | 12 | `MG_Remote/Client` 发射表;`Server/PipeApplier``ServerLoop``mgl-srv-io` + `mgl-srv-apply`);`Init.cpp` 单一 hook 装 `BackendObject_Remote``MGPCaps` 快照;阻塞 `read_pixels`client 侧保守 `MarkGpuWritten`**client 侧块粒度 persistent-map 推送**`InProcessTransport` 走与 spawn 相同的 G3 编解码;trace-replay `SPLIT` 后缀 + `-DTRACE_TRANSPORT=``MOBILEGL_TRANSPORT` 解析 | `DirectGLES.Split.*(ClearThenReadPixels|Triangle)``inproc` 下绿;OpenRA trace split SSIM ≥ 0.99`PersistentCoherentMapScenario` 绿;两个角色峰值 RSS 在案;`persistent-map-push` 出数;未迁移字段读 = `Fatal{UnmigratedPipeInput}`。**第 99 天:首个 IPC 帧(缩减路径)** | P4a |
| **P6** spawn transport | 5 | `SocketTransport`socketpair + fork/execveenvp 剔除 + 强制 monolith 双保险);`ServerMain``MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch | P5 全部测试在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。**第 104 天:首个跨进程帧** | P5 |
| **P3b / P4b** 深化(Espryt | 2938 | memo 重键(`ResolvedDrawBuffers``ResolvedTextureBindingMemo``SamplerPassMemo`、image sweep、program registry…);server 删 `g_unitTextureSyncList`/`g_fboTextureSyncList`/`DirectGLES.cpp` 的 ~115 行 unit-bindings epoch 推导,**同时**在 Tracker 落地集合 hash 抑制器;dirty 归属反转(按存储属主键控的发射游标);`MGPSubRegion` 跨步描述符改造;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`raw-depth-fetch sampler 原生化;回读 / pack state | ~25 个纹理场景、21 个 program 场景 + `MG_Test/ShaderTranspiler`;两台设备 `KHR-GL46.texture_*`/`internalformat.texture2d.*`/`shader_image_*`/`packed_pixels` 在 pull 基线 0.5 pp 内;每一个 Iris trace**`TextureUploadShapeScenario`**(形状金标,Mali 帧时增量必须发布);view/owner 发射游标别名场景;verify 保留模式下 subdata 形状逐项相等;XFB 场景 + `capture_special_interleaved_test` | P4a |
| **P7** DirectVulkanMagma)全量迁移 | 80104 | §5.5 其余 10 个子系统(子系统 1、4 已在 P2):`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化(~120 行删除);具名 UBO host payloadD-B8`kCapNeedsHostUboBytes`);blit/depth-mipmap 内部 shader 烘焙 + 新鲜度测试;`VertexInputStateFactory` 裸指针写回删除;D18 容器纪律原样保留 | 集成 + 40 trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;**`nm -D libMobileGLServer.so | grep glslang` 为空**Iris trace 上 `stage-ubo-named` 逐帧字节发布;两台设备 CTS 0.5 pp 内。**再基线检查点 2:中点(第 40–52 工作日)完成子系统 < 40% 立即重定基线** | P0.5、P2;可与 P5/P6/P8 并行 |
| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 12–16 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,各带逐站点 reconcile);`MGHostSpan` split 填法;`Server/IndexHostMirror`CopyImage 镜像搬到 client`draw_vbo` 收编 multi-draw 族(分档仍在 server);viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;G3 分块路径;无 present fence tick + 无 present split 用例;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'``'^DirectGLES\.'` 逐名相同(DirectVulkan 同);40 trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` Create fixture`ClientArrayAfterComputeWriteScenario` 绿(去掉等待必须见几何缺失);`create-indirect``roundtrips-per-frame` 读零;`index-mirror-bytes`/`index-bytes-shipped` 逐用例发布。**第 145 天:全功能 split** | P6、P3b/P4b |
| **P9** 反向通道 | 10 | `SEG_REPLY` slot 池;阻塞 `read_pixels`PBO 回读 fire-and-forget`OnGpuWritten` 收窄;`OnBufferWriteback` 按操作级批处理 + epoch 排序;`OnXfbScatterReady` + client scatter`OnTextureWriteback``OnMipLevelsGenerated`;纹理拉取四条缓解 + 终止符;`OnGlError` 有序 + `glBufferStorage` 的 ack`OnCapsInvalidated``OnSurfaceChanged``OnLog` 分级 + 速率限制;`SEG_EVENT` 溢出策略 | 回读/XFB 场景在 split 下绿;`TextureRemintPullScenario` 绿且含无解用例(终止符前表现为 apply 线程挂死/超时);拉取计数逐 trace 发布;故障注入:credit 阻塞时灌满 `SEG_EVENT`、日志洪泛下注入 link 失败 | P8 |
| **P10** sync / query / present 节奏 | 6 | client 铸造 sync/query handle;轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`;fence 完成度来自真的逐 fence 退休;DirectGLES 非 present fence tick`present` 1:1credit 默认 1 + 叠加公式;roundtrip 计数器与输入延迟直方图;三个独立 `dev` monolith 修复(`glEndTransformFeedback` 无限 `ClientWaitSync` → 推迟到首次读;`glDispatchCompute` 三次 `GetIntegeri_v` 校验 → 读 `CompileEnv`D21 已落地) | query/XFB/`AsyncCompile` 场景在 split 下绿;40 个用例上 draw/state/upload 路径 roundtrip 读零,条件渲染与阻塞 query 次数逐用例发布;零 timeout 轮询在有界时间退出;`bench.sh` 配对 A/B:两侧都关采纳时 split 帧时在 monolith 10% 内,输入延迟 p50/p99 在案 | P9 |
| **P11** persistent map 与 ≥16 MiB 采纳 | 8 | POST 探针档位选择(T0 主攻,Adreno 可选 T1,T2 回退);`SEG_ADOPT` 生命周期绑 `completedFrameSerial``MOBILEGL_IPC_ADOPT_TIER` 负面对照 | `LargeArenaAdoptionScenario` 在所选档下绿;26.3 与两个 Create fixture SSIM ≥ 0.99`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips`Adreno 830 上 p99 帧时与峰值 RSS 对 monolith 采纳基线(163→21 ms / 40→115 fps / ~400 MB**回归不超过 10%**;若 T2 成为某设备的永久答案,其实测代价写进文档 | P10、spike B(已答) |
| **P12** Android 生产窗口路径 | 10 | `android:process=":mgl"` Service 收 Java `Surface``ANativeWindow_fromSurface`server 生命周期绑 ActivityFCL 用户 env 与 plugin APK V2 开关表接线 | Minecraft 经 FCL 在 spawn 模式下于 Adreno 830 双后端入世界;配对 reboot-clean bench + 输入延迟直方图;杀 server 产生干净 device-lost latchSIGKILL 故障注入 | P11 |
| **P13** 退役 pull 路径 | 812 | 删 `SnapshotFromGLContext()` 非 verify 分支、`MGB_CTX``MOBILEGL_PIPE_PUSH``MOBILEGL_PIPE_LEGACY_MEMOS`;保留 `MOBILEGL_PIPE_VERIFY`MGPipe recorder 金标模式;删 `set_residual_value_state``MG_Backend``MG_State` include 收缩到 `MGPipeValueTypes.h`;在计数器活着的情况下重调幸存缓存容量(`VaoDrawMemo` 2048、`SetupDrawSnapshot` 4、pipeline memo 8、`syncedTextureMemo` 8)并变成带 env 覆盖的调优参数;最终符号/尺寸/CPU 报告 | `static_assert(sizeof(ResidualValueBlock) == 0)` 编译通过;三道纯度门在非 verify 构建上转绿;verify 构建仍零分歧;recorder 金标在 40 trace 上建立;全套门(集成 × 2 后端 × {monolith, split}、单元、40 trace、两台设备 CTS 在 `81b17c0b` 基线 0.5 pp 内);**monolith 逐线程 CPU 在两台设备 p50/p99 上不差于 P0 基线** | P7、P8、P12 |
累计(低端):P0 9 → P0.5 15 → P1 25 → P2 43 → P3a 61 → P4a 87 → P5 99 → P6 104 → P3b/P4b 133 → P8 145 → P9 155 → P10 161 → P11 169 → P12 179 → P13 187P7 另 80104,单跑道累计 267。
**CTS 周转单独计价**`gl44to46` 约 56,271 例。逐阶段只跑该阶段可能影响的具名块(P4a `packed_pixels`、P3b/P4b `texture_*`/`shader_image_*`、P9 `transform_feedback*`);完整 caselist 只在五个架构边界(P0.5、P3a、P4a、P3b/P4b、P13)与每次合并 `dev` 之前跑,放 CI 不放关键路径。若周转仍主导排期,加宽估时而不是削弱门。
## 里程碑
- **第 25 天(P1 出口)**verify harness 逐 draw 逐字段证明"推送等价于拉取"。零产品风险,**不是** GO/NO-GO。
- **第 43 天(P2 出口):GO/NO-GO**
- 第 99 天:首个 `inproc` IPC 帧(缩减路径);第 104 天:首个跨进程帧;第 145 天:全功能 split;第 187 / 267 天:三道纯度门转绿。
## 第 43 天 GO/NO-GO 清单
手上必须有:
- [ ] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧)
- [ ] 两个后端上都已推送的渲染状态,`SyncRenderState` 693 行一行未动
- [ ] 两片 Track H 的实测单位成本(Espryt 0b、Magma 子系统 4
- [ ] 两台设备(Adreno 830 `35d0befa`、Mali `3B159D009VZ00000`reboot-clean 配对的逐线程 CPU 时间增量,p50 与 p99
- [ ] tracker 每 draw 的**绝对 ns**(上限从设备基线定:稳态每 draw 6.59.3 次 accessor + memo 探测,见 `MEASUREMENTS.md`
- [ ] Blaze3D blend-toggle 微基准(enable/draw/disable/drawMC batch 速率)
- [ ] 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开
判据与出口:
- **继续**:两台设备 p50 与 p99 逐线程 CPU 增量都不为负;tracker 绝对 ns 在上限内;Track H 单位成本不超出估计的 50%。按两条跑道推进。
- **收缩为 headless 工装用途或重新评估**:任一判据落空。**不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev`MGPipe 收缩为 `MG_Test` mock 后端 → MGPipe recorder(给 trace_replay 一种记录已解析状态的录制格式)+ `inproc` 渲染线程实验;IPC 跑道搁置到出现新判据。
- 沉没成本:P0 与 P0.5 无论走哪条路都要花(后者本身是 monolith 净收益);真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天,NO-GO 分支下仍留下上述产物。
## 再基线检查点
| 触发 | 动作 |
|---|---|
| P3a > 27 天 | "窄句柄化"的前提错了,P4a 开始前重定基线 |
| P4a > 39 天 | 同上 |
| P7 中点(第 40–52 工作日)完成子系统 < 40% | 立即重定基线(P3a 的检查点发现不了 Magma 特有的超期) |
任一触发,先跑 `inproc` 的证伪数字再决定是否继续。
## 仍然开放的问题
P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。
1. **client 侧 dirty 走查的真实每 draw CPU 代价。** 拉取基线已实测为每 draw 6.59.3 次 accessor + memo 探测;推送要在这个数字下净减少。P2 的头号数字,逐线程 CPU + 绝对 ns,两台设备。
2. **真实语料上纹理重铸拉取的发生率。** `ImageBindableHint` 预防主因,但整格式再生在普通 `glTexImage` 格式变更上就触发。若 MC/Iris fixture 上非平凡,保留 LRU 从默认 0 升为强制并拿真预算。
3. **spike B 的 `untrusted_app` 域复核。** 两台设备的分档在 `shell` 域测得;T0 的 AHB socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,风险在 memfd/opaque-fd 腿上。从应用进程再跑一次 `extmem_probe`spike A 的 exec 钩子已可用)。
4. **渲染状态的 wire 粒度。** chunk 划分定下来后,CSO LRU 容量(暂定 64)与 `set_dynamic_state` 的 chunk 粒度由计数器定。
5. **`FramebufferSrgb` / `DepthClamp` 的拍板。** 事实已清(无存储、`glEnable` 静默吞掉、六个读点恒 false、41 个 fixture 无一开启);建议在 chunk 表冻结前补真存储并把 `FramebufferSrgb` 划进 pipeline 半边。由计划所有者拍板,**拍板前不冻结 chunk 表**。
6. **具名 UBO host payload 的形状(D-B8)。** 第一个数字已有:Magma 在 26.3 世界每帧重打包 331 KB 具名 UBO 字节,Espryt 为 0。要么冻结现在的第二变长尾形状,要么走备选(Magma 直接描述符绑定常驻 `VkBuffer` range,独立 `dev` PR + Iris 性能门)。
7. **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross pass 流水线、ESSL 转译缓存、格式处理器、POST 探针;client 需要 glslang phase A/B 与反射层。P0.5 解决了 `ProgramObject.h` 一处,`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。
8. **一份反射归档能否服务三个消费者**Espryt 读前端表、Magma 跑 SPIRV-Reflect、`DirectVulkan.cpp``glGetProgramResource*` 又反射一遍)。
9. **viewport-array 回放能否塞进一次 `draw_vbo`**`EndViewportRoutingPasses``InvalidateSyncedRenderState`,各遍之间观察到的状态是否与今天一致未验证。
10. **`ResidentSubData` 的不对称怎么收口。** null 项保住今天的行为;给 Magma 补真实现是行为变更,独立 `dev` PR。
11. **`SEG_STAGE` 的上限。** 六类新字节需要 P8 之后用 MC in-world 与 Create 两类 fixture 的 `stage-*` 计数器给 p99 占用;G3 分块路径需要设计与测试。
12. **P13 之后 split-only 渲染 bug 的 server 侧第二意见。** verify 构建 + recorder 只覆盖推送内容,不覆盖后端对它的解释。
13. **烘焙后的内部 shader 能否在没有活 `ProgramObject` 的情况下表达 uniform location 与 UBO 布局。** 未做原型。
14. **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。
15. **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口**compute 写的 indirect buffer)。独立 `dev` 问题,拆分不得借机顺手修。
16. **索引宿主镜像的实际内存占用。** MC/Sodium/Iris 语料里 element-array buffer 总量未测;若显著超 64 MiB,退化路径的频率与代价必须实测。
17. **create-indirect fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;它是 P3a/P8 验收清单里的用例,需要先在 `dev` 上修。
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
# MobileGL - scripts/check_doc_citations.py
# 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
"""Resolve every `path:line` citation in a set of markdown documents.
A design document that cites the code is only as good as its line numbers, and a wrong one
is worse than none: it sends the next reader to a function that does something else. The
disaggregation plan's first draft cited SamplerObject.h:468-492 for a struct that lives at
:72-96 in a 160-line file, and nothing caught it.
So every `File.h:123` and `File.cpp:123-456` in the given documents is resolved against a
git revision - the file must exist there and must have at least that many lines. Bare file
names are resolved by basename, which is how the plan spells most of its citations; an
ambiguous basename is reported rather than guessed.
python3 scripts/check_doc_citations.py docs/Disaggregated/*.md
python3 scripts/check_doc_citations.py --rev 81b17c0b --strict docs/Disaggregated/*.md
Exits non-zero only with --strict, so it can be wired into CI as a warning first.
"""
import argparse
import os
import re
import subprocess
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# `Managers.cpp:4340-4390`, `MobileGL/MG_State/.../RenderState.h:263`, `:12-13` is NOT
# matched on purpose: a citation with no file name cannot be checked, only guessed.
CITATION_RE = re.compile(
r"(?<![\w/.-])((?:[\w.-]+/)*[\w.-]+\.(?:h|hpp|cpp|cc|c|py|def|inc|md|json|yml|yaml|txt|fbs))"
r":(\d+)(?:-(\d+))?")
def git(args, rev_root=REPO_ROOT):
return subprocess.run(["git", "-C", rev_root] + args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=False).stdout.decode("utf-8", "replace")
class Tree(object):
def __init__(self, rev):
self.Rev = rev
listing = git(["ls-tree", "-r", "--name-only", rev]).splitlines()
if not listing:
sys.exit("check_doc_citations: revision %s has no files (bad revision?)" % rev)
self.Paths = set(listing)
self.ByBasename = {}
for path in listing:
self.ByBasename.setdefault(os.path.basename(path), []).append(path)
self.LineCounts = {}
def Resolve(self, cited):
if cited in self.Paths:
return [cited]
candidates = self.ByBasename.get(os.path.basename(cited), [])
if len(candidates) > 1 and "/" in cited:
candidates = [p for p in candidates if p.endswith(cited)] or candidates
return candidates
def LineCount(self, path):
if path not in self.LineCounts:
blob = git(["show", "%s:%s" % (self.Rev, path)])
# A file with no trailing newline still has that last line.
count = blob.count("\n") + (1 if blob and not blob.endswith("\n") else 0)
self.LineCounts[path] = count
return self.LineCounts[path]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("documents", nargs="+", help="markdown files to check")
parser.add_argument("--rev", default="HEAD", help="git revision the citations point into")
parser.add_argument("--strict", action="store_true", help="exit 1 when a citation does not resolve")
args = parser.parse_args()
tree = Tree(args.rev)
checked = 0
problems = []
for document in args.documents:
if not os.path.exists(document):
problems.append("%s: no such document" % document)
continue
with open(document, "r", encoding="utf-8", errors="replace") as handle:
lines = handle.read().splitlines()
for number, line in enumerate(lines, start=1):
for match in CITATION_RE.finditer(line):
cited, first, last = match.group(1), int(match.group(2)), match.group(3)
last = int(last) if last else first
checked += 1
where = "%s:%d: `%s`" % (document, number, match.group(0))
candidates = tree.Resolve(cited)
if not candidates:
problems.append("%s -> no such file at %s" % (where, args.rev))
continue
if len(candidates) > 1:
problems.append("%s -> ambiguous: %s" % (where, ", ".join(sorted(candidates))))
continue
if last < first:
problems.append("%s -> inverted line range" % where)
continue
count = tree.LineCount(candidates[0])
if last > count:
problems.append("%s -> %s has %d lines at %s"
% (where, candidates[0], count, args.rev))
print("check_doc_citations: %d citations in %d document(s) against %s, %d problem(s)"
% (checked, len(args.documents), args.rev, len(problems)))
for problem in problems:
print(" %s" % problem)
if problems and args.strict:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+626
View File
@@ -0,0 +1,626 @@
# Backend read inventory -> delta catalog matrix
> GENERATED by `scripts/extract_backend_read_inventory.py` - do not edit by hand.
> Acceptance rule: zero UNMAPPED rows before the delta path goes live (P5).
## Summary
- scanned files: 57 (MG_Backend/**.cpp|.h)
- total read points: 477
- pGLContext accesses: 293
- SharedPtr<MG_State wire args: 167
- BufferBackendOps touchpoints: 17
- UNMAPPED: 0
## Delta kind distribution
| delta kind | count |
|---|---|
| handle-ify (wire handle) | 167 |
| RenderStateBlob | 99 |
| ObjectBind(Texture) | 33 |
| ObjectBind(Buffer) | 29 |
| ObjectBind(BufferRange) | 24 |
| FboAttach | 19 |
| Buffer ops delta | 17 |
| XfbOp | 15 |
| ObjectBind(Image) | 14 |
| ObjectBind(VAO) | 12 |
| ObjectBind(Program) | 10 |
| TexParam | 9 |
| Texture state | 7 |
| PixelStoreBlob | 6 |
| client-resolved (error queue) | 6 |
| ProgramPublish | 3 |
| client-resolved (validation) | 3 |
| CurrentAttrib | 2 |
| client-resolved (compile env) | 2 |
## Per-file hits
### `MobileGL/MG_Backend/BackendObject.h` (9 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 34 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> Texture;` |
| 35 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;` |
| 158 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferfv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 160 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 162 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 164 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 168 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 169 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
| 186 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void (*GetTextureImage)(const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
### `MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp` (1 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 849 | BufferBackendOps | - | Buffer ops delta | `BufferImpl::RegisterBufferBackendOps();` |
### `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp` (142 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 55 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static SharedPtr<MG_State::GLState::SamplerObject> g_rawDepthFetchSamplerState;` |
| 142 | pGLContext | GetFramebufferBindingSlot | FboAttach | `std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;` |
| 182 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool NeedsRawDepthFetchSampler(const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject,` |
| 260 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 356 | pGLContext | GetTouchedBufferBindingPointCount | ObjectBind(BufferRange) | `auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);` |
| 369 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);` |
| 428 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);` |
| 461 | pGLContext | GetTouchedBufferBindingPointCount | ObjectBind(BufferRange) | `MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);` |
| 464 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();` |
| 473 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);` |
| 480 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,` |
| 517 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(target).GetBoundObject();` |
| 537 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,` |
| 661 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 712 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::BufferObject> buffer;` |
| 739 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> scatterProgram;` |
| 891 | pGLContext | GetTransformFeedbackCapturedVertices | XfbOp | `static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());` |
| 986 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
| 1006 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,` |
| 1199 | SharedPtr<MG_State | - | handle-ify (wire handle) | `BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao) {` |
| 1215 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncCurrentVAO(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,` |
| 1237 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& program) {` |
| 1243 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
| 1274 | pGLContext | GetCurrentVertexAttribute | CurrentAttrib | `const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);` |
| 1298 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
| 1372 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 1385 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 1413 | pGLContext | GetTextureContextId | Texture state | `const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();` |
| 1414 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
| 1457 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>* slot = nullptr;` |
| 1503 | pGLContext | GetTextureContextId | Texture state | `keys.contextId = MG_State::pGLContext->GetTextureContextId();` |
| 1505 | pGLContext | GetMaxTouchedTextureUnit | ObjectBind(Texture) | `keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();` |
| 1506 | pGLContext | GetSamplingResolutionGeneration | TexParam | `keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 1552 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);` |
| 1700 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));` |
| 1792 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `const auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));` |
| 1998 | pGLContext | GetRenderStateParametersVersion | RenderStateBlob | `Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();` |
| 2012 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
| 2041 | pGLContext | GetViewport | RenderStateBlob | `IntVec4 backendViewport = MG_State::pGLContext->GetViewport();` |
| 2124 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);` |
| 2680 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {` |
| 2800 | pGLContext | GetPatchVertices | RenderStateBlob | `static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) \|\|` |
| 2802 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `MG_State::pGLContext->GetPatchDefaultOuterLevel()) \|\|` |
| 2804 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {` |
| 2853 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncAndBindFramebufferObject(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 2902 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
| 2905 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);` |
| 2915 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
| 2926 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
| 2973 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool ResolveAndBindUnitTextures(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
| 2998 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 3113 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {` |
| 3162 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();` |
| 3250 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {` |
| 3312 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());` |
| 3322 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,` |
| 3423 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);` |
| 3521 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 3547 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject =` |
| 3603 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
| 3653 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();` |
| 3762 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackActive() \|\|` |
| 3763 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
| 3767 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
| 3862 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,` |
| 3941 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,` |
| 4003 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();` |
| 4029 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) {` |
| 4034 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
| 4097 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;` |
| 4156 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 4313 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {` |
| 4314 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static const SharedPtr<MG_State::GLState::BufferObject> none;` |
| 4315 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
| 4331 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
| 4332 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {` |
| 4337 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();` |
| 4377 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();` |
| 4505 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
| 4544 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();` |
| 4615 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 4646 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 4647 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
| 4717 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 4748 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 4749 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
| 4908 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 4949 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 5194 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackActive() &&` |
| 5195 | pGLContext | IsTransformFeedbackPaused | XfbOp | `!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {` |
| 5811 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 5812 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer, GLint srcX0, GLint srcY0,` |
| 5979 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),` |
| 5980 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,` |
| 5990 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 5991 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
| 6042 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto unit = MG_State::pGLContext->GetActiveTextureUnit();` |
| 6043 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 6120 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
| 6267 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
| 6310 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
| 6335 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
| 6561 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
| 6586 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
| 6634 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `Uint activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
| 6635 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit)` |
| 6729 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
| 6730 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)` |
| 6794 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
| 6867 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto unitIndex = MG_State::pGLContext->GetActiveTextureUnit();` |
| 6868 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& unit = MG_State::pGLContext->GetTextureUnitObject(unitIndex);` |
| 6994 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {` |
| 7059 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static Bool CanMirrorCopyImageShadow(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
| 7260 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 7266 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 7271 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 7288 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7297 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7306 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7315 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7324 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7333 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 7352 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 7357 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 7408 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) return;` |
| 7409 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
| 7456 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool IsWidenedNamedDrawBuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 7514 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 7534 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 7551 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 7571 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 7605 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
| 7613 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 8595 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 8825 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 9092 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;` |
| 9135 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 9245 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();` |
| 9248 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)` |
| 9471 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
| 9561 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 10114 | BufferBackendOps | - | Buffer ops delta | `BufferImpl::RegisterBufferBackendOps();` |
### `MobileGL/MG_Backend/DirectGLES/DirectGLES.h` (6 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 60 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 62 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 64 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 66 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 70 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 71 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
### `MobileGL/MG_Backend/DirectGLES/Managers.cpp` (39 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 443 | SharedPtr<MG_State | - | handle-ify (wire handle) | `String source, const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
| 495 | BufferBackendOps | - | Buffer ops delta | `using MG_State::GLState::BufferBackendOps;` |
| 1333 | BufferBackendOps | - | Buffer ops delta | `const BufferBackendOps g_glesBufferBackendOps = {` |
| 1356 | BufferBackendOps | - | Buffer ops delta | `void RegisterBufferBackendOps() {` |
| 1357 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps);` |
| 1363 | BufferBackendOps | - | Buffer ops delta | `void UnregisterBufferBackendOps() {` |
| 1364 | BufferBackendOps | - | Buffer ops delta | `if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) {` |
| 1365 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(nullptr);` |
| 1379 | BufferBackendOps | - | Buffer ops delta | `UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch` |
| 1453 | SharedPtr<MG_State | - | handle-ify (wire handle) | `GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {` |
| 2264 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject) {` |
| 2496 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count) {` |
| 2787 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 3604 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 3606 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
| 3607 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 3614 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 3707 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 3735 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId == MG_State::pGLContext->GetTextureContextId() &&` |
| 3736 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration == MG_State::pGLContext->GetSamplingResolutionGeneration() &&` |
| 3806 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
| 3807 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 4661 | pGLContext | GetTextureContextId | Texture state | `m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();` |
| 4662 | pGLContext | GetSamplingResolutionGeneration | TexParam | `m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 4670 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 4781 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {` |
| 5280 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
| 5385 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
| 5409 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {` |
| 5520 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {` |
| 6155 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);` |
| 7118 | pGLContext | GetPatchVertices | RenderStateBlob | `? MG_State::pGLContext->GetPatchVertices()` |
| 7126 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `? MG_State::pGLContext->GetPatchDefaultOuterLevel()` |
| 7129 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `? MG_State::pGLContext->GetPatchDefaultInnerLevel()` |
| 7229 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
| 8262 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {` |
| 8450 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {` |
| 8620 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject) {` |
| 8676 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
### `MobileGL/MG_Backend/DirectGLES/Managers.h` (18 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 501 | BufferBackendOps | - | Buffer ops delta | `void RegisterBufferBackendOps();` |
| 502 | BufferBackendOps | - | Buffer ops delta | `void UnregisterBufferBackendOps();` |
| 511 | SharedPtr<MG_State | - | handle-ify (wire handle) | `GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);` |
| 679 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject);` |
| 681 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);` |
| 953 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 959 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 960 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 966 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 967 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 973 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);` |
| 1127 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
| 1151 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,` |
| 1156 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);` |
| 1531 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);` |
| 1639 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);` |
| 1817 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);` |
| 1846 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);` |
### `MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp` (8 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 44 | pGLContext | GetPrimitiveRestartIndex | RenderStateBlob | `return MG_State::pGLContext->GetPrimitiveRestartIndex();` |
| 50 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
| 51 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);` |
| 86 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 92 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {` |
| 93 | SharedPtr<MG_State | - | handle-ify (wire handle) | `static const SharedPtr<MG_State::GLState::BufferObject> none;` |
| 94 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = MG_State::pGLContext->GetBoundVertexArray();` |
| 355 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,` |
### `MobileGL/MG_Backend/DirectGLES/Utils.cpp` (2 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 2297 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 2301 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
### `MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp` (2 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 389 | pGLContext | InvalidateCompileEnv | client-resolved (compile env) | `MG_State::pGLContext->InvalidateCompileEnv();` |
| 789 | pGLContext | InvalidateCompileEnv | client-resolved (compile env) | `MG_State::pGLContext->InvalidateCompileEnv();` |
### `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp` (25 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 280 | pGLContext | ValidateProgramName | client-resolved (validation) | `if (!MG_State::pGLContext->ValidateProgramName(program)) {` |
| 283 | pGLContext | GetProgramObject | ProgramPublish | `auto& programObject = MG_State::pGLContext->GetProgramObject(program);` |
| 288 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 369 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 376 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 383 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 390 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 412 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 475 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
| 543 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 596 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 706 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 712 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 717 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 739 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));` |
| 765 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 770 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);` |
| 816 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
| 849 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,` |
| 886 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 1010 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 1112 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 1113 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
| 1337 | pGLContext | GetTransformFeedbackPausedPrimitiveCounter | XfbOp | `primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -` |
| 1384 | pGLContext | GetTransformFeedbackPausedPrimitiveCounter | XfbOp | `MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;` |
### `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h` (7 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 36 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 38 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 40 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 42 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,` |
| 76 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,` |
| 77 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,` |
| 103 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp` (23 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 161 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::TextureObjectMipmap> MakePlaceholderTextureObject(TextureTarget target,` |
| 505 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 508 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;` |
| 554 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 781 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();` |
| 809 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
| 820 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 846 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 873 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> texture;` |
| 1015 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
| 1188 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));` |
| 1193 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);` |
| 1294 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
| 1303 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> placeholder;` |
| 1392 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(` |
| 1433 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(` |
| 1477 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;` |
| 1597 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetUnboundStorageImageTexture(` |
| 1661 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);` |
| 1849 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();` |
| 1937 | pGLContext | GetImageTextureBinding | ObjectBind(Image) | `const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);` |
| 2012 | pGLContext | GetBufferBindingPointCount | ObjectBind(BufferRange) | `static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));` |
| 2017 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h` (7 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 165 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(` |
| 191 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(` |
| 213 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,` |
| 305 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;` |
| 308 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;` |
| 317 | SharedPtr<MG_State | - | handle-ify (wire handle) | `mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp` (9 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 50 | BufferBackendOps | - | Buffer ops delta | `using MG_State::GLState::BufferBackendOps;` |
| 104 | BufferBackendOps | - | Buffer ops delta | `const BufferBackendOps g_vulkanBufferBackendOps = {` |
| 130 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);` |
| 137 | BufferBackendOps | - | Buffer ops delta | `if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) {` |
| 138 | BufferBackendOps | - | Buffer ops delta | `MG_State::GLState::SetBufferBackendOps(nullptr);` |
| 255 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {` |
| 496 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {` |
| 566 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
| 614 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h` (4 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 146 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
| 151 | SharedPtr<MG_State | - | handle-ify (wire handle) | `Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,` |
| 165 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);` |
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp` (11 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 57 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;` |
| 238 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
| 261 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
| 319 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture) {` |
| 325 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;` |
| 350 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;` |
| 372 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
| 392 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
| 404 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
| 409 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {` |
| 461 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h` (4 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 127 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::ITextureObject>& texture);` |
| 135 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
| 148 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
| 150 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject>& outTexture);` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp` (5 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 342 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {` |
| 613 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
| 965 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
| 1111 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));` |
| 1595 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h` (1 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 353 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp` (2 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 808 | pGLContext | GetTextureObject | Texture state | `const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());` |
| 951 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp` (140 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 462 | pGLContext | GetViewportIndexed | RenderStateBlob | `const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);` |
| 467 | pGLContext | GetDepthRangeIndexed | RenderStateBlob | `const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);` |
| 522 | pGLContext | GetBlendColor | RenderStateBlob | `const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();` |
| 555 | pGLContext | GetPolygonOffsetUnits | RenderStateBlob | `const Float constantFactor = MG_State::pGLContext->GetPolygonOffsetUnits();` |
| 556 | pGLContext | GetPolygonOffsetFactor | RenderStateBlob | `const Float slopeFactor = MG_State::pGLContext->GetPolygonOffsetFactor();` |
| 569 | pGLContext | GetLineWidth | RenderStateBlob | `Float lineWidth = MG_State::pGLContext->GetLineWidth();` |
| 648 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);` |
| 649 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);` |
| 1242 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));` |
| 1246 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));` |
| 1302 | pGLContext | RecordError | client-resolved (error queue) | `MG_State::pGLContext->RecordError(` |
| 2770 | pGLContext | GetClampReadColor | RenderStateBlob | `const GLenum clampMode = MG_State::pGLContext->GetClampReadColor();` |
| 2987 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 3419 | SharedPtr<MG_State | - | handle-ify (wire handle) | `? SharedPtr<MG_State::GLState::BufferObject>{}` |
| 3446 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) \|\|` |
| 3447 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);` |
| 3924 | pGLContext | GetCurrentVertexAttribute | CurrentAttrib | `const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);` |
| 4058 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
| 4074 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::BufferObject>& indexBufferShared =` |
| 4813 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;` |
| 4814 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;` |
| 4815 | pGLContext | GetRenderStateParameters | RenderStateBlob | `return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;` |
| 4826 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();` |
| 4938 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
| 4982 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
| 5160 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);` |
| 5161 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);` |
| 5163 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&` |
| 5166 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);` |
| 5168 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;` |
| 5169 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);` |
| 5175 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 5187 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);` |
| 5188 | pGLContext | GetStencilState | RenderStateBlob | `const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);` |
| 5190 | pGLContext | GetPolygonModeFront | RenderStateBlob | `MG_Util::ConvertPolygonModeToVkEnum(MG_State::pGLContext->GetPolygonModeFront());` |
| 5257 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),` |
| 5258 | pGLContext | GetMinSampleShadingValue | RenderStateBlob | `.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),` |
| 5264 | pGLContext | GetPatchVertices | RenderStateBlob | `.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),` |
| 5268 | pGLContext | GetCullFaceMode | RenderStateBlob | `? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)` |
| 5282 | pGLContext | GetDepthMask | RenderStateBlob | `.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),` |
| 5287 | pGLContext | GetDepthFunc | RenderStateBlob | `.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MG_State::pGLContext->GetDepthFunc()),` |
| 5288 | pGLContext | GetLogicOp | RenderStateBlob | `.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MG_State::pGLContext->GetLogicOp()),` |
| 5324 | pGLContext | GetPatchDefaultOuterLevel | RenderStateBlob | `const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();` |
| 5325 | pGLContext | GetPatchDefaultInnerLevel | RenderStateBlob | `const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();` |
| 5377 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 5405 | pGLContext | GetBlendFuncIndexed | RenderStateBlob | `MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);` |
| 5406 | pGLContext | GetBlendEquationIndexed | RenderStateBlob | `MG_State::pGLContext->GetBlendEquationIndexed(i, colorEquation, alphaEquation);` |
| 5407 | pGLContext | IsCapabilityEnabledIndexed | RenderStateBlob | `const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);` |
| 5412 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `MG_State::pGLContext->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);` |
| 5828 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();` |
| 5889 | pGLContext | GetRenderStateParametersVersion | RenderStateBlob | `const Uint paramsVersion = MG_State::pGLContext->GetRenderStateParametersVersion();` |
| 5903 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();` |
| 5979 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 6003 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDraw();` |
| 6051 | pGLContext | IsTransformFeedbackActive | XfbOp | `MG_State::pGLContext->IsTransformFeedbackActive() &&` |
| 6065 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 6070 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 6081 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
| 6082 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
| 6090 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
| 6264 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 6385 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& drawProgram = *MG_State::pGLContext->GetProgramForDraw();` |
| 6397 | pGLContext | GetFramebufferBindingSlot | FboAttach | `MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 6404 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 6405 | pGLContext | GetProgramForDraw | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDraw();` |
| 6445 | pGLContext | IsTransformFeedbackActive | XfbOp | `if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&` |
| 6459 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
| 6465 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 6583 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
| 6602 | pGLContext | GetSamplingResolutionGeneration | TexParam | `const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 6763 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) \|\|` |
| 6764 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);` |
| 6905 | pGLContext | GetPipelineStateVersion | RenderStateBlob | `snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();` |
| 6906 | pGLContext | GetTextureBindGeneration | ObjectBind(Texture) | `snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();` |
| 6932 | pGLContext | GetSamplingResolutionGeneration | TexParam | `snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();` |
| 6969 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDispatch();` |
| 7021 | pGLContext | GetProgramForDispatch | ObjectBind(Program) | `const auto& program = *MG_State::pGLContext->GetProgramForDispatch();` |
| 7065 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();` |
| 7139 | pGLContext | GetScissorBox | RenderStateBlob | `? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),` |
| 7142 | pGLContext | GetScissorBox | RenderStateBlob | `: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);` |
| 7190 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
| 7193 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();` |
| 7201 | pGLContext | GetClearColor | RenderStateBlob | `.color = MG_State::pGLContext->GetClearColor(),` |
| 7202 | pGLContext | GetClearDepth | RenderStateBlob | `.depth = MG_State::pGLContext->GetClearDepth(),` |
| 7203 | pGLContext | GetClearStencil | RenderStateBlob | `.stencil = MG_State::pGLContext->GetClearStencil()` |
| 7210 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
| 7233 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
| 7260 | pGLContext | GetDepthMask | RenderStateBlob | `if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {` |
| 7273 | pGLContext | GetStencilState | RenderStateBlob | `MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
| 7302 | pGLContext | GetDepthMask | RenderStateBlob | `if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {` |
| 7306 | pGLContext | GetStencilState | RenderStateBlob | `const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
| 7322 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
| 7343 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);` |
| 7372 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {` |
| 7414 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
| 7445 | pGLContext | GetDepthMask | RenderStateBlob | `const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };` |
| 7447 | pGLContext | GetStencilState | RenderStateBlob | `const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
| 7459 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));` |
| 7516 | pGLContext | GetColorMaskIndexed | RenderStateBlob | `const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));` |
| 7534 | pGLContext | GetDepthMask | RenderStateBlob | `if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&` |
| 7542 | pGLContext | GetStencilState | RenderStateBlob | `MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;` |
| 7561 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();` |
| 7598 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
| 7622 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
| 7645 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
| 7660 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, GLint drawbuffer,` |
| 8042 | SharedPtr<MG_State | - | handle-ify (wire handle) | `VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {` |
| 8519 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
| 8520 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();` |
| 8524 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VulkanRenderer::BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,` |
| 8525 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,` |
| 8552 | pGLContext | IsCapabilityEnabled | RenderStateBlob | `if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {` |
| 8553 | pGLContext | GetScissorBox | RenderStateBlob | `const IntVec4& scissor = MG_State::pGLContext->GetScissorBox();` |
| 9147 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 9147 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 9155 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
| 9866 | pGLContext | GetFramebufferBindingSlot | FboAttach | `auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();` |
| 10621 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();` |
| 10622 | pGLContext | GetPixelStoreParameters | PixelStoreBlob | `const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);` |
| 10652 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 10652 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 10657 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void VulkanRenderer::GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,` |
| 10896 | pGLContext | GetTextureUnitObject | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 10896 | pGLContext | GetActiveTextureUnit | ObjectBind(Texture) | `auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());` |
| 11137 | pGLContext | GetBoundTransformFeedbackName | XfbOp | `const Uint name = MG_State::pGLContext->GetBoundTransformFeedbackName();` |
| 11151 | pGLContext | IsTransformFeedbackActive | XfbOp | `!MG_State::pGLContext->IsTransformFeedbackActive()) {` |
| 11157 | pGLContext | IsTransformFeedbackPaused | XfbOp | `if (MG_State::pGLContext->IsTransformFeedbackPaused()) {` |
| 11160 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
| 11199 | pGLContext | GetBufferBindingPoint | ObjectBind(BufferRange) | `auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,` |
| 11230 | pGLContext | GetTransformFeedbackGeneration | XfbOp | `const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration();` |
| 11253 | pGLContext | GetTransformFeedbackProgram | XfbOp | `const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();` |
| 11901 | pGLContext | GetRenderStateParameters | RenderStateBlob | `const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();` |
| 11979 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 11989 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 11995 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();` |
| 12080 | pGLContext | GetBoundVertexArray | ObjectBind(VAO) | `const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();` |
| 12090 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 12160 | pGLContext | GetBufferBindingSlot | ObjectBind(Buffer) | `auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();` |
| 12650 | pGLContext | GetProvokingVertexMode | RenderStateBlob | `MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)` |
| 14813 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ITextureObject> liveTexture;` |
### `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h` (12 hits)
| line | kind | member | delta | snippet |
|---|---|---|---|---|
| 184 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 186 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 188 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 190 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,` |
| 195 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,` |
| 196 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,` |
| 256 | SharedPtr<MG_State | - | handle-ify (wire handle) | `void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,` |
| 378 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> program;` |
| 379 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::SamplerObject> nearestSampler;` |
| 380 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::SamplerObject> linearSampler;` |
| 388 | SharedPtr<MG_State | - | handle-ify (wire handle) | `SharedPtr<MG_State::GLState::ProgramObject> program;` |
| 1390 | SharedPtr<MG_State | - | handle-ify (wire handle) | `const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);` |

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