Compare commits

...
Author SHA1 Message Date
swung0x48 4826806881 [CI] (Workflows): run the test and apk lanes on every push to feat/disaggregated
- the MGPipe phases land as a series of pushes and each one needs the full lane; dispatching
  by hand after every merge is a step that gets forgotten
- both entries carry a remove-before-merging-to-dev note: dev's trigger set is what ships
2026-09-06 06:32:37 -04:00
swung0x48 e7a6a72f6a [Docs] (Disaggregated): record the P1 landing and what its verify lane found
- README status, the ROADMAP P1 row with the measured site and accessor counts, and the
  ARCHITECTURE correction from 293 to the 277 arrow sites the tree actually has
- MEASUREMENTS gains the P1 scale table, the two classes of finding the verify lane
  produced (nine missing fill rows, three fields a backend moves inside its own verb) with
  the push-on-mutation decision and its three hooks, and the acceptance numbers
2026-09-06 06:17:12 -04:00
swung0x48 62a7786184 [Fix] (Pipe, Purity): end a declared verb honestly, and gate the header MG_State now includes
- MGPipeLeaveVerb() bumps the serial and puts the current verb back to none, so a test that
  drives a backend helper directly stops declaring where it says it stops and a later
  unguarded read aborts as "<Field>@<none>" instead of naming an unrelated verb
- check_include_closure.py gains a fourth probe: F2 put MGP_NOTE_MUTATION into frontend
  mutators, so MG_State includes MG_Pipe/PipeMutation.h and that header must never reach
  back into MG_State, MG_Impl or a backend
2026-09-06 06:15:56 -04:00
swung0x48 ef6227e19b [Test] (Pipe): let a test that drives a backend helper directly declare the verb it stands in
- ScopedPipeVerb (MG_Test/ScopedPipeVerb.h): an RAII "as if we were inside verb X" object
  that runs the real MGPipeFillForVerb for the verb it names, so the eleven unit entries
  that construct a GLContext by hand and call a backend helper with no GL entry point in
  between stop reading an empty, unstamped PipeInputs block
- it weakens nothing: it fills exactly that verb class's may-read mask, so a read outside
  it is still Fatal{UnmigratedPipeInput} naming the field and the declared verb; leaving
  the scope re-arms the poison with a one-field kQuery fill, so one case's declaration
  cannot cover a later one when the binary runs as a single process
- placed the way MGP_FILL is placed in production: immediately before the backend call,
  after every frontend mutation it is meant to see; a second call after the test moved
  state is a second verb (Renew()), a helper of another class gets a nested scope
- no-op in the pull build, no test renamed, no test added, no production source touched
2026-09-06 06:01:41 -04:00
swung0x48 9bd6d39403 [Test] (Pipe): pin the push-on-mutation shape - a frontend write inside a verb refreshes the pushed field, and only its value
- AFrontendMutationInsideAVerbRefreshesThePushedField: fills DrawArrays, then
  does what UniformManager's fallback path does mid-draw (a SamplerObject filter
  change) and what a bind reached from inside a verb does
  (NoteTextureUnitTouched), and asserts the block still equals the live context
  for GetSamplingResolutionGeneration, GetTextureBindGeneration and
  GetMaxTouchedTextureUnit.
- AFrontendMutationInsideAVerbDoesNotDivergeAtRead: the lane failure end to end,
  under the armed comparator in a forked child - the read after the mutation
  must complete and the log must carry no Fatal{.
- TheMutationNoticeRefreshesTheValueButNotTheStamp: the notice must not restamp
  a field whose stamp the fill withheld (negative control B), and must not stamp
  a field the verb class never fills (the generation under a kQuery verb).
- Falsified: with the notice's body short-circuited, the first two fail (the
  read-side one by SIGABRT on Fatal{PipeVerifyDiffer,
  "GetSamplingResolutionGeneration@DrawArrays", where=read}) and the third
  stays green, which is what a guard case should do.
- The three names also exist as visible GTEST_SKIPs in the pull build, as every
  other case in this file does.
2026-09-06 05:32:25 -04:00
swung0x48 6b681c4a63 [Fix] (Pipe, State): refresh a pushed PipeInputs field when the frontend moves it inside a verb
- The verify lane aborted eight integration entries and two retrace cases with
  Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
  where=read}, always one line after "ResolveSamplerDescriptor: using fallback
  texture for unbound sampler". The backends write into frontend objects during
  their own verb - Magma synthesises a fallback texture for an unbound sampler
  and gives it a shape, materialises a queued clear, overrides a unit's sampler
  filter - and every one of those writes moves a counter MGP_FILL already
  copied, so the pushed block stops equalling the live context for the rest of
  the verb. That is a real divergence, not a harness artefact: the pull build
  reads the moved value and the push build reads the boundary one.
- Takes the findings' preferred option, push on mutation, over the volatile-in-
  verb class: it keeps the comparator's invariant ("the pushed block equals the
  live context at every read") literally true, keeps push semantics equal to
  pull, and is the shape P2's tracker needs. The fallback would have had to skip
  compare-at-read for the field, which is the one comparator arm that is real in
  P1 - it would have blinded the gate on the very field that found the bug.
- MG_Pipe/PipeMutation.h declares MGP_NOTE_MUTATION(Field), a no-op that
  includes nothing in the pull build; MG_Impl/Pipe/PipeFill.cpp defines the
  notice next to the filler it shares CopyField with. The notice refreshes one
  field's value when a context is live, a verb has been filled, and the field is
  in that verb class's may-read mask; it never touches the poison stamp, so a
  stamp MOBILEGL_PIPE_POISON_OMIT withheld stays withheld and a field the verb
  never filled stays Fatal{UnmigratedPipeInput} rather than being healed.
- The enumeration behind the three hook sites: of the ~40 backend->frontend
  write sites, only the texture family reaches a pushed value. Every path
  through them funnels into TextureState::BumpSamplingResolutionGeneration
  (SamplerObject::BumpVersion for the sampler setters,
  TextureObjectBase::BumpShapeVersion for AllocateStorage / SetInternalFormat /
  TruncateMipmapLevels / SetSamples / SetFixedSampleLocations),
  BumpTextureBindGeneration (a default texture becoming defined, delete-unbind,
  a unit's sampler object changing) or NoteUnitTouched (which also moves the
  touched-unit high-water mark), so the notice sits on the counters rather than
  on each writer and covers the whole family including writers added later.
  The buffer, program and VAO writes reach no pushed field: their objects are
  read back through O-class live references, not copied values.
2026-09-06 05:32:13 -04:00
swung0x48 80a6b39003 [Fix] (Pipe): fill the capability set on a texture op and a dispatch, and the shader blit's viewport, provoking vertex and buffer bindings
- The verify retrace aborted four cases with Fatal{UnmigratedPipeInput,
  "IsCapabilityEnabled@GenerateMipmap"} (x3) and "@DispatchCompute" (x1):
  Magma materialises a texture's queued clear inside both verbs
  (GenerateMipmap -> MaterializePendingClearForTexture,
  DispatchCompute -> PrepareStorageImageTextures -> the same), and the clear
  pre-compensates its colour against GL_FRAMEBUFFER_SRGB in
  VkClearManager::PreCompensateSrgbClearColor. Neither class named the field.
- Audited every class the same way rather than stopping at those two rows. Two
  more helper-program draws sit inside verbs whose class did not name what they
  read: GenerateMipmap takes GenerateDepthMipmapWithShader for a depth texture
  and BlitFramebuffer takes TryBlitToDefaultFramebufferWithShader for the
  default framebuffer. Both bind their helper's descriptors through
  BindProgramUniformBuffers, whose sampler resolver reads the draw framebuffer
  for its feedback-loop check and whose buffer-block resolvers read the frontend
  binding points; the blit additionally sets the dynamic viewport through
  ApplyGLViewportState -> ComputeGLViewport and picks its pipeline's provoking
  vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode.
- kTextureOp gains IsCapabilityEnabled, GetFramebufferBindingSlot and
  GetBufferBindingPoint; kDispatch gains IsCapabilityEnabled; kBlitOrCopy gains
  GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode and
  GetBufferBindingPoint. Every row carries the path it was derived from.
- Also unfolds the kReadback transform-feedback rows 9087f133 landed on one
  1200-column line back into the file's one-row-per-line shape; no row changes.
2026-09-06 05:31:50 -04:00
swung0x48 97b997d5da [CI] (Pipe): read the verify library's static symtab, grep only the arming lane's log, and stop the two retrace lanes overwriting each other's evidence
- both `nm -D --defined-only ... | grep -q MGPipe...` gates could never pass: the library
  is built CXX_VISIBILITY_PRESET hidden in every non-Debug configuration and the MGPipe
  entry points carry no export attribute, so the dynamic table holds none of them (0 of
  11930 exported symbols on this tree, while `nm --defined-only` finds MGPipeFillForVerb
  as a local `t`). build-linux-verify, and with it integration-verify and every
  retrace-verify entry, would have been red forever for a reason unrelated to the
  comparator. Both now read the static table, name what a miss means, and refuse a
  stripped artifact instead of reporting its silence as a missing symbol
- "Every verify process really armed" grepped one shared per-lane log that holds only the
  LAST process of 406 - and the last ambient entry is the poison control's parent, which
  forks, execve()s and waits without ever issuing a verb, so the step would have red a
  healthy lane while proving nothing about the other 405. It now greps the two
  VerifyArming. lanes' own logs, requires one per backend, and says what that establishes
- remove-artifact-clutter kept fixtures for failed `retrace (` jobs only, so a failed
  `retrace verify (` case lost the fixture needed to reproduce it; both prefixes now count
- the retrace negative control replayed OpenRA into the same case directory, so
  "Upload actual image" shipped the deliberately corrupted run's output under the good
  run's name. The verified output is put aside and restored before the verdict
- build-linux-verify regains build-linux's "Show installed toolchain" step, and its
  deliberate divergence (Release even under ACTIONS_STEP_DEBUG - a Debug build would flip
  visibility and arm the poison through a different #if arm) is written down
- the lane's scope is stated where it is run: every integration ENTRY under the
  comparator, not every configuration - `integration`'s second
  MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 pass (186 entries here) is not affordable at
  the 5-10x the comparator costs, so that tier stays covered unverified by `integration`
2026-09-06 04:41:28 -04:00
swung0x48 7d80c9678e [Test] (Retrace): scan a verify retrace's log for the third MGPipe Fatal too
- the block looked for Fatal{PipeVerifyDiffer and Fatal{UnmigratedPipeInput only. A
  misspelt MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT reports
  Fatal{PipeVerifyBadKnob, and it was caught only because D2 makes that one abort the
  process - which is precisely what MOBILEGL_PIPE_VERIFY_FATAL=0, the supported triage
  configuration, takes away. A typo'd knob would have left a negative-control run
  looking healthy
- the FATAL_ERROR text now says what each of the three means and where the two
  vocabularies live, because that message is the whole diagnosis a `cmake -P` step gets
2026-09-06 04:41:28 -04:00
swung0x48 72aa9191b4 [Fix] (Tooling): gate symbol_report on all four buckets and on a .text that moved in either direction
- G1 is spelled "added == removed == resized == renamed == 0, .text delta 0", but
  gate_failures() took only added and removed and fired on text_delta > N: a pull build
  whose .text SHRANK, or whose functions were resized with zero net delta - the exact
  shape a null-guard or ternary rewrite produces - walked through
  `--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` green
- --fail-on-symbol-set-change now covers the four buckets the report prints, and
  --fail-on-added-bytes 0 means byte-identical rather than "did not grow" (a positive
  budget keeps the one-sided meaning). --fail-on-text-delta is the explicit spelling for
  a run that wants no byte budget at all
- the gates read the threshold-0 buckets whatever --threshold says: --threshold is a
  report control, and a gate that read the thresholded resize list would have quietly
  weakened itself the day someone raised it. The run says so in its output
- the self-test drives the three shapes that used to pass (a shrunk .text at budget 0, a
  resize and a rename at zero net delta) from the same canned transcripts
2026-09-06 04:41:28 -04:00
swung0x48 1e3a74686f [Test] (Pipe): give the arming assertion a lane and a log of its own, and stop the poison child calling a sequence it never ran a success
- the arming case read the ambient lane's MOBILEGL_LOG_FILE_PATH, and that log is
  opened fopen(path, "w") by every process in the lane: with 406 entries per backend
  and CI running them -j 4, a whole-file read races a neighbour's bring-up, and the
  file that survives the lane holds only the LAST writer. Every other log-reading
  scenario in this suite (UnlocatedIoBlocks, the primgen reroute, the point-size
  demotion) is registered in a filtered lane with its own log for exactly that reason;
  PipeVerifyArmingScenario.Armed now follows them, in DirectGLES.VerifyArming. /
  DirectVulkan.VerifyArming., and skips anywhere MGITEST_PIPE_ARMING_LANE is unset
- what that can prove is written down where it is asserted: arming is a property of
  (this library, this environment) and these two processes share both with their ~400
  ambient siblings. A per-process census is not available through a shared log, and a
  comment that claimed one was the reason CI grepped a file that could not answer
- the re-exec'd poison child ran RunSequence() and then _exit(0) unconditionally, so a
  fatal assertion inside it - the shader failing to compile, say - returned before the
  draw and the glGenerateMipmap and still reported success: WithoutOmissionCompletes,
  the one green entry negative control B turns red, passed on a child that ran none of
  the sequence. It now exits HasFailure() ? 1 : 0, and checks glGetError() after the
  mipmap so a rejected sequence is part of the answer rather than stderr nobody reads
2026-09-06 04:41:28 -04:00
swung0x48 0fe7bf82d2 [CI] (Pipe): the third CI mode - a verify build, its integration and retrace lanes, and the two negative controls as always-on steps
- build-linux-verify is a second Release/INFO build with -DMOBILEGL_PIPE_VERIFY=ON,
  because the comparator is a compile-time option and does not exist in the shipped
  library. It refuses to ship an artifact whose libMobileGL.so does not export
  MGPipeVerifyInputs and MGPipeFillForVerb: a typo'd -D is not an error in CMake,
  and every lane below would then be green having compared nothing.
- integration-verify runs the suite with the comparator armed and then proves it
  armed twice over: --no-tests=error reds a build whose verify entries were never
  registered, and a step greps every pipe-verify-*.log for the arming line.
- The two negative controls are steps of that job, not a manual exercise: a gate
  that can only be shown to work by someone remembering to break it has already
  stopped working. Each passes when ctest FAILS, and each first counts its own
  selection - an empty selection also exits non-zero under --no-tests=error, and a
  control that passed because it ran nothing would be worse than no control.
- Control B targets PoisonOmissionScenario.WithoutOmissionCompletes, the only
  integration entry in the tree that calls glGenerateMipmap at all; the case no
  longer skips itself when the omission knob is set, precisely so that the control
  has a green entry to turn red.
- retrace-verify replays the eight "verify": true cases against the verify library,
  copied over build-linux/libMobileGL.so because build-retrace freezes that absolute
  path into every case, with an nm check that the swap happened and an inverted
  OpenRA step that must go red under MOBILEGL_PIPE_VERIFY_CORRUPT.
  remove-artifact-clutter now waits for it: it deletes the trace fixtures these jobs
  download.
- monolith-symbol-report is G1 as a job: two pull builds with identical flags and
  LTO off, the baseline named by a workflow_dispatch input, symbol_report.py with
  both hard gates, and a refusal of any MG_Remote symbol in the monolith. It is
  dispatch-only because its answer is about a baseline, not about this push.
- pipe-gates gains the two --self-test steps. Regenerating and diffing cannot see a
  structural check that silently stopped checking; a broken gate and a clean tree
  produce the same green. Its dirty-surface comment now says P2, which is where
  ROADMAP.md:18 puts the first mapping round.
2026-09-06 04:41:28 -04:00
swung0x48 416cd23c28 [Feat] (Tooling): give symbol_report.py the two hard gates G1 needs, with the report written first
- --fail-on-added-bytes was a reserved no-op that printed "this run stays
  informational"; it now exits non-zero when .text grew past the budget, and
  --fail-on-symbol-set-change joins it for the added/removed buckets. Together they
  are the spelling of P1's G1 ("the pull build is byte-identical"): --threshold 0
  --fail-on-symbol-set-change --fail-on-added-bytes 0.
- Default behaviour is unchanged: with no gate flag the tool prints its report and
  exits 0, which is what every existing caller and the informational
  monolith-symbol-report job expect.
- A gate fires AFTER the Markdown and JSON are written, never before: the report is
  the diagnosis, and a CI job that failed before uploading its artifact is one
  nobody can act on.
- The decision lives in a pure gate_failures(), so --self-test drives it from the
  same two canned transcripts as the buckets: each flag fires on the canned
  add/remove/+100 delta, each stays quiet when it was not asked for, and a
  tolerated budget is tolerated. A gate whose only test is a real build is a gate
  nobody re-tests.
2026-09-06 04:41:28 -04:00
swung0x48 5f8e8db1b9 [Test] (Retrace): make a retrace under MOBILEGL_PIPE_VERIFY prove it armed, and give the lane a label
- A retrace that exports MOBILEGL_PIPE_VERIFY=1 at a library configured without
  -DMOBILEGL_PIPE_VERIFY=ON is a no-op: the frames still match their goldens and the
  case reports green having compared nothing. run_trace_case.cmake now demands the
  evidence whenever the variable is set to anything but 0/false - mobilegl.log must
  exist, must carry "MGPipe: verify armed", and must carry neither
  Fatal{PipeVerifyDiffer nor Fatal{UnmigratedPipeInput. With the variable unset the
  script is byte-for-byte the old one.
- The Fatal scan is not redundant with the replay's exit status:
  MOBILEGL_PIPE_VERIFY_FATAL=0 is the supported triage configuration, and there a
  divergence is logged and counted rather than aborted, so the run would finish 0
  with its own report sitting unread in the log.
- LABELS retrace on both registrations: the lane was selectable only by regex, so
  `ctest -L retrace --no-tests=error` - the spelling that reds a lane which
  registered nothing - could not be written at all.
- "verify": true on eight cases (the five 180s cases plus minecraft-1.21.4-in-world,
  minecraft-1.21.4-fabric-sodium-in-world and improved-transparency-minecraft-26.3),
  and --format github-verify-matrix over that subset: 16 entries, against the full
  lane's 77. The verify build compares at every verb boundary and again at every
  accessor read, which the design budgets at 5-10x, so the per-push job runs the
  subset and the full sweep is a phase-exit / workflow_dispatch run.
- The flag is validated in the manifest loader, not at the matrix, so a non-boolean
  or a verify case excluded from CI is a loud error in every consumer instead of a
  subset that is quietly one case short.
2026-09-06 04:41:28 -04:00
swung0x48 bdf05514c3 [Test] (Pipe): the integration-verify lanes and their two always-on negative controls
- ARCHITECTURE.md 13.2-(2) asks for a third CI mode, and a third mode whose only
  evidence is "ctest was green" proves nothing: MOBILEGL_PIPE_VERIFY=1 against a
  library that never compiled the comparator in is a silent no-op that looks
  exactly like a clean pass. Six registrations, all under if (MOBILEGL_PIPE_VERIFY)
  and all labelled integration-verify, make both halves falsifiable - a
  mis-configured build registers nothing and --no-tests=error reds the lane, and
  PipeVerifyArmingScenario.Armed fails a lane whose library never printed its
  arming line.
- PipeVerifyArmingScenario.CorruptedFieldIsReported is negative control A (G4):
  its lane pins MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters with
  MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and can
  read the report back; the CI step that exports the same knob against the ambient
  lane, where FATAL keeps its default, asserts the other half - the abort.
- PoisonOmissionScenario is negative control B (G5), in two cases that cannot share
  a process because the knob is process-wide: the omitted (verb, field) pair must
  abort the glGenerateMipmap and NOT the draw before it, and the same sequence with
  the knob unset must complete with no Fatal at all.
- The sequence runs in a fork()+execve() of this same binary rather than a bare
  fork(): the fixture has already brought a context up, and a bare fork of a
  process holding a live Vulkan device inherits the driver's mutexes with no
  threads to release them - measured here as a 120s wedge on DirectVulkan against a
  clean pass on DirectGLES. The child gets its own MOBILEGL_LOG_FILE_PATH because
  the library opens its log with fopen(path, "w") and would otherwise truncate the
  file the parent is about to read.
- No ambient Verify. entry names MOBILEGL_PIPE_VERIFY_CORRUPT or
  MOBILEGL_PIPE_POISON_OMIT in its ENVIRONMENT property, because a property entry
  overrides the job environment for the names it lists: the two CI negative-control
  steps export those knobs into the job environment and must reach the processes.
  Every list appends MGL_ITEST_COMMON_ENV / MGL_ITEST_VULKAN_ENV for the same
  reason, so the vendor and ICD pinning survives.
2026-09-06 04:41:28 -04:00
swung0x48 bf8b39a867 [Refactor] (Magma): route every frontend read through MGB_CTX - 164 arrow sites sed'd, 49 non-arrow lines converted (43 asserts keep their meaning as MGB_CTX_LIVE); pull build byte-identical
- Every MG_State::pGLContext-> in the seven DirectVulkan TUs becomes MGB_CTX->
  (DirectVulkan.cpp 12, BackendObject_DirectVulkan.cpp 2, UniformManager.cpp 14,
  VkClearManager.cpp 1, VkRenderPassManager.cpp 3, VkTextureManager.cpp 2,
  VulkanRenderer.cpp 130 occurrences on 127 lines); each TU includes
  <MG_Pipe/PipeInputsSwitch.h> right after its MG_State/GLState/Core.h include
  (VkRenderPassManager.cpp after its include block, it never included Core.h).
- The 43 MOBILEGL_ASSERT(pGLContext) / (pGLContext != nullptr) lines become
  MOBILEGL_ASSERT(MGB_CTX_LIVE, ...): identical in every INFO build (the macro is
  empty there) and still a null-context assert in a DEBUG build.
- The six code-bearing guards are like-for-like pointer tests in the pull arm:
  if (MGB_CTX_LIVE) at the two InvalidateCompileEnv sites, MGB_CTX_LIVE in the
  XFB query counter condition and the ternary that snapshots the paused-primitive
  counter, !MGB_CTX_LIVE in BeginXfbCaptureForDraw, MGB_CTX_LIVE && in the
  provoking-vertex resolve. No semantic rewrite: under push MGB_CTX_LIVE is simply
  "a context exists", the real meaning of these guards is P2's business.
- VertexInputStateFactory.h's comment stops naming pGLContext so purity gate C
  (grep -rc pGLContext MobileGL/MG_Backend/DirectVulkan) reads 0 in every file.
- The 12 SyncPersistentMappedRange and 3 SyncGpuWrites sites of the D10 table are
  untouched; PipeStats AddCalls literals unchanged.
- Proof on the pull build (Release/INFO, LTO off, vs feat/disaggregated@087685d1):
  symbol_report --threshold 0 -> 27799 symbols, 0 added / 0 removed / 0 resized /
  0 renamed, .text 10792579 -> 10792579 (+0); nm --defined-only name set identical;
  ctest -N name set identical (2334); unit 1466/1466; DirectVulkan integration lane
  427/427 on lavapipe (two ArmedWhenTheEnvironmentPinsItOn entries flake under -j 4
  exactly as on the baseline and pass serially). The push build compiles and links.
2026-09-06 04:41:28 -04:00
swung0x48 d4504e30f8 [Refactor] (Espryt): route every frontend read through MGB_CTX - 113 arrow sites sed'd, 9 non-arrow lines converted (the fb-slot cache keys on MGB_CTX_IDENTITY); pull build byte-identical
- P1 package B (BRIEF-P1 C.2): the four DirectGLES TUs include <MG_Pipe/PipeInputsSwitch.h> right after
  their MG_State/GLState/Core.h include (Managers.cpp after its Managers.h include) and spell every
  frontend read MGB_CTX->Accessor(...). In the pull build MGB_CTX is MG_State::pGLContext, so the
  code is the tree before this commit token for token; in the push build it is &gPipeInputs, the block
  the frontend fills at every verb boundary.
- 113 arrow occurrences on 113 lines went through the mechanical sed (DirectGLES.cpp 91, Managers.cpp 15,
  MultiDraw.cpp 5, Utils.cpp 2). The 9 non-arrow lines follow D9: Managers.cpp's five bare/compound
  guards and three ternary conditions become MGB_CTX_LIVE (UniquePtr::operator bool spelled out, so the
  pull build does not move); DirectGLES.cpp's GetFramebufferBindingSlotFast keys its static cache on
  MGB_CTX_IDENTITY (a const void* compare) instead of pGLContext.get().
- The cache refill loop dereferences MGB_CTX once into a local reference and reads the slots through it,
  instead of &MGB_CTX->GetFramebufferBindingSlot(i) per iteration as D9 spells it: a store into
  g_fbSlotCache (a pointer) may alias the unique_ptr's own pointer under clang's TBAA, so the per-iteration
  spelling re-reads pGLContext inside the loop and grows the three functions the loop is inlined into
  (SyncCurrentProgram +16, ForceBindCurrentFBO +9, BlitNamedFramebuffer +1; .text +32). Hoisting the
  dereference restores the single load and a zero .text delta.
- grep -rc pGLContext MobileGL/MG_Backend/DirectGLES reports 0 in every file; symbol_report --threshold 0
  against the 087685d1 baseline: 0 added / 0 removed / 0 resized / 0 renamed, .text +0, nm --defined-only
  set identical. The only bytes that move are __LINE__ immediates in RecordError sites after the inserted
  include line. The 8 SyncPersistentMappedRange and 3 SyncGpuWrites sites and the PipeStats AddCalls
  literals are untouched.
2026-09-06 04:41:28 -04:00
swung0x48 9087f13308 [Fix] (Pipe): let a readback fill the transform-feedback state its emulation reads
- the depth/stencil read emulation opens a ScopedEmulationDrawState that pauses an active
  capture around its own draw, so ReadPixels/GetTexImage read IsTransformFeedbackActive and
  IsTransformFeedbackPaused; without the two kReadback rows every emulated readback aborts
  with Fatal{UnmigratedPipeInput, "IsTransformFeedbackActive@ReadPixels"} once the Espryt
  sites are converted
2026-09-06 04:41:15 -04:00
swung0x48 44ffafb2dc [Test] (Pipe): pin the pre-fill window - every stamp of 0 is stale at serial 0, a read before any fill aborts naming "<none>", and the FATAL=0 child leaves through std::exit so the teardown summary is observed
- PipeCatalogue.PipeInputFieldsStartUnfilled asserted "unfilled" only after setting the serial to 1 by hand, stepping around the serial-0 window; it now asserts every field stale on a value-initialised state first (sticky fields included).
- PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb: a live context, no fill, gPipeInputs.GetLineWidth() in a forked child; the parent expects SIGABRT, exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}, no PipeVerifyDiffer and no arming line. In a verify build the child sets Features.PipeVerify first, the lane's shape.
- VerifyFatalOffLogsTheDivergenceAndContinues: the child _exit(0)ed, so VerifyState's destructor never ran and the "verify summary" line was covered only by a lane run; std::exit(0) runs it, and the parent now asserts "2 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0".
2026-09-06 02:55:17 -04:00
swung0x48 12b57055b7 [Docs] (Pipe): record why the forwarders carry no poison check, the InHook re-entry guard, and the Index slot no FillPoints.def row can fill
- The seven F-class forwarders are the declared exception to D4's "every accessor body": a forward is a live call, not a stored value, and InvalidateCompileEnv is reached from backend initialisation before any verb has filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp is consulted by no accessor; the tests pin it through MGPipeInputFieldIsFresh directly. Written down so P2's tracker does not "fix" the missing check.
- MGPipeVerifyReadHook: InHook is what keeps a GetProgramForDraw-triggered backend re-entry from recursing into a second hook, and the whole-field re-read is a per-read cost to keep in mind when reading the verify lane's wall time.
- GetBufferBindingSlot(Index) is polymorphic on GLContext (the bound VAO's element-buffer slot) and null in the block by design: a push Fatal there is not a missing row. No backend reads it today.
2026-09-06 02:55:17 -04:00
swung0x48 d0ff647581 [Fix] (Pipe): re-arm the verify comparator when any of its three knobs changes, not only when PipeVerify does
- ArmVerify latched PipeVerifyFatal and PipeVerifyCorrupt at the moment PipeVerify changed; a later change to either without a PipeVerify toggle was not seen, so 878db2c4's "re-parse when their value changes" held for one knob of three.
- The latch now keys on all three Features values. A lane loads Features before its first fill, so it still arms once per process; cost is two Bool compares and one String compare per fill, verify builds only.
2026-09-06 02:55:17 -04:00
swung0x48 30d72c5b4e [Fix] (Pipe): refuse a stamp of 0 as fresh on both branches - a read before the first fill is Fatal{UnmigratedPipeInput, "<Field>@<none>"}, not default storage
- MGPipeInputFieldIsFresh (gen_pipe.py gen_filled, emitted into PipeFilled.inc) compared FilledGen == CurrentVerbSerial for a non-sticky field; before the first MGPipeFillForVerb both are 0, so 55 of the 63 fields read as fresh and served their default-constructed storage silently, with no log line and no abort. Only the four raw-pointer O-class accessors tripped, through their null-base checks.
- D6 says the serial starts at 1 so that FilledGen == 0 means never filled, and names the window "<Field>@<none>"; the predicate now refuses a stamp of 0 before consulting the sticky branch or the serial, so the window is the poison's case as documented. This is the window E's risk table expects the verify lane to find (an init-time read, the first link, anything reached from eglMakeCurrent).
- Reproduced with the reviewer's pre-fill program (a live context with line width 7, no fill, gPipeInputs.GetLineWidth()): stored=0, rc=0 before; rc=134 with exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"} after, with and without Features.PipeVerify set first.
- The compare-at-read hook arms at the first fill and cannot see this window either; a static_assert next to it pins that a verify build always carries the poison, which is what covers the reads before arming.
2026-09-06 02:55:17 -04:00
swung0x48 d9f4698d98 [Test] (Pipe): give every PipeInputsTest process its own log file, and cover the compare-at-read arm, the three knob parsers and VERIFY_FATAL=0 with forked children
- gtest_discover_tests runs each case as its own process; under ctest -j the five shared one fixed log path, each main() unlinked it and each fork parent re-read it by path, so a sibling's Fatal line or unlink landed in another case's assertion (red 40/40 at -j 8, for a reason unrelated to the poison). The name now carries the pid and the file is removed on the way out; a forked child inherits the path on purpose.
- MutatedFieldIsNamedAtRead: the first falsifier of MGPipeVerifyReadHook - the child arms verify, fills DrawArrays, reads GetLineWidth (completes), mutates the live context's line width and reads again, and the parent expects SIGABRT with Fatal{PipeVerifyDiffer, "GetLineWidth@DrawArrays", verb=<serial>, where=read} and no where=entry.
- PoisonOmitKnobArmsTheOmission / BadPoisonOmitKnobIsFatalNamingTheKnob / VerifyCorruptKnobNamesTheFieldAtEntry / BadVerifyCorruptKnobIsFatalNamingTheKnob / VerifyFatalOffLogsTheDivergenceAndContinues: the knob parsers through MG_Config::Features, their arming lines, the exact Fatal{PipeVerifyBadKnob} text, and a FATAL=0 run that logs two divergences at consecutive serials and exits 0.
- OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale now loops every field: fresh iff in kTextureOp's mask and not the omitted one, so the name is true.
- The fork/waitpid/log-delta shape is one RunInChild helper; every case is still a visible skip where its switch is off.
2026-09-06 02:31:56 -04:00
swung0x48 878db2c405 [Fix] (Pipe): re-parse the POISON_OMIT and VERIFY knobs when their Features value changes, not once per process
- The two parsers (ParsePoisonOmissionKnob, ArmVerify) latched on the first fill, so the only way to reach them was a lane that loads MG_Config::Features before any fill; no unit test could exercise the parse, the arming lines or Fatal{PipeVerifyBadKnob}.
- The latch now keys on the value: a lane still parses once (Features is loaded before the first fill), while a forked test child that sets Features after its parent filled gets its own parse. An empty omission value never clears an omission armed through MGPipeSetPoisonOmission.
- Re-arming resets the CORRUPT field so a stale corruption cannot outlive the knob that named it.
2026-09-06 02:31:56 -04:00
swung0x48 77ecde1524 [Fix] (Pipe): refuse an eighth sticky row at compile time - the forwarded count equals the generated sticky count
- kMGPipeForwardedFieldCount = 7 was a hand-written twin of the generated kMGPipeInputStickyFieldCount, tied only by PipeCatalogue.StickyFieldsAreExactlyTheSeven; a static_assert in the header makes a PipeFields.def sticky row without a forwarder a build error instead of a test failure.
2026-09-06 02:31:56 -04:00
swung0x48 510ecd9293 [Fix] (Impl): move the DeleteSync fill of the orphan sweep inside its null-entry guard
- D7 says a verb whose table entry is null never bumps the serial; the sweep's fill sat before `if (backendDeleteSync && syncObject->backendHandle)`, so a backend without DeleteSync, or a sync without a backend handle, bumped once per orphan with nothing reading the fill.
- The sibling sweep in GL_Query.cpp (DeleteBackendQuery) already fills inside its guard; this makes the two the same shape and leaves the declared list of guarded-expression sites at nine.
- Pull build unchanged: MGP_FILL is ((void)0) there.
2026-09-06 02:31:56 -04:00
swung0x48 440d3c5253 [Test] (Pipe): pin the P1 poison and verify shapes - 63 fields, 69 verbs, the seven sticky fields, an omitted GenerateMipmap field leaves exactly that field stale, a corrupted snapshot names its field, reading an unfilled field aborts with SIGABRT
- PipeCatalogueTest (header-only, every build): VerbTableIsTheFunctionTable (69 verbs, 9 non-empty classes, the seven sticky bits in every class mask, D7's edges), StickyFieldsAreExactlyTheSeven, FloatVectorsCompareBitwise (a NaN FloatVec4 equals itself, -0.0f differs from 0.0f, a differing BlendStates[3] names RenderState then BlendStates), SixValueStructsHaveFieldLists (69 payloads, PixelStoreParameters and MGHostSpan compared member-wise with Pad0 ignored).
- PipeInputsTest, a new unit target linking the static library with its own main() that points MOBILEGL_LOG_FILE_PATH at a temp file: a fake GLContext, the real filler and accessors. OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale (G5 layer 1), ReadingAnOmittedFieldAbortsNamingTheVerb (G5 layer 2: fork, the child reads the omitted field after a draw and a sibling read that must not abort, the parent expects SIGABRT and the exact Fatal line and no @DrawArrays), ReadingAFilledFieldCompletes (the sibling without the omission: _exit(0), no Fatal), CorruptedSnapshotFieldIsNamedWithItsSerial (G4 at block level: clean compare true, a corrupted GetRenderStateParameters is named, its stamp is the fill serial, a corruption outside the mask is not seen), EveryVerbFillsItsClassAndNothingElse (after each of the 69 fills a field is fresh iff its class bit is set).
- Every PipeInputsTest case is a visible GTEST_SKIP in a pull build and the poison/verify cases skip in a push build without them; the ctest name set is the same in all three configurations (additions only: 9 names).
2026-09-06 02:06:37 -04:00
swung0x48 83b16561c2 [Feat] (Pipe): give the six value structs G4 field lists and assert the lists cover their members - the memcmp fallback is now a compile error, floats in vector types compare bitwise
- PipeFields.def gains MGP_FIELDS_RenderStateParameters (65 members), PixelStoreParameters (8), PerBufferBlendState (7), StencilFaceState (7), DynamicBackendParameters (85) and MGHostSpan (Ptr, Seg, Size, Offset), all appended to MGP_VERIFY_PAYLOAD_LIST: kMGPipeVerifiedPayloadCount 63 -> 69, and ResidualValueBlock / MGPPixelPackState / MGPCaps are now compared field by field all the way down.
- gen_verify: MEMCMP_FALLBACK_TYPES is empty and the generic MGPipeFieldEqual's last branch is static_assert(sizeof(T) == 0) - a struct without a field list is a compile error, not a padding false positive; Array<T, N> gets an element-wise overload, and a VecBase-derived vector (FloatVec4, IntVec4, BoolVec4...) is detected by a probe and compared bitwise over its data, because VecBase::operator== is IEEE == and a derived-to-base overload would lose resolution to the exact-match generic template.
- check_field_lists_cover_struct_members(): for every payload in MGP_VERIFY_PAYLOAD_LIST, parse `struct <Name> {` out of MGPipeTypes.h / MGPipeValueTypes.h / MGPipeHostSpan.h / BackendObject.h (comments and strings masked, statics, functions, nested types and Pad<n> members excluded) and refuse a member without an F(...) or an F(...) that is not a member; runs in both modes, so it is a pipe-gates gate.
- scan_live_accessors(): every MGB_CTX-> / pGLContext-> read under MG_Backend must have a Coverage.def row (rows nobody reads are printed: today only the dead GetBoundTransformFeedbackName).
- --self-test: six negative controls (struct member without F, F without member, payload without struct, verb missing from FillPoints.def, verb outside GLFunctionsTable, field row naming a non-accessor) that must each trip, plus a positive control; zero trips is itself an error.
2026-09-06 02:02:07 -04:00
swung0x48 275dd3edb4 [Feat] (Pipe): the MOBILEGL_PIPE_VERIFY shadow comparator - a per-verb entry compare over the fill set and a compare-at-read in every accessor, first differing field and verb serial, fatal by default
- Entry compare: at the end of MGPipeFillForVerb a file-static second PipeInputs is filled by SnapshotFromGLContext (the branch that survives P13) over the same class mask, MOBILEGL_PIPE_VERIFY_CORRUPT perturbs one field of that snapshot arm, and MGPipeVerifyInputs compares every field in the mask through MGPipeInputsFieldEqual (V by G4's MGPipeFieldEqual, O by identity, F equal by definition). Both arms come from the same context at the same instant, so this arm is tautological until P2 - the CORRUPT knob keeps it falsifiable.
- Compare-at-read: MGP_INPUT_VERIFY_READ now calls MGPipeVerifyReadHook(*this, field, i0, i1), which re-reads the whole field from the live context into a scratch block and compares it against the stored value on the live block only - a superset of "the same indices"; the indices decorate the report. This is the arm that is real in P1.
- Reporting per D8: MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"Field@Verb\", verb=<serial>, where=entry|read}") then abort unless MOBILEGL_PIPE_VERIFY_FATAL=0, which counts and summarises at teardown with MGLOG_E; arming logs "MGPipe: verify armed - 63 fields, 69 verbs, fatal=N" once, the knobs acknowledge themselves, an unknown field name is Fatal{PipeVerifyBadKnob}; a push build without the comparator answers MOBILEGL_PIPE_VERIFY=1 with one MGLOG_W_ONCE.
- MGPipeVerifyInputs carries default visibility so the retrace-verify job's nm -D probe can prove the verify library was the one swapped in; pointer corruption flips low bits instead of nulling (a null pointer already null was invisible to the compare), a SharedPtr becomes an aliasing pointer with no control block; CurrentVertexAttributeValue gets its own bitwise equality.
2026-09-06 01:56:44 -04:00
swung0x48 a196ada4c1 [Feat] (Impl): call MGP_FILL before every GLFunctionsTable entry - 83 statements over 69 verbs, a no-op in the pull build
- Every call through gBackendFunctionsTable.GL in the seven MG_Impl TUs (Drawing 36, Framebuffer 11, Texture 8, Getter 3, Program 1, Query 18, Sync 6) is preceded by MGP_FILL(<Verb>); placed after every early return the call sits behind - the conditional-render check, the null-entry guards, the loop bodies - so a verb whose entry is null on this backend never bumps the serial.
- Seven calls sit on a continuation line of a guarded expression (GetQueryResult64 x2, BeginXfbPrimitivesQuery, BeginTimeElapsedQuery, QueryCounterTimestamp, IsTimerQuerySupported, GetSyncStatus) and two more fold the null guard into the same expression (IsQueryResultAvailable, ClientWaitSync's guarded return); there the fill precedes the statement, so a null entry bumps the serial once with nothing to read it - harmless for the poison, recorded for the record.
- Each TU includes <MG_Impl/Pipe/PipeFill.h> after its last MG_State/MG_Backend include; under MOBILEGL_PIPE_PUSH=OFF the macro is ((void)0) and the pull library is symbol-identical with a .text delta of zero.
2026-09-06 01:51:16 -04:00
swung0x48 3aa4d8af1f [Feat] (Pipe): fill PipeInputs per verb class from GLContext and stamp per-verb generations - a read of a field the verb did not fill is Fatal{UnmigratedPipeInput}
- MGPipeFillForVerb now walks kMGPipeClassFieldMask[kMGPipeVerbClass[verb]] and copies every field in it by calling the GLContext accessor of the same name (MGPipeFillAccess::CopyField, one switch over the 56 stored fields; the seven forwarded fields copy nothing), stamping each with the new serial; the sticky seven get FilledGen = 1 on the first live fill through the same mask walk, since every class mask carries them.
- MOBILEGL_PIPE_POISON_OMIT (<Verb>:<FieldName>) is parsed once on the first fill and MGPipeSetPoisonOmission is the programmatic form for the unit tests; the omitted pair keeps its value copy and loses only its stamp, so the omission is indistinguishable from a forgotten FillPoints.def row. An unknown name is Fatal{PipeVerifyBadKnob}; a non-poison push build acknowledges the knob with one warning because no stamp exists to omit.
- PipeInputs names one friend, struct MGPipeFillAccess, instead of two friend functions, and VisitStorage gains a const overload for the comparator that follows.
2026-09-06 01:48:23 -04:00
swung0x48 bf86b1ede6 [Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged
- MG_Pipe/PipeInputsSwitch.h is the strangler switch (ARCHITECTURE.md 9.2): MGB_CTX is the
  live GLContext in the pull build and &gPipeInputs under MOBILEGL_PIPE_PUSH, so the pull
  arm's pGLContext spelling stays outside MG_Backend/ and purity gate C's grep.
- MG_Backend/MGPipe/PipeInputs.h holds one struct with every accessor a backend reads (63:
  the 61 Coverage.def rows plus GetBoundTransformFeedbackLifetimeId and
  HasOpenTransformFeedbackSpan), each keeping its GLContext name, parameters and return
  type so the site conversion is type-neutral; V fields are copied values, O fields are
  SharedPtr copies or pointers into the context, the seven F fields forward to the live
  context from MG_Impl/Pipe/PipeFill.cpp and are the only sticky ones (Coverage.def's
  MGP_COVERAGE_STICKY_LIST argues each: argument-keyed lookups and reverse-channel writes,
  never a version or generation counter).
- MG_Pipe/FillPoints.def is the verb table: one row per GLFunctionsTable function pointer in
  declaration order (69), nine classes and the may-read field rows; gen_pipe.py parses the
  struct and refuses a row set that is not exactly its member set, then emits
  generated/PipeFillPoints.inc (verb enum, class tables, per-class field masks with the
  sticky fields OR'ed in). MGP_FILL(Verb) in MG_Impl/Pipe/PipeFill.h is the fill point;
  MGPipeFillForVerb only bumps the serial, records the verb and stamps the sticky fields
  here - the per-class copies land in the next commit, the fill points in MG_Impl after.
- MOBILEGL_PIPE_POISON is derived once in PipeInputs.h from MOBILEGL_PIPE_PUSH and the DEBUG
  level, MOBILEGL_BUILD_DISAGGREGATED or MOBILEGL_PIPE_VERIFY (the tree has no
  MOBILEGL_DEBUG); under it every accessor is a read-side freshness check that aborts with
  Fatal{UnmigratedPipeInput, "Field@Verb"}.
- CMake: MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_VERIFY options (VERIFY forces PUSH on), the two
  new sources appended only under PUSH, the compile definitions; Config.h/ConfigLoader.cpp
  gain PipeVerifyFatal / PipeVerifyCorrupt / PipePoisonOmit under #if MOBILEGL_PIPE_PUSH so
  the pull build's FeaturesTable does not change size.
- Pull build proof: symbol_report.py against the 087685d1 baseline reports 0 added / 0
  removed / 0 resized / 0 renamed and a .text delta of 0; ctest -N names unchanged; gen_pipe
  --check clean; unit tests green in the pull, push and verify builds.
2026-09-06 01:35:45 -04:00
swung0x48 087685d19b [CI] (Purity): install libx11-dev for the include-graph-check job
- Includes.h defines VK_USE_PLATFORM_XLIB_KHR before vulkan.h, so the clang-mode probes and
  the negative controls need X11/Xlib.h on the runner; run 34008829271 failed on exactly that
2026-09-05 23:27:10 -04:00
swung0x48 5635e33ffe [Docs] (Disaggregated): record the P0.5 landing
- README status, the ROADMAP P0.5 row (measured outcome, the 8-includer correction, the
  DynamicBackendParameters exception) and the ARCHITECTURE note on which header gate A asserts
2026-09-05 23:22:15 -04:00
swung0x48 5d99ee435f [CI] (Purity): require both P0.5 closure probes now that the headers exist
- the ctest and the include-graph-check job pass --require-all, so a SKIP on
  MGPipeValueTypes.h or ProgramArtifacts.h is red from here on
2026-09-05 23:14:30 -04:00
swung0x48 b566bf4db9 [Fix] (Purity, Program, Pipe): close the review minors of the three P0.5 packages
- check_include_closure.py keeps the directory of a two-token -isystem, makes --require-all
  fail on a missing required header even when --probe narrowed the run, and removes its
  temp dir at exit
- ProgramArtifactsTest follows whichever STL branch the header pinned (#ifdef the size
  macro) instead of re-spelling the libstdc++ condition, and loses its stray executable bit
- MGPipeTypes.h's debt comment says what its closure still reaches (TextureEnum.h via
  BackendObject.h), which is why gate A asserts MGPipeValueTypes.h and not this header
2026-09-05 23:14:30 -04:00
swung0x48 09d2bb11b7 [Feat] (Program): add the reflection-archive field tables and sizeof trip wires to ProgramArtifacts.h
- One VisitFields table per type (ARCHITECTURE.md:259), a free constrained template so
  the moved struct bodies stay verbatim and one table serves both the const (serialize)
  and non-const (deserialize) direction. LinkArtifacts::program is deliberately absent:
  it is null for every archived instance and must never be serialized.
- Trip wires: TypeFacts is pinned at 44 bytes on every ABI; the container-bearing four
  are pinned per standard library - libstdc++ 64-bit here (128/128/1056/88, measured on
  this build), the libc++ branch left inert for the integrator to pin from the NDK
  build, MSVC unasserted. A member added without a table entry changes the size and
  the assertion message sends the author to the table.
- ProgramArtifactsTest counts the tables (20/14/11/57/8; const and non-const walks
  agree, names distinct), proves constness passes through, and records every sizeof as
  a ctest property so a new toolchain's numbers are readable from any `ctest -V` log.
2026-09-05 23:11:00 -04:00
swung0x48 fee3902472 [Refactor] (Program): stop ProgramObject.h including SpvcSession.h - it names no spirv-cross or SPIRV-Reflect symbol
- The include line was the header's only match for spvc_*/SpvReflect*/SpvcMetadata/
  SpvcSession; it only ever forwarded <spirv_reflect.h> and the session type to the
  eight includers, every one of which builds without it (no consumer needed a direct
  include added).
- One less transpiler header behind ProgramObject.h, on the way to a ProgramArtifacts.h
  closure that stays clear of MG_Util/ShaderTranspiler/ (P0.5 gate A).
2026-09-05 23:11:00 -04:00
swung0x48 da249f30e2 [Refactor] (Program): extract ProgramArtifacts.h - move TypeFacts, ResourceReflection, XfbVarying, LinkArtifacts and SpirvArtifacts to namespace scope with in-class aliases so every existing spelling compiles unchanged; pure move
- P0.5 of the MGPipe disaggregation (ROADMAP P0.5, ARCHITECTURE.md:260): the five
  reflection types a link produces now live in a header that includes only
  <Includes.h> and <set>, so a future server-side consumer can name them without
  dragging ShaderObject.h / SpvcSession.h / the transpiler behind it.
- Struct bodies move verbatim, comments included, re-indented one level; no field is
  added, removed, reordered or re-typed. The two glslang-typed members
  (LinkArtifacts::program, uniformInitialValues) stay as they are (B.0 D5); the
  comment mentions of glslang types are respelled without the scope token so the
  include-closure gate's "glslang:: exactly twice" limit holds.
- kInvalidUniformOffset moves to namespace scope (SpirvArtifacts defaults to it);
  ProgramObject::kInvalidUniformOffset is defined from it, so the two cannot drift.
- ProgramObject keeps in-class aliases (fully qualified on the right-hand side) for all
  nine names, so none of the 8 includers nor any ProgramObject::X spelling changes.
- ProgramArtifactsTest pins the aliases as the same types (is_same_v) and TypeFacts as
  a 44-byte POD; its first include is the new header, so it is also the proof that the
  header is self-contained.
2026-09-05 23:11:00 -04:00
swung0x48 8566a288f8 [Refactor] (Pipe, State): extract MGPipeValueTypes.h - move the render-state, sampler and vertex value types and their enums out of MG_State/GLState so MG_Pipe no longer reaches RenderState.h; pure move, member order and namespaces unchanged
- ROADMAP P0.5 / ARCHITECTURE.md value-header manifest: MGPipeTypes.h embedded
  RenderStateParameters and PixelStoreParameters through RenderState.h, which drags
  FramebufferObject.h and the whole texture/renderbuffer/sampler chain into MG_Pipe;
  purity gate A (no MG_State/MG_Impl/MG_Backend/MG_Remote in the closure) could not
  be armed for anything in MG_Pipe while that include existed.
- MGPipeValueTypes.h is a verbatim cut, comments included: the eleven RenderState.h
  enums (all of them - a split would be the maintenance trap), PixelStoreParameters,
  PerBufferBlendState, StencilFaceState, RenderStateParameters (member order
  untouched: DirectGLES' offsetof spans and PipeSpanTable.inc name the members),
  the six SamplerObject.h enums and SamplerParameters (BorderColorForm stays Uint8,
  it sets the tail padding), and VertexAttribute / VertexBufferBindingPoint /
  VertexAttributeVersion, which keep namespace MobileGL::MG_State::GLState with a
  forward-declared BufferObject so no mangled name changes.
- MAX_DRAW_BUFFERS becomes inline constexpr kMGMaxDrawBuffers in namespace MobileGL
  and FramebufferObject::MAX_DRAW_BUFFERS is defined from it, so the eighty existing
  spellings keep working and the two cannot drift. No other constant is added.
- The four MG_State headers become forwarders (include the value header, keep their
  class definitions); RenderState.cpp gains a direct FramebufferObject.h include
  because it spells FramebufferObject::MAX_DRAW_BUFFERS and only ever got that
  header transitively. No other TU lost a transitive include: the full build
  (Release, clang, tests + integration tests) passed without touching anything
  under MG_Backend, MG_Impl or MG_Util.
- DynamicBackendParameters does NOT move (SizeT members and TextureTarget-taking
  member functions make that a type change, not a move); MGPipeTypes.h keeps its
  BackendObject.h include and the debt comment now says so, which is why gate A
  asserts MGPipeValueTypes.h rather than MGPipeTypes.h.
- New trip wires in the header: trivially-copyable + exact sizeof for
  PixelStoreParameters/PerBufferBlendState/StencilFaceState (28), SamplerParameters
  (100), VertexAttributeVersion (6), RenderStateParameters (1168, standard layout,
  BlendStates before LogicOp, BlendStates sized by kMGMaxDrawBuffers). Their runtime
  twins ValueTypeLayoutsArePinned and the carrier check
  ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail (Pack at 1168,
  CapabilityBits at 1200) are added to PipeCatalogueTest without a new include.
- gen_pipe.py's "field lists of their own in P0.5" comment now says P1 (the
  comparator needs std::array<struct> support first); PipeVerify.inc regenerated.
- Verified: ctest -L unit 1460/1460 and -L integration-gpu green; ctest -N names
  a superset of feat/disaggregated@6672778b (two added, none lost); one definition
  per moved type; the -H closure of the new header contains no MG_State/MG_Backend/
  MG_Impl/MG_Remote header and the header compiles alone; nm --defined-only -S
  against the base libMobileGL.so: 0 added / 0 removed / 0 resized, .text
  byte-identical, the only differing bytes are the build-id and two stamp strings.
2026-09-05 23:11:00 -04:00
swung0x48 2318f6ae44 [Tooling] (Purity): add scripts/symbol_report.py - per-symbol nm/.text attribution between two libMobileGL.so builds
- P0.5's acceptance gate requires every `nm --defined-only -S` delta to be explainable
  per symbol, and nothing in the tree reads nm or size today.
- The problem the tool exists to solve: de-nesting a type renames every mangled name
  that mentions it, including inside template arguments, so a raw nm diff of a pure move
  looks catastrophic. --strip-scope 'A::B::C::' rewrites 'A::B::C::X' to 'A::B::X' on the
  demangled name before comparing, which folds those into a renamed-only bucket - same
  normalised name, byte-identical size - and leaves the real churn visible.
- Buckets sorted by |delta|: removed, added, resized, renamed-only, unchanged, plus
  .text/.data/.bss/Total from `size --format=sysv`; --only-names narrows the listing,
  --markdown/--json write the report the integrator pastes into the merge commit.
- Always exits 0 (this is informational, ARCHITECTURE.md:507); --fail-on-added-bytes is
  accepted and documented as reserved for the day it becomes a hard gate.
- The docstring carries the guard rails a reader would otherwise supply by assumption:
  same CMAKE_BUILD_TYPE (the visibility presets differ per configuration), LTO off on
  both sides, same compiler - and every report prints both paths and their byte sizes.
- --self-test runs two canned nm/size transcripts through the same parser and bucketer
  and pins all five buckets, including that a de-nested member folds to renamed rather
  than to an added+removed pair.
2026-09-05 22:51:32 -04:00
swung0x48 fe3dc1dde8 [CI] (Purity): add scripts/check_include_closure.py - the -H include-closure gate for the P0.5 headers with an always-on negative control, wired as a unit ctest and the include-graph-check job
- ROADMAP P0.5 asks for a CI assertion on the include closure of the two headers the
  phase extracts, and ROADMAP.md:7 asks every gate to be able to fail for the reason it
  exists. `nm --undefined-only` cannot express either: a header that is included but
  whose types are never named leaves no symbol behind, and "included at all" is exactly
  the coupling P1 and P7 have to sever. The preprocessor's own `-H` transcript can.
- Three probes, coded against the fixed path contract of the P0.5 brief so this package
  lands before the headers do: value-header (MG_Pipe/MGPipeValueTypes.h: no MG_State/,
  MG_Impl/, MG_Backend/, MG_Remote/), artifacts-header (ProgramArtifacts.h: no
  ShaderObject.h, ShaderTranspiler/, Config.h, MG_Backend/, BufferState/, ProgramState/
  Shader*, plus a budget of two `glslang::` tokens for the two members D5 keeps
  verbatim) and wire-header (ITransport.h must not reach Includes.h - green today, so
  the gate has a live probe from its first commit).
- The forbidden sets say nothing about glslang, spirv-cross or vulkan on purpose:
  Includes.h pulls all three unconditionally and both new headers are allowed
  <Includes.h>, so a textually glslang-free closure is unsatisfiable by construction.
  P7 measures that with `nm -D | grep glslang` on the server binary instead.
- Two modes because they check different things. Text mode walks literal #include lines,
  needs no compiler and no submodules, and is what the ctest runs (the CI `test` job's
  runner has neither); clang mode is the arbiter, and adds a -fsyntax-only pass proving
  the header is self-contained. `--mode both` additionally fails on a disagreement
  between the two violation sets, so text mode's blindness to `#if` cannot hide a hit.
- -H parsing normalises before matching (today's transcripts contain
  TextureState/../SamplerState/SamplerObject.h) and accepts only `^\.+ ` lines, which
  discards the "Multiple include guards may be useful for:" paragraph g++ appends.
  Both are pinned by a canned-transcript check inside --self-test.
- D9 skip semantics: a probe whose header does not exist prints SKIP and is counted, and
  --require-all turns every SKIP into a failure. That is what lets the gate land first
  and still stops an all-SKIP run from passing for free once the headers exist; the
  integrator flips --require-all on after all three P0.5 packages land.
- --self-test is always on in both the ctest and the CI job: it synthesizes its TUs in a
  tempdir (it never touches a tracked file) and requires a negative control that does
  not depend on P0.5 at all - MGPipeHandles.h plus RenderState.h checked against the
  value-header list - to report RenderState.h as a depth-1 violation in every enabled
  mode. Zero trips anywhere is an ::error:: and exit 1, because a gate that cannot go
  red is not a gate. Controls 2 and 3 arm themselves as the two headers appear.
- Registered as MobileGLPurity.IncludeClosure with LABELS unit so `ctest -L unit` runs
  it, and with no ENVIRONMENT property, which would replace the job env wholesale.
2026-09-05 22:51:32 -04:00
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
swung0x48 50fb13430f [Merge] (MG_State, ShaderTranspiler, tools/cts): land the write-map landing and open-ended fp64 storage block fixes that take KHR-Single-GL45.subgroups to 100% on Magma 2026-09-05 05:43:34 -04:00
swung0x48 d4f8adcf6d [Tools, Test] (tools/cts, MG_Test): make the CTS runner's chunk timeout idle-based so a healthy 20-minute invocation is no longer killed and its in-flight case mis-recorded as a crash, and link MSVC test executables with /WHOLEARCHIVE so the dllimport gl* references in GetProcAddress.cpp resolve 2026-09-05 05:36:50 -04:00
swung0x48 795e08f7e6 [Fix, Test] (ShaderTranspiler): flatten fp64 storage blocks whose last member is a runtime array - the pass declined them, so the fp64 demotion re-derived ArrayStride 4 over the application's 8/16/32-byte double buffer and every double/dvecN data[] SSBO read raw words 2026-09-05 05:36:49 -04:00
swung0x48 1e7ecab4db [Fix, Test] (MG_State, BufferObject): land a non-persistent write map's staged bytes into a GPU-resident store at unmap and explicit flush instead of dropping them - SSBO binding and large-store adoption make resident stores reachable through glMapBufferRange, so every per-draw re-initialisation was silently lost 2026-09-05 05:36:48 -04:00
swung0x48 81b17c0b75 [Test] (ShaderTranspiler, Integration): pin the five reworked shapes - the read-only capture stage through the real link, the seeded carrier's ESSL declaration, the metadata-driven block redeclaration and its decline, the control-stage-less evaluation decline, and a carrier clearing an i64vec4 2026-08-28 06:22:12 -04:00
swung0x48 d1edf765f5 [Fix] (Link, Async): carry the resolved gl_PointSize capture request into the SPIR-V handoff and let a deferred verdict name its own severity - the demotion read the request off a reflection slice phase A never fills it into, so its forced carrier was dead code, and its decline reason replayed at a level no shipped build keeps 2026-08-28 06:22:12 -04:00
swung0x48 97e07190ac [Fix] (ShaderTranspiler): seed a forced point-size carrier nothing writes and decline a control stage whose live clip/cull distance would still print gl_PointSize - an ES front end deletes a never-written output, and SPIRV-Cross redeclares that block from member decorations rather than access 2026-08-28 06:22:11 -04:00
swung0x48 a4dcdf989e [Fix] (ShaderTranspiler): stop the point-size carrier landing on a location a live varying owns, and decline the evaluation stage a synthesized pass-through control stage cannot feed - an i64vec4 counted as one location, and a located input carrier trips both backends' pass-through guard 2026-08-28 06:22:11 -04:00
swung0x48 1c113e4b26 [Fix] (ShaderTranspiler): count a 64-bit integer vector as two locations in the XFB flattener's LocationSpan - it read 64-bitness off the element's float type alone, so an i64vec3/4 member packed the members after it onto locations it already owns 2026-08-28 06:22:11 -04:00
swung0x48 bf9cfb3079 [Test] (Integration): PointSizeDemotionScenario - the demoted value chain is client-invisible in both configurations, with pinned per-backend lanes and a log-latch arming guard 2026-08-28 06:22:10 -04:00
swung0x48 e1818d497a [Test] (ShaderTranspiler): pin the point-size demotion's shapes - capability stripped, carriers named and located past the program's varyings, byte-identical no-ops, whole-struct-copy decline, and the two new L1 key bits 2026-08-28 06:21:31 -04:00
swung0x48 d7f66722d1 [Fix] (ShaderTranspiler, Link, DirectGLES, DirectVulkan): demote tessellation/geometry gl_PointSize to an ordinary varying where the device cannot host the built-in - the value survives for gl_in reads and by-name capture, both backends' declines stay for shapes the pass refuses, and the verdict rides the L1 key 2026-08-28 06:21:30 -04:00
swung0x48 92dc41ebf9 [Test] (DirectVulkan, SelfTest): pin the probe's fence-timeout teardown against a fake driver, the never-worse arming refusal, and paused-span patch/instanced counting against an unpaused control 2026-08-28 06:19:41 -04:00
swung0x48 feea131d8b [Fix] (DirectVulkan, SelfTest): honour the primitives-generated probe's leak-on-timeout contract in both of its callers, count paused-span draws the frontend cannot price, refuse a substitute measured worse than the stream query, and derive the POST row's failure clause from the measurement 2026-08-28 06:19:40 -04:00
swung0x48 19f4402fbf [Test] (DirectVulkan): pin the primitives-generated probe's verdict and override mapping, and hold the reroute's GL answers and arming observable on the integration lane 2026-08-28 06:19:40 -04:00
swung0x48 1350031368 [Fix] (DirectVulkan): count GL_PRIMITIVES_GENERATED for transform-feedback-inactive draws - a bring-up probe measures the silent stream query and reroutes such draws through the dedicated primitives-generated query or a clipping-statistics pool, reported from the Vulkan POST 2026-08-28 06:19:39 -04:00
swung0x48 0ee3384b22 [Fix] (Espryt): land a SubData into an adopted store as a GPU-ordered copy - the in-place coherent write tore the frames still reading the old section bytes 2026-08-28 04:50:07 -04:00
swung0x48 ba3f8d6774 [Test] (Integration): pin the adopted mesh-arena store - cross-frame SubData visibility, readback identity, and the GPU-written readback 2026-08-28 04:19:11 -04:00
swung0x48 3327784fd0 [Fix] (Espryt): adopt mesh-arena-sized stores into coherent persistent maps at definition, and stop the flush tiers from re-synchronizing them 2026-08-28 04:19:11 -04:00
swung0x48 ff426da3a9 [Fix, Test] (MG_State, DirectVulkan): order host writes to an adopted store after recorded GPU work - a SubData issued after a dispatch landed in coherent memory before the deferred dispatch executed, so its increments overwrote the newer bytes; un-skip the DirectVulkan half of the SubData-after-dispatch scenario 2026-08-28 03:01:15 -04:00
swung0x48 08419a1fe6 [Chore] (Config): point the two stale comments at the renamed feature fields 2026-08-28 00:47:35 -04:00
swung0x48 5d51372c44 [Chore] (Config): triage backend-scoped env toggles under MOBILEGL_ESPRYT_ and MOBILEGL_MAGMA_ prefixes 2026-08-28 00:38:17 -04:00
swung0x48 7fd4550968 [Fix] (Espryt): copy before any FBO attach in the packed16 probe - attaching the array relayouts it to the plain order and was neutralizing the subject 2026-08-28 00:22:16 -04:00
swung0x48 971537058e [Fix] (Espryt): probe every allocation recipe of the packed16 shape - the Mali layout heuristic inverts between contexts, so one recipe cannot speak for the storage 2026-08-28 00:22:16 -04:00
swung0x48 dd98c450ad [Test] (SelfTest): model the params-before-upload escape so a params-first probe regression reads as a red test 2026-08-28 00:22:15 -04:00
swung0x48 5dbbbbd7eb [Fix] (Espryt): allocate the packed16 probe's textures uploads-first, the order a minted backend texture performs and the one the Mali layout choice keys on 2026-08-28 00:22:15 -04:00
swung0x48 5d140a41ce [Fix] (Espryt): re-arm the packed16 probe on the allocation-scoped mirror the device actually has and let any mirrored level trigger the widening 2026-08-28 00:22:15 -04:00
swung0x48 bf376b230f [CI] (Integration): rerun the buffer scenarios with the map flush disabled so the upload-ring tier keeps coverage 2026-08-28 00:15:15 -04:00
swung0x48 29599dcf90 [Fix] (DirectGLES): flush queued buffer ranges through a range-invalidating map first, so a partial write into a busy store is priced by the range 2026-08-27 23:54:19 -04:00
swung0x48 734fab9f90 [Test] (Integration): scope the SubData-after-dispatch readback case to DirectGLES, naming the DirectVulkan upload-ordering gap it exposed 2026-08-27 23:04:52 -04:00
swung0x48 2cd1809c29 [Fix] (Review): rebuild the packed16 probe on the CTS's three-level chains and let the POST row answer for the widening knob actually in force 2026-08-27 22:46:13 -04:00
swung0x48 faed498476 [Test] (IntegrationTest): shield the packed16 renderbuffer clear from an inherited scissor 2026-08-27 22:46:13 -04:00
swung0x48 8282eecbfa [Fix] (Espryt): store RGB565/RGB5_A1/RGBA4 as 8-bit channels where a POST probe measures the Mali packed16 array-mip field-order mirror 2026-08-27 22:46:12 -04:00
swung0x48 f4f3afb0b6 [Test] (Integration): pin that a queued SubData survives an immediate readback of a GPU-written buffer 2026-08-27 22:19:45 -04:00
swung0x48 a28da07641 [Fix] (DirectGLES): queue app buffer updates and flush them through a staged-copy upload ring instead of stalling in glBufferSubData 2026-08-27 22:19:44 -04:00
swung0x48 200c21336f [Fix] (Readback): size a cube-face pack buffer by one face, and give a cube view's face its owner layer 2026-08-27 22:05:23 -04:00
swung0x48 2c3fc583d5 [Test] (Readback): pin the one-face pixel pack buffer and a cube view's per-face layers 2026-08-27 21:58:51 -04:00
swung0x48 645a12d8bc [Fix] (Readback): read the cube face a readback names, and let glGetTextureSubImage name one 2026-08-27 21:54:47 -04:00
swung0x48 8e6acc5528 [Test] (Readback): pin that a cube map's readback answers the face it was asked for 2026-08-27 21:54:46 -04:00
swung0x48 525ffe0f14 [Test] (Review): pin the inverted override mapping, the probe's controls, and that the pinned-on lane is really armed 2026-08-27 21:06:01 -04:00
swung0x48 ad28d2b744 [Fix, Test] (Review): strip a block's member-level locations too, restore the probe's colour mask, and let the POST verdict follow the override 2026-08-27 20:15:31 -04:00
swung0x48 eab622388f [Test] (TranslationCache): pin the two interface-block location-strip flags as L2 key material 2026-08-27 20:07:49 -04:00
swung0x48 75e573c923 [Fix] (SelfTest): give the located-interface-block probe its own entry-point gate instead of the storage probe's 2026-08-27 19:56:26 -04:00
swung0x48 0d0ef13619 [Fix] (DirectGLES): run the interface-block location strip last, where its deliberately Vulkan-invalid module reaches no validator 2026-08-27 19:44:18 -04:00
swung0x48 23565fcacd [Fix, Test] (DirectGLES, SelfTest, MG_Test): probe the located-interface-block defect with its controls and cover the strip in both gates 2026-08-27 19:38:39 -04:00
swung0x48 5dc26e3e2c [Fix] (DirectGLES, ShaderTranspiler): drop the location qualifier from inter-stage interface blocks on a driver that loses their payload 2026-08-27 19:18:35 -04:00
swung0x48 90564aa82e [Test] (Review): give the geometry point-size case its own capability probe 2026-08-27 15:42:04 -04:00
swung0x48 7520607d47 [Fix] (Review): decline a point-size program the device cannot run, bound the compile-failure source dump, and correct the pass-through rationale 2026-08-27 15:42:04 -04:00
swung0x48 57635a9198 [Test] (Tessellation): unit and headless-GPU coverage for capturing a patch draw's per-vertex payload 2026-08-27 15:06:02 -04:00
swung0x48 c19d0f0b75 [Fix] (DirectVulkan): mirror gl_PointSize for capture and enable the feature its stages need 2026-08-27 15:06:02 -04:00
swung0x48 e42e7d00f5 [Fix] (DirectGLES): request the point-size extension a tessellation or geometry stage's ESSL needs 2026-08-27 15:06:01 -04:00
swung0x48 62695ee3c2 [Test] (Review): pin the single-sample sample mask, the completeness-flip sampled set, and the integer multisample placeholders 2026-08-27 13:17:56 -04:00
swung0x48 02cc0ce83c [Fix] (Review): scope the sample mask to a multisample target, version the sampled-set memo on completeness, clamp the mask word count, give the multisample placeholder every numeric domain, unwind the fixup revert one pass at a time, and bound the validator log 2026-08-27 13:03:51 -04:00
swung0x48 9e52a0b23e [Test] (DirectVulkan): pin the incomplete-default-texture draw, the unbound multisample sampler, and the unwritten redeclared gl_Position 2026-08-27 12:52:19 -04:00
swung0x48 9dee53337f [Fix] (DirectVulkan): give an unbound multisample sampler a placeholder, clear a multisample texture through a load-op pass, and plumb glSampleMaski into the pipeline 2026-08-27 12:52:19 -04:00
swung0x48 28c5badf8f [Fix] (SPIR-V): keep the entry-point interface consistent when the clip fixup and the XFB position mirror inject through gl_Position 2026-08-27 12:52:18 -04:00
swung0x48 01116f7b41 [Fix] (DirectVulkan): treat an incomplete sampled texture as unbound in the collect path too, and stop dereferencing a declined texture sync 2026-08-27 12:52:18 -04:00
swung0x48 05d627ba2d [Fix] (Xfb): never issue a transform feedback capture-point bind the application did not ask for, and error-check the driver span instead of losing it silently 2026-08-27 12:28:32 -04:00
swung0x48 ea5d52f126 [Test] (Tessellation): assert the patch-array cross-stage link outright now that the fork pin carries the guard 2026-08-27 10:52:00 -04:00
swung0x48 3c70b4fc0f [Fix] (Program): read an API colour index of zero as no override, matching the IO resolver and ProgramInterface 2026-08-27 10:52:00 -04:00
swung0x48 b6d6316333 [Fix] (Blend): decline a GL_SRC1_* factor on an incapable GLES driver even when blending is off 2026-08-27 10:51:59 -04:00
swung0x48 3c9ab5a68f [Test] (Blend): make the dual-source scenario's capability gate skip the case, not just the helper 2026-08-27 10:48:22 -04:00
swung0x48 532b5e9cc5 [Fix] (Program): let two fragment outputs share a colour number when their colour index differs 2026-08-27 10:48:22 -04:00
swung0x48 06605ed0ea [Test] (Tessellation): include the standard headers the link test uses directly 2026-08-27 10:48:21 -04:00
swung0x48 e1d5bdc4a5 [Fix] (Blend): decline an unsupported dual-source blend instead of throwing through the GL ABI 2026-08-27 10:48:21 -04:00
swung0x48 66867a41ba [Fix] (Program): count the tessellation control stage as a transform-feedback capture stage 2026-08-27 10:48:20 -04:00
swung0x48 ebff4b21f7 [Fix] (Query): tie the two tessellation pipeline-statistics targets to the GL_ARB_tessellation_shader advertisement 2026-08-27 10:48:20 -04:00
swung0x48 d4e7378868 [Chore] (ShaderTranspiler): pin glslang fork at d89cf443 (patch-array io-resize guard for the evaluation stage) 2026-08-27 10:22:37 -04:00
240 changed files with 46428 additions and 2044 deletions
+3 -3
View File
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
+8 -4
View File
@@ -6,6 +6,10 @@ on:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
# branch runs the full lane on every push so a phase's landing is not gated on
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
workflow_dispatch:
jobs:
@@ -417,12 +421,12 @@ jobs:
- name: Retrace and validate
env:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
+784 -9
View File
@@ -6,7 +6,19 @@ on:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
# branch runs the full lane on every push so a phase's landing is not gated on
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
workflow_dispatch:
inputs:
baseline_sha:
description: >-
The commit monolith-symbol-report compares this tree against. P1's G1 says the pull
build is byte-identical to feat/disaggregated@087685d1, and that is what the default
names. The trigger set is unchanged: this job runs on workflow_dispatch only.
required: false
default: "087685d1"
jobs:
build-linux:
@@ -265,16 +277,26 @@ jobs:
# crash stack without burning a CI round on an in-workflow debugger.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
# Second, filtered pass: with the range-invalidating map flush disabled,
# the buffer scenarios run on the upload ring's staged-copy tier - which
# the default pass never reaches (the map tier absorbs every flush on
# Mesa), so without this the Mali fallback tier would have zero CI
# coverage. The flag is NOT baked into the ctest ENVIRONMENT properties,
# so an inline env reaches the test processes (unlike the ICD pin above).
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L integration-gpu --no-tests=error
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest -V -L integration-gpu \
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
else
ctest --output-on-failure -L integration-gpu --no-tests=error
MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --output-on-failure -L integration-gpu \
-R 'Buffer|Readback|Atomic|Ssbo|Arena' --no-tests=error
fi
- name: Upload core dumps
@@ -285,6 +307,399 @@ jobs:
path: /tmp/core.*
if-no-files-found: ignore
# THE THIRD CI MODE (ARCHITECTURE.md 13.2-(2)): the same library, built with the PipeInputs
# comparator compiled in, running the integration suite and a trace subset with two state models
# in one address space. It is a second build rather than a flag on the first because
# MOBILEGL_PIPE_VERIFY is a compile-time option - the snapshot, the entry compare and the
# compare-at-read hook do not exist in the shipped library, and are never meant to.
build-linux-verify:
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
actions: write
contents: read
env:
BUILD_DIR: build-verify
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache/restore@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Update glslang external sources
working-directory: 3rdparty/glslang
run: python update_glslang_sources.py
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build
- name: Show installed toolchain
run: |
ccache --version
clang-20 --version
clang++-20 --version
ld.lld-20 --version || ld.lld --version || true
dpkg -l 'libc++*' 'libegl*' 'libgles*' 'mesa*' 'vulkan*' || true
- name: Configure CMake
# Release/INFO like the shipped build on purpose. The poison arms in this configuration
# through MOBILEGL_PIPE_VERIFY (PipeInputs.h derives MOBILEGL_PIPE_POISON from it), so this
# job needs neither a Debug log level nor MOBILEGL_BUILD_DISAGGREGATED - and a Debug build
# would compare a different library from the one the other lanes measure. (build-linux
# switches to Debug under ACTIONS_STEP_DEBUG; this job deliberately does not - a Debug
# build flips CXX_VISIBILITY_PRESET and arms MOBILEGL_PIPE_POISON through a second, unrelated
# arm of its #if, so the debug switch would change what the lane is measuring.)
run: |
cmake -S . -B "${BUILD_DIR}" -G Ninja \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Release \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DMOBILEGL_PIPE_VERIFY=ON \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
- name: Build
run: cmake --build "${BUILD_DIR}" --parallel "$(nproc)"
# The lane is worthless if the option silently did not take, and that is a one-character
# mistake away at all times (a typo'd -D is not an error in CMake). Two checks, both cheap:
# the comparator's entry point must be in the library, and the fill entry point with it.
#
# `nm` and NOT `nm -D`. The library is built CXX_VISIBILITY_PRESET hidden in every non-Debug
# configuration (CMakeLists.txt:600-604) and the MGPipe entry points are plain namespace
# functions with no export attribute, so not one of them appears in the DYNAMIC table: on a
# perfectly healthy verify build `nm -D --defined-only ... | grep -c MGPipe` answers 0 out of
# ~11900 exported symbols, and a gate spelled that way is red forever for a reason that has
# nothing to do with what it claims to test. The static symbol table has them as local `t`
# entries, this artifact is never stripped, and `No MG_Remote in the pull build` below already
# uses this spelling. The symbol count guards the remaining hole: a stripped library would
# make both greps fail for a third, silent reason.
- name: The verify library really carries the comparator
run: |
test -f "${BUILD_DIR}/libMobileGL.so"
defined=$(nm --defined-only "${BUILD_DIR}/libMobileGL.so" | wc -l)
if [ "${defined}" -lt 1000 ]; then
echo "::error::nm --defined-only sees only ${defined} symbols in ${BUILD_DIR}/libMobileGL.so - it looks stripped, so the two checks below could not have failed honestly"
exit 1
fi
for entry in MGPipeVerifyInputs MGPipeFillForVerb; do
if ! nm --defined-only "${BUILD_DIR}/libMobileGL.so" | grep -q "${entry}"; then
echo "::error::libMobileGL.so defines no ${entry}: -DMOBILEGL_PIPE_VERIFY=ON did not take, and every lane that consumes this artifact would run the comparator-free library and pass having compared nothing"
exit 1
fi
done
echo "libMobileGL.so defines MGPipeVerifyInputs and MGPipeFillForVerb (${defined} defined symbols)"
- name: Show ccache stats
if: always()
run: ccache --show-stats
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
- name: Package Linux verify runtime
run: |
mkdir -p ci-artifacts
mapfile -t SHARED_LIBS < <(find "${BUILD_DIR}" -type f \( -name '*.so' -o -name '*.so.*' \) -print | sort)
tar \
--exclude='*/CMakeFiles' \
--exclude='*.o' \
--exclude='*.a' \
--exclude='*.ninja*' \
--exclude='build.ninja' \
--exclude='cmake_install.cmake' \
-czf ci-artifacts/mobilegl-linux-runtime-verify.tgz \
"${BUILD_DIR}/CTestTestfile.cmake" \
"${BUILD_DIR}/MobileGL/MG_Test" \
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
"${SHARED_LIBS[@]}"
- name: Upload Linux verify runtime
uses: actions/upload-artifact@v7
with:
name: mobilegl-linux-runtime-verify
path: ci-artifacts/mobilegl-linux-runtime-verify.tgz
if-no-files-found: error
# The verify lane itself, plus the two negative controls that keep it falsifiable. The controls
# are ALWAYS-ON steps, not a manual exercise: a gate that can only be shown to work by someone
# remembering to break it on purpose is a gate that has already stopped working.
integration-verify:
runs-on: ubuntu-latest
timeout-minutes: 180
needs: build-linux-verify
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux verify runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime-verify
path: .
- name: Unpack Linux verify runtime
run: |
tar -xzf mobilegl-linux-runtime-verify.tgz
test -f build-verify/libMobileGL.so
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-verify').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
- name: Integration scenarios under MOBILEGL_PIPE_VERIFY
working-directory: build-verify
# --no-tests=error is half the gate: the verify entries only exist when the library was
# configured with -DMOBILEGL_PIPE_VERIFY=ON, so a build that lost the option matches no
# tests and reds here instead of reporting a green run of nothing. The other half is
# PipeVerifyArmingScenario.Armed, which fails when the library never printed its arming
# line - the failure mode a bare `MOBILEGL_PIPE_VERIFY=1` cannot detect by itself.
#
# SCOPE, stated so nobody reads more into a green than is there: this is every integration
# ENTRY under the comparator, not every integration CONFIGURATION. The `integration` job
# runs a second, filtered pass with MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 for the
# upload ring's staged-copy tier; that pass is 186 entries here and, at the 5-10x the
# comparator costs, is not affordable inside this job's budget. The tier is covered by
# `integration`, unverified, and P2 can take it once the comparator's cost is known.
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: "1"
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: "1"
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
ctest -V -L integration-verify --no-tests=error
else
ctest --output-on-failure -L integration-verify --no-tests=error
fi
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
# library opens it fopen(path, "w"), so after an ambient lane of 400-odd processes the file
# holds the LAST one - grepping it would say nothing about the other 405 and would red a
# healthy lane whenever the last entry happened not to issue a verb (which is what the
# PoisonOmissionScenario parent, the last ambient entry, does by construction: it forks,
# execve()s and reads files). The DirectGLES.VerifyArming. / DirectVulkan.VerifyArming.
# entries are one process each on a log path nothing else writes, so this grep means exactly
# what it says.
#
# What it proves: arming is a property of (this library, this environment), and these two
# processes ran the same library with the same MOBILEGL_PIPE_VERIFY=1 as their ~400 ambient
# siblings. It is not, and cannot be, a per-process census - the shared log cannot support one.
# It catches the case ctest cannot: an arming entry that SKIPPED still reports green.
- name: The verify lanes armed the comparator
working-directory: build-verify
run: |
shopt -s nullglob
logs=(MobileGL/MG_IntegrationTest/pipe-verify-arming-*.log)
if [ ${#logs[@]} -lt 2 ]; then
echo "::error::found ${#logs[@]} pipe-verify-arming-*.log (expected one per backend). The VerifyArming. entries did not run, so nothing in this job establishes that the comparator was ever armed."
exit 1
fi
for log in "${logs[@]}"; do
if ! grep -q "MGPipe: verify armed" "${log}"; then
echo "::error::${log} carries no arming line: that lane's process ran the whole scenario without the comparator, so every green entry beside it is green for no reason"
exit 1
fi
done
echo "arming line present in all ${#logs[@]} arming-lane log(s)"
# NEGATIVE CONTROL A (gate G4). The knob perturbs one field in the snapshot arm before the
# entry compare, so a working comparator must abort the run. This step passes when ctest
# FAILS - `if ctest ...; then error` - which is the only shape that can catch a comparator
# that silently compares nothing.
#
# The knob reaches the test process through the JOB environment: no ctest ENVIRONMENT
# property on the ambient Verify. entries names it (MG_IntegrationTest/CMakeLists.txt says
# so out loud), and a property entry would otherwise override this and the control would
# prove nothing. Same precedent as MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH in `integration`.
- name: Negative control A - a corrupted snapshot field must turn the lane red
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_PIPE_VERIFY_CORRUPT: GetRenderStateParameters
run: |
FILTER='DirectGLES\.Verify\..*ClearThenReadPixels'
# An empty selection would ALSO make ctest exit non-zero (--no-tests=error), and this
# step reads non-zero as "the control worked" - so the selection is counted first. A
# control that passes because it ran nothing is worse than no control.
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 1 ]; then
echo "::error::negative control A selected ${matched} tests; its filter no longer matches anything"
exit 1
fi
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left ${matched} verify entries GREEN. The comparator is not comparing, so every green entry above is green for no reason."
exit 1
fi
echo "the corrupted field turned ${matched} selected entries red, as it must"
# NEGATIVE CONTROL B (gate G5). The omission skips the STAMP of one field for one verb while
# still copying its value - indistinguishable from a fill row nobody wrote - so the poison
# must abort the glGenerateMipmap. Again: this step passes when ctest fails.
#
# The entry it targets is PoisonOmissionScenario.WithoutOmissionCompletes, which is green in
# the ambient lane above and is the ONLY integration entry in the tree that calls
# glGenerateMipmap at all. It deliberately does not skip itself when the knob is set, exactly
# so that this control has something to turn red.
- name: Negative control B - an omitted fill point must turn the lane red on that verb
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_PIPE_POISON_OMIT: GenerateMipmap:GetActiveTextureUnit
run: |
FILTER='DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes'
matched=$(ctest -N -L integration-verify -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 1 ]; then
echo "::error::negative control B selected ${matched} tests; its filter no longer matches anything"
exit 1
fi
if ctest --output-on-failure -L integration-verify -R "${FILTER}" --no-tests=error; then
echo "::error::MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit left the verify lane GREEN. The per-verb poison is not armed, so a forgotten fill row would ship silently."
exit 1
fi
echo "the omitted fill point turned the lane red, as it must"
- name: Upload verify lane logs
if: always()
uses: actions/upload-artifact@v7
with:
name: integration-verify-logs
path: build-verify/MobileGL/MG_IntegrationTest/pipe-*.log*
if-no-files-found: warn
- name: Upload core dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: integration-verify-core-dumps
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
# P0.5 interface-purity gate A (ARCHITECTURE.md:501): the two extracted headers' include closure,
# asserted on `-H` output because `nm --undefined-only` is blind to "included but not called" -
# a header whose types are never named leaves no symbol behind, and "included at all" is exactly
# the coupling P1 and P7 have to sever. Needs a preprocessor and three header submodules, no
# CMake configure and no glslang sources, so like pipe-gates it does not depend on build-linux.
# The script's own --self-test is always on: a negative control that stopped tripping fails the
# job, because a gate that cannot go red is not a gate (ROADMAP.md:7).
include-graph-check:
name: Include-closure purity gate
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Check out the three header submodules the closure needs
# ska/flat_hash_map.hpp, xxhash.h and vulkan/vulkan.h are the only submodule headers
# Includes.h reaches; glslang and spirv_cross are vendored under include/.
run: git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers
- name: Install clang and the X11 headers vulkan.h pulls on Linux
# Includes.h defines VK_USE_PLATFORM_XLIB_KHR before <vulkan/vulkan.h>, which then
# includes <X11/Xlib.h>; without libx11-dev every clang-mode probe dies in the
# preprocessor and the gate reports 5 problems that have nothing to do with purity.
run: sudo apt-get update && sudo apt-get install -y clang-20 libx11-dev
- name: Include-closure assertions and negative control
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
benchmark:
runs-on: ubuntu-latest
needs: build-linux
@@ -496,6 +911,7 @@ jobs:
outputs:
matrix: ${{ steps.trace-cases.outputs.matrix }}
names: ${{ steps.trace-cases.outputs.names }}
verify-matrix: ${{ steps.trace-cases.outputs.verify-matrix }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -505,6 +921,9 @@ jobs:
run: |
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
# The subset the verify build retraces ("verify": true in trace_cases.json). It is a
# SUBSET of the matrix above, so retrace-verify needs no fixtures of its own.
echo "verify-matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-verify-matrix)" >> "$GITHUB_OUTPUT"
trace-fixtures:
name: trace fixture (${{ matrix.case }})
@@ -644,9 +1063,9 @@ jobs:
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
export MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
export MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1
export MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
@@ -718,10 +1137,295 @@ jobs:
archive: false
if-no-files-found: error
# The trace half of the third CI mode. Same replay, same goldens, but the library underneath is
# the verify build and MOBILEGL_PIPE_VERIFY=1 is in the environment, so every backend read of
# frontend state is checked against a snapshot taken at the verb boundary. Eight cases rather
# than the full lane's 40 (tools/trace_replay/trace_cases.json, "verify": true): the comparator
# is budgeted at 5-10x, and the full sweep is a phase-exit / workflow_dispatch run.
retrace-verify:
name: retrace verify (${{ matrix.backend }}, ${{ matrix.case }})
runs-on: ubuntu-latest
timeout-minutes: 240
needs:
- build-linux-verify
- build-retrace
- trace-cases
- trace-fixtures
if: ${{ always() && needs.build-linux-verify.result == 'success' && needs.build-retrace.result == 'success' && needs.trace-cases.result == 'success' }}
strategy:
fail-fast: false
max-parallel: 4
matrix: ${{ fromJSON(needs.trace-cases.outputs.verify-matrix) }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 16
- name: Checkout repo
uses: actions/checkout@v6
- name: Download trace fixture
uses: actions/download-artifact@v8
with:
name: trace-fixture-${{ matrix.case }}
path: trace-fixture-download
- name: Install trace fixture
run: |
mkdir -p tools/trace_replay/fixtures
find trace-fixture-download -type f -exec cp {} tools/trace_replay/fixtures/ \;
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers
test -e /usr/lib/x86_64-linux-gnu/libEGL.so
test -e /usr/lib/x86_64-linux-gnu/libGLESv2.so
- name: Download Linux verify runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime-verify
path: .
- name: Download trace replay
uses: actions/download-artifact@v8
with:
name: mobilegl-trace-replay
path: .
- name: Unpack the VERIFY runtime as the library under test
# build-retrace's CTestTestfile.cmake has the absolute path
# <workspace>/build-linux/libMobileGL.so frozen into every case, so the swap happens here
# rather than through a variable: the verify .so is put where that path points. The nm
# check is what makes the swap falsifiable - a run against the ordinary library would
# carry no comparator, ignore MOBILEGL_PIPE_VERIFY entirely, and match its golden.
#
# `nm`, not `nm -D`, for the reason spelled out in build-linux-verify: everything MGPipe is
# hidden-visibility in a Release build and the dynamic table has none of it.
run: |
tar -xzf mobilegl-linux-runtime-verify.tgz
tar -xzf mobilegl-trace-replay.tgz
test -f build-verify/libMobileGL.so
test -f build-retrace/tools/trace_replay/mobilegl_trace_replay
mkdir -p build-linux
cp build-verify/libMobileGL.so build-linux/libMobileGL.so
if ! nm --defined-only build-linux/libMobileGL.so | grep -q MGPipeVerifyInputs; then
echo "::error::the library unpacked at build-linux/libMobileGL.so defines no MGPipeVerifyInputs, so this retrace would replay against a comparator-free build and pass on its golden having verified nothing"
exit 1
fi
echo "the library at build-linux/libMobileGL.so is the verify build"
- name: Retrace and validate under MOBILEGL_PIPE_VERIFY
working-directory: build-retrace/tools/trace_replay
# run_trace_case.cmake turns MOBILEGL_PIPE_VERIFY into three assertions of its own (the
# arming line, no Fatal{PipeVerifyDiffer, no Fatal{UnmigratedPipeInput), so a case that
# somehow ran the wrong library reds here instead of passing on its golden.
# --timeout 10800: the 1800s cases run 5-10x slower with both comparator arms live, which
# is well past ctest's 1500s default.
run: |
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
export MOBILEGL_PIPE_VERIFY=1
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error --timeout 10800 \
-R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
# The retrace lane's own always-on negative control, on one case so it costs one short trace:
# with a snapshot field corrupted, the SAME replay must fail. Without it, "40 traces, zero
# divergences" would be a statement about a comparator nobody watched.
#
# The rerun replays into the SAME case directory, so the verified run's images are put aside
# first and restored before the verdict: "Upload actual image" below runs `if: always()` and
# would otherwise ship the deliberately corrupted run's output under the name of the good one.
# The restore happens whichever way the control goes, which is why the ctest exit status is
# captured rather than tested inline.
- name: Negative control - a corrupted snapshot field must red this retrace
if: ${{ matrix.case == 'OpenRA' && matrix.backend == 'DirectGLES' }}
working-directory: build-retrace/tools/trace_replay
run: |
export MOBILEGL_PIPE_VERIFY=1
export MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters
GOOD_OUTPUT="${RUNNER_TEMP}/openra-verified-output"
rm -rf "${GOOD_OUTPUT}"
if [ -d OpenRA ]; then
cp -a OpenRA "${GOOD_OUTPUT}"
fi
set +e
ctest -V --no-tests=error --timeout 10800 \
-R '^MobileGLTraceReplay\.OpenRA\.DirectGLES$'
control_rc=$?
set -e
if [ -d "${GOOD_OUTPUT}" ]; then
rm -rf OpenRA
mv "${GOOD_OUTPUT}" OpenRA
echo "restored the verified run's OpenRA output over the corrupted rerun's"
fi
if [ "${control_rc}" -eq 0 ]; then
echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left the OpenRA retrace GREEN, so the comparator is not comparing and the whole verify retrace lane proves nothing."
exit 1
fi
echo "the corrupted field turned the retrace red, as it must (ctest exit ${control_rc})"
- name: Upload core dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: retrace-verify-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
path: /tmp/core.*
if-no-files-found: ignore
- name: Upload actual image
if: always()
uses: actions/upload-artifact@v7
with:
name: retrace-verify-result-${{ matrix.backend }}-${{ matrix.case }}
path: |
build-retrace/tools/trace_replay/${{ matrix.case }}/actual-images/**
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
if-no-files-found: warn
# G1's own job: the pull build must be the tree before P1, symbol for symbol and byte for byte.
# workflow_dispatch only - it builds the library twice from scratch, and its answer is about a
# BASELINE rather than about this push, so a per-push run would be measuring the wrong pair.
monolith-symbol-report:
name: monolith symbol report
runs-on: ubuntu-latest
timeout-minutes: 180
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 32
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Restore ccache
uses: actions/cache/restore@v5
with:
path: .ccache
key: ${{ runner.os }}-test-${{ github.job }}-ccache-v1
restore-keys: |
${{ runner.os }}-test-${{ github.job }}-ccache-
- name: Prepare Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.1
with:
vulkan-query-version: 1.4.304.1
vulkan-components: Vulkan-Headers, Vulkan-Loader
vulkan-use-cache: true
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y ccache clang-20 clang++-20 lld-20 libc++-20-dev libc++abi-20-dev libvulkan-dev libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers ninja-build binutils
# Both sides with IDENTICAL flags, LTO off, the same compiler and the same standard library:
# symbol_report.py's guard rails (scripts/symbol_report.py) say a mismatched pair "adds"
# thousands of symbols and the comparison then means nothing. The library alone - no tests,
# no benchmark, no integration test, no trace replay - because those targets do not ship.
- name: Build the baseline library (${{ inputs.baseline_sha }})
run: |
git worktree add ../baseline "${{ inputs.baseline_sha }}"
cd ../baseline
git submodule update --init --recursive
(cd 3rdparty/glslang && python update_glslang_sources.py)
cmake -S . -B build-sym-base -G Ninja \
-DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Release \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_INTEGRATION_TEST=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=OFF \
-DMOBILEGL_PIPE_PUSH=OFF -DMOBILEGL_PIPE_VERIFY=OFF \
-DMOBILEGL_ENABLE_LTO=OFF \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build-sym-base --parallel "$(nproc)"
cp build-sym-base/libMobileGL.so "${GITHUB_WORKSPACE}/libMobileGL-baseline.so"
- name: Build the head library
run: |
(cd 3rdparty/glslang && python update_glslang_sources.py)
cmake -S . -B build-sym-head -G Ninja \
-DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Release \
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_INTEGRATION_TEST=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=OFF \
-DMOBILEGL_PIPE_PUSH=OFF -DMOBILEGL_PIPE_VERIFY=OFF \
-DMOBILEGL_ENABLE_LTO=OFF \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build-sym-head --parallel "$(nproc)"
# The monolith must not have grown a remote half. ARCHITECTURE.md:506: MG_Remote lives behind
# MOBILEGL_BUILD_DISAGGREGATED and nothing of it may reach a shipped pull build.
- name: No MG_Remote in the pull build
run: |
if nm --defined-only build-sym-head/libMobileGL.so | grep -q MG_Remote; then
echo "::error::the pull build defines MG_Remote symbols; the disaggregated half leaked into the monolith"
nm --defined-only build-sym-head/libMobileGL.so | grep MG_Remote | head -20
exit 1
fi
echo "no MG_Remote symbols in the pull build"
- name: Symbol report (G1)
run: |
python3 scripts/symbol_report.py \
--before libMobileGL-baseline.so \
--after build-sym-head/libMobileGL.so \
--threshold 0 \
--fail-on-symbol-set-change \
--fail-on-added-bytes 0 \
--markdown symbol-report.md \
--json symbol-report.json
- name: Upload the symbol report
if: always()
uses: actions/upload-artifact@v7
with:
name: monolith-symbol-report
path: |
symbol-report.md
symbol-report.json
if-no-files-found: error
remove-artifact-clutter:
name: remove artifact clutter
runs-on: ubuntu-latest
needs: retrace-summary
# (d) retrace-verify too: this job deletes the trace-fixture-* artifacts, and the verify
# retraces download the same ones.
needs:
- retrace-summary
- retrace-verify
if: always()
permissions:
actions: write
@@ -730,14 +1434,19 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
# Both retrace lanes, not just the pull one: `retrace verify (backend, case)` downloads
# the same trace-fixture-<case> artifact, and a failed verify retrace is exactly when
# someone needs that fixture to reproduce locally. The two prefixes are stripped in
# order, longest first, because "retrace (" is not a prefix of "retrace verify (".
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${job_name#retrace verify (*, }"
case_name="${case_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
--jq '.jobs[] | select((.name | startswith("retrace (")) or (.name | startswith("retrace verify ("))) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
@@ -768,3 +1477,69 @@ 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
# The generators' own negative controls: canned inputs that MUST trip each structural check
# (a field list that does not cover its struct's members, a verb set that is not the function
# table's). Regenerating and diffing above cannot see a check that silently stopped
# checking - a broken gate and a clean tree produce the same green.
- name: The MGPipe generators' checks can still fail
run: python3 scripts/gen_pipe.py --self-test
# The same question for the symbol tool the P1 gate is written in terms of.
- name: The symbol report's buckets and gates can still fail
run: python3 scripts/symbol_report.py --self-test
# 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 P2, when the mapping file exists to diff against
# (ROADMAP.md:18 puts the first mapping round in P2, not P1).
- 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
+139
View File
@@ -14,6 +14,19 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
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 +251,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
@@ -285,6 +300,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
@@ -306,6 +322,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -313,6 +330,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -414,6 +432,65 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
)
# ---------------------------------------------------------------------------
# MG_Remote (disaggregated transport). Everything below is gated: with the
# option OFF not one file here is compiled and no include path is added.
# ---------------------------------------------------------------------------
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
# a checkout without the submodule must configure and build, just without the
# disaggregated shape, rather than fail with a missing-header error a hundred
# lines later. Note this only checks for the RUNTIME headers - flatc is never
# built here (see scripts/gen_protocol.py).
if (MOBILEGL_BUILD_DISAGGREGATED AND
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
message(WARNING
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
"disaggregated shape for this configure; the cached ON takes effect once the "
"submodule is present.")
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
# made the plain re-configure after `git submodule update` stay OFF with no message at
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
# was, so the next configure - with the submodule there - honours it.
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
endif()
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
# cache write, for the same reason as the disaggregated fallback above.
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
set(MOBILEGL_PIPE_PUSH ON)
endif()
if (MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
MobileGL/MG_Impl/Pipe/PipeFill.cpp
)
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
@@ -464,11 +541,26 @@ set(MOBILEGL_COMPILE_DEF
-DASIO_NO_DEPRECATED
)
if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
endif()
if (MOBILEGL_PIPE_PUSH)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
endif()
if (MOBILEGL_PIPE_VERIFY)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
endif()
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}
@@ -479,6 +571,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}
)
@@ -695,3 +794,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()
+164 -25
View File
@@ -69,34 +69,34 @@ namespace MobileGL::MG_Config {
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
// there, because the view's ES internalformat is normalized independently of the storage
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
// so that work can be done without editing the gate.
Bool EnableGlesTextureView = false;
Bool EsprytEnableTextureView = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool EsprytUseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool DisableSubgroup = false;
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// FixIterationRPSubgroupScratch below instead). Off by default.
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
@@ -106,12 +106,12 @@ namespace MobileGL::MG_Config {
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool IterationRPFixBarrier = false;
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
@@ -119,7 +119,7 @@ namespace MobileGL::MG_Config {
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
@@ -132,16 +132,39 @@ namespace MobileGL::MG_Config {
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
Bool EsprytAvoidSamplerMipmapMinFilter = false;
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
// the uniform turns a compile-time-constant LOD into a runtime expression, which
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
Bool AvoidExplicitLodBias = false;
Bool EsprytAvoidExplicitLodBias = false;
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
// match them by block name and member sequence instead. The Mali ES driver delivers
// nothing at all through a located block once a tessellation or geometry stage is in
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
// healthy driver - which is what the integration lane does, since llvmpipe and
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
// on a device that advertises neither EXT/OES_tessellation_point_size /
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
// natively and would otherwise never run this code, which is what the pinned
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
// negative control). Cross-backend by design: the demotion runs in the shared
// phase-B chain, so one switch covers both. See DemotePointSizePass.
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
@@ -151,15 +174,38 @@ namespace MobileGL::MG_Config {
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool DisableUboRing = false;
// MOBILEGL_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
Bool EsprytDisableUboRing = false;
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
// glTexSubImage from the client pointer instead of staging them through the
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
// hatch).
Bool DisableUnpackRing = false;
Bool EsprytDisableUnpackRing = false;
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
// (glBufferSubData / map flushes) back to the immediate driver upload instead
// of queueing them for the staged-copy flush through the persistent-mapped
// upload ring (negative control / driver-bug escape hatch; the immediate
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
Bool EsprytDisableUploadRing = false;
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
// to the upload ring's staged glCopyBufferSubData (negative control / escape
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
// on Mali both the immediate glBufferSubData and a staged copy into a busy
// mutable store ghost the whole destination on the CPU.
Bool EsprytDisableInvalidateFlush = false;
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
@@ -180,10 +226,10 @@ namespace MobileGL::MG_Config {
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
Bool MagmaDisableRobustBufferAccess = false;
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
// unset picks the best supported tier.
@@ -224,7 +270,7 @@ namespace MobileGL::MG_Config {
// miscompiled shader: if a device ever renders differently with the cache
// on, one run with this falsy says so.
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// emulation - the builtin becomes a flat varying, the fragment stage gets a
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
@@ -236,7 +282,100 @@ namespace MobileGL::MG_Config {
// the pre-emulation path, extension passthrough where it exists and
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
// the negative control the emulation is measured against.
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
// packed16 allocations with a MIRRORED field order (allocation-scoped and
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
// reversed whenever exactly one endpoint sits in a mirrored allocation
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
// With no 16-bit packed ES image left there is no field order to disagree about; the
// client word still round-trips exactly, because the canonical shadow is already
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
// path); ForceOff keeps the native narrow storage even where the probe fires - the
// negative control that replays the corruption. Costs 2x the memory of the affected
// formats where it engages, which is why Auto is probe-gated rather than always-on.
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
// reroute for draws made while transform feedback is INACTIVE. The stream query
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
// tessellation tests per tree size a capture buffer from the 0 and die on the
// zero-length map. Auto defers to a device probe at renderer bring-up
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
// the same capture-less draws and arms the best proven one: the dedicated
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
// the reroute structurally wherever a pool can exist (the arming-observable lane,
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
// 0 - the 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;
#if MOBILEGL_PIPE_PUSH
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
// counts instead, for triage and for the lane that must survive to read its own
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
// off.
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
// green verify run goes red naming it (negative control A). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipeVerifyCorrupt;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
#endif
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
// alive so the first handle waves have a real old-versus-new arm to be compared
// against. ON by default for the whole migration window, deleted with the pull
// path itself.
Bool PipeLegacyMemos = true;
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
// holds a complete CPU shadow, so this cache buys latency, never correctness.
Uint32 PipeTexelRetainMb = 0;
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
// the server without shipping index bytes per draw. Over budget it degrades to
// per-draw staging, counted separately in the stats.
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
// needs its numbers at all sets this low enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
// the log whenever PipeStats is on, so a device run needs no writable path.
String PipeStatsFile;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+77 -15
View File
@@ -159,37 +159,74 @@ 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");
features.EnableGlesTextureView = QueryEnvFlag("MOBILEGL_ENABLE_GLES_TEXTURE_VIEW");
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.FixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.DisableUnpackRing = QueryEnvFlag("MOBILEGL_DISABLE_UNPACK_RING");
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
@@ -197,8 +234,33 @@ namespace MobileGL::MG_ConfigLoader {
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.ViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
#if MOBILEGL_PIPE_PUSH
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
#endif
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
// drop them.
features.PipeLegacyMemos =
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
}
inline void InitBackendType() {
+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();
+47 -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.
@@ -495,6 +522,24 @@ namespace MobileGL {
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
@@ -1198,8 +1198,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// Until that reconciliation exists, advertising here would be the same lie the comment
// above refuses to tell, just with an extra prerequisite met. Set
// MOBILEGL_ENABLE_GLES_TEXTURE_VIEW=1 to re-enable it for that work.
if (textureViewSupported && MG_Config::Features.EnableGlesTextureView) {
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
extensions.push_back(E_GL_ARB_texture_view);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
@@ -1255,8 +1255,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
@@ -1417,6 +1415,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
}
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
@@ -1479,6 +1484,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
// extension pairs the loader probed, independently, because they really do come
// separately. False arms the shared phase-B demotion
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
// never names the built-in in those stages and needs no extension.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
// exercised on a healthy driver (the pinned integration lane); =0 restores the
// detected answer's declines.
m_dynamicParameters.SupportsTessellationPointSize =
m_GLESCapabilities.TessellationPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_dynamicParameters.SupportsGeometryPointSize =
m_GLESCapabilities.GeometryPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
m_dynamicParameters.SupportsTessellationPointSize = false;
m_dynamicParameters.SupportsGeometryPointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the driver's extensions");
m_dynamicParameters.SupportsTessellationPointSize = true;
m_dynamicParameters.SupportsGeometryPointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
File diff suppressed because it is too large Load Diff
@@ -92,8 +92,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
File diff suppressed because it is too large Load Diff
+42 -5
View File
@@ -230,7 +230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
//
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
// Whether emulation runs. Off only under MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
// restores the pre-emulation path as a negative control.
Bool ViewportArrayEmulationEnabled();
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
@@ -461,6 +461,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the owning thread replaying them: guard both fields with pendingMutex.
Bool pendingRespecify = false;
VecRange1D pendingRanges;
// App bytes for an ADOPTED store, awaiting their GPU-ordered landing (ring
// stage + glCopyBufferSubData at the next sync; see
// BufferBackendOps::ResidentSubData). The frontend keeps such writes out of
// the coherent mapping - an in-place host write tears the in-flight frames
// still reading the old bytes. Guarded by pendingMutex like pendingRanges.
struct PendingResidentWrite {
SizeT offset = 0;
Vector<Uint8> bytes;
};
Vector<PendingResidentWrite> pendingResidentWrites;
std::mutex pendingMutex;
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
@@ -544,12 +554,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// BackendVertexArrayObject::SyncToBackend.
extern Uint64 g_bufferBackendIdGeneration;
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/
// sampler caches already do. Invalidated on MakeCurrent (context may reset).
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER / GL_TRANSFORM_FEEDBACK_BUFFER):
// skips the GL call when the (id, range) already at that index matches, like the
// array-buffer/texture/sampler caches already do. Invalidated on MakeCurrent
// (context may reset).
// Binds the transform feedback capture points [0, bufferCount) from the frontend
// state, and touches nothing else - in particular it never binds a zero the
// application did not ask for. See the definition for why that matters on Mali.
void SyncTransformFeedbackBindingPoints(SizeT bufferCount);
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
void InvalidateIndexedBufferBindingCache();
// The transform feedback capture points are per-transform-feedback-OBJECT state, so
// every glBindTransformFeedback swaps all of them under the shadow above. XfbImpl
// calls this on each bind/delete.
void InvalidateTransformFeedbackBindingShadows();
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
@@ -618,7 +637,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// which is what to watch if this ring ever shows up in an RSS regression: it
// grows on demand from 4 MiB and is capped, not unbounded.
//
// False when the feature is disabled (MOBILEGL_DISABLE_UNPACK_RING),
// False when the feature is disabled (MOBILEGL_ESPRYT_DISABLE_UNPACK_RING),
// EXT_buffer_storage / fences are missing, the ES context is not current, or
// ring creation already failed under this context. Callers then upload from
// the client pointer exactly as before.
@@ -633,6 +652,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Largest single staging request the ring can ever satisfy.
SizeT UnpackRingMaxBytes();
void UnpackRingOnPresent();
// --- Buffer upload ring ---------------------------------------------------
// The same persistent-mapped bump allocator, staging APP BUFFER UPDATES
// (glBufferSubData / non-persistent map flushes) whose destination store may
// still be referenced by in-flight GPU work. Mali resolves that WAR hazard by
// BLOCKING the calling glBufferSubData (osup_sync_object_wait) until every
// referencing job retires - Minecraft 26.3 rewrites its chunk-section and
// dynamic-transform UBOs and streams chunk meshes with per-frame SubData, and
// each such call serialized against the whole GPU queue (~1 fps while chunks
// stream in, and again on every camera pan). App SubData ranges are queued on
// the resource instead (the frontend shadow already holds the bytes) and
// draw-time sync drains them: bytes staged into this ring, then one
// glCopyBufferSubData per merged range - the copy is ordered on the GPU
// timeline, so the hazard costs no CPU wait. Reclamation contract identical
// to the other two rings. MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING restores the
// historical immediate-upload path (negative control / escape hatch).
void UploadRingOnPresent();
} // namespace BufferImpl
namespace VertexArrayImpl {
@@ -1624,6 +1660,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
const std::map<String, String>& inputBlockRenames,
const std::map<String, String>& outputBlockRenames,
Bool stripInputBlockLocations, Bool stripOutputBlockLocations,
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
String& outSource,
std::set<String>& outFlattenedXfbBlockNames,
+31 -11
View File
@@ -9,6 +9,8 @@
#include "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring>
#include <limits>
@@ -41,14 +43,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// verbatim is already "this batch restarts nowhere".
Uint32 RestartSentinelFor(GLenum type) {
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
return MG_State::pGLContext->GetPrimitiveRestartIndex();
return MGB_CTX->GetPrimitiveRestartIndex();
}
return MG_Util::FixedRestartIndexForGLType(type);
}
Bool RestartActive() {
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
}
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
@@ -83,7 +85,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
Uint BoundDrawIndirectBufferId() {
const auto& indirect =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!indirect) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
return resource ? resource->id : 0;
@@ -91,7 +93,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = MGB_CTX->GetBoundVertexArray();
if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject();
}
@@ -156,7 +158,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
// orphans the previous store, so the upload never waits on a dispatch still reading
// the old contents out of the same name.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
// statsClass: which MGPipe byte population these bytes belong to. Counted here
// rather than at the four call sites so a new tier cannot forget it.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass) {
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
@@ -169,6 +174,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
buffer.cursor = 0;
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
}
return true;
}
@@ -183,7 +191,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
constexpr SizeT kMinRingBytes = 1u << 16;
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
outOffset = 0;
if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false;
@@ -207,6 +216,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
}
buffer.cursor += aligned;
return true;
@@ -417,7 +429,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
SizeT commandBase = 0;
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
return false;
}
@@ -532,7 +545,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
}
SizeT indexBase = 0;
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
return false;
}
@@ -737,10 +751,16 @@ void main() {
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
if (!EnsureComputeProgram()) return;
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
return;
}
// data == nullptr: pure respecify, the compute pass writes the contents, so no
// host bytes cross here and nothing is counted.
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
return;
}
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
+84 -2
View File
@@ -11,10 +11,13 @@
#include "Managers.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
#include "MG_Util/SelfTest/DriverBugProbes.h"
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -125,6 +128,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
requestedInternalFormat,
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
}
// Outside the caveat branch on purpose: the driver CAN create the native narrow
// storage - the capability probes say so - it just cannot be trusted as a raw-copy
// endpoint. Texture and renderbuffer targets both come through here, which is what
// keeps a renderbuffer -> texture copy of these formats same-ES-format when the
// widening engages.
if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) {
options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm;
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
} // namespace
@@ -182,6 +193,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
return options;
}
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) {
switch (internalFormat) {
// TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG
// converter folds the two spellings onto one logical format.
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGB5A1:
case TextureInternalFormat::RGBA4:
break;
default:
return false;
}
switch (MG_Config::Features.EsprytWidenPacked16Storage) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
break;
}
// Behind the backend gate on purpose: the memoized probe latches its first answer
// for the whole process, and before the backend is up the GL function table may
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
// forever. Once the backend exists, the first narrow-format image this process
// creates runs the probe on a live context.
if (pActiveBackendObject == nullptr) {
return false;
}
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
}
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, TextureTarget target) {
#ifdef TRACY_ENABLE
@@ -713,6 +754,47 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation) {
using Tier = MG_External::GLESCapabilities::PointSizeTier;
switch (tier) {
case Tier::ExtensionEXT:
return tessellation ? "GL_EXT_tessellation_point_size" : "GL_EXT_geometry_point_size";
case Tier::ExtensionOES:
return tessellation ? "GL_OES_tessellation_point_size" : "GL_OES_geometry_point_size";
default:
return nullptr;
}
}
String RequestPointSizeExtension(String glslCode, const char* extensionName) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The gl_ViewportIndex story, one built-in over: ESSL 320 makes the tessellation and
// geometry STAGES core but leaves gl_PointSize out of their gl_PerVertex entirely,
// and SPIRV-Cross - which only ever sees a SPIR-V BuiltIn PointSize decoration -
// prints the identifier with no directive behind it. Same hard rule as the two
// neighbours: never emitted speculatively, because `#extension` on a name the driver
// does not advertise is a compile error of its own.
if (extensionName == nullptr || glslCode.find(extensionName) != String::npos) {
return glslCode;
}
const String directive = String("#extension ") + extensionName + " : require\n";
// Right after the #version line, the one position that must stay first;
// ForceSupporterOutput's scan for the LAST #extension directive still finds
// whichever one that ends up being.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return directive + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + directive;
}
glslCode.insert(lineEnd + 1, directive);
return glslCode;
}
String BakeImageFormatQualifiers(String glslCode,
const UnorderedMap<String, String>& esslFormatByUniformName) {
#ifdef TRACY_ENABLE
@@ -2213,11 +2295,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
+26 -1
View File
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
// Whether this format's ES storage is widened to 8-bit-per-channel because the
// driver stores some packed16 allocations with a mirrored field order
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
// is no longer 16-bit packed.
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown);
@@ -273,6 +282,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// error, so this is never emitted speculatively. A no-op when not needed or already
// present.
String RequestViewportArrayExtension(String glslCode, Bool needed);
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
// advertises neither spelling, because requesting an unadvertised extension is itself a
// compile error. A no-op when nullptr or already present.
String RequestPointSizeExtension(String glslCode, const char* extensionName);
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
// ones; the two extensions are separate and neither implies the other.
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
@@ -507,7 +532,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
// so their constant level stays constant; only the implicit-LOD forms take the bias.
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl
@@ -12,6 +12,7 @@
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -385,8 +386,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
@@ -624,7 +625,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
@@ -740,8 +741,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
@@ -785,8 +784,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
@@ -939,6 +938,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
// them. Not clamped: unlike the block counts these are not amounts an application
// allocates, and the frontend already raises them to the GL minimum.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
}
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks);
@@ -1081,6 +1089,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
// exactly as it always has.
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
// False arms the shared phase-B point-size demotion, whose modules then carry no
// TessellationPointSize/GeometryPointSize capability and build without the feature.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
// detected answer's declines.
{
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
supportsStagePointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the device feature");
supportsStagePointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
}
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
// exist in the module at all; a 64-bit vertex FETCH was already impossible
@@ -70,7 +70,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
+97 -170
View File
@@ -10,9 +10,11 @@
#include "DirectVulkanResourceState.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/ErrorState/ErrorInfo.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Miscellany/IndexGenerator.h"
#include <atomic>
@@ -77,7 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 blockBindingVersion = 0;
Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables;
GLint computeWorkGroupSize[3] = {1, 1, 1};
};
struct DrawElementsIndirectCommand {
@@ -208,16 +209,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
for (auto& module : modules) {
for (Uint32 entryIndex = 0; entryIndex < module.entry_point_count; ++entryIndex) {
const auto& entryPoint = module.entry_points[entryIndex];
if ((entryPoint.shader_stage & SPV_REFLECT_SHADER_STAGE_COMPUTE_BIT) == 0) {
continue;
}
cache.computeWorkGroupSize[0] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.x, 1));
cache.computeWorkGroupSize[1] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.y, 1));
cache.computeWorkGroupSize[2] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.z, 1));
}
uint32_t bindingCount = 0;
SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr);
if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) {
@@ -277,15 +268,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
if (!MGB_CTX->ValidateProgramName(program)) {
return nullptr;
}
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
auto& programObject = MGB_CTX->GetProgramObject(program);
return programObject.get();
}
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
@@ -344,64 +335,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfi called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfi called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfi called with null GL context");
pVulkanRenderer->ClearBufferfi(buffer, drawbuffer, depth, stencil);
}
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfv called with null GL context");
pVulkanRenderer->ClearBufferfv(buffer, drawbuffer, value);
}
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferuiv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferuiv called with null GL context");
pVulkanRenderer->ClearBufferuiv(buffer, drawbuffer, value);
}
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferiv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferiv called with null GL context");
pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value);
}
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
if (drawcount <= 0) {
return;
@@ -409,7 +400,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
return;
@@ -452,13 +443,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride);
}
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
if (maxdrawcount <= 0) {
return;
@@ -472,7 +463,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
@@ -503,7 +494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
DrawIndexedCmd payload{};
payload.mode = mode;
@@ -530,7 +521,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsIndirect called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsIndirect called with null GL context");
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
@@ -540,7 +531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, 1, 0);
return;
@@ -574,7 +565,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysInstancedBaseInstance called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
DrawCmd payload{};
payload.mode = mode;
@@ -589,11 +580,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysIndirect called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysIndirect called with null GL context");
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, 1, 0);
return;
@@ -623,13 +614,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexImage2D called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexImage2D called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, 0, 0, x, y, width, height);
}
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexSubImage2D called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
void CopyImageSubData(const CopyImageEndpoint& src,
@@ -638,32 +629,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth);
}
void GenerateMipmap(GLenum target) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GenerateMipmap called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GenerateMipmap called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GenerateMipmap called with null GL context");
pVulkanRenderer->GenerateMipmap(target);
}
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchCompute called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchCompute called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchCompute called with null GL context");
pVulkanRenderer->DispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
}
void DispatchComputeIndirect(GLintptr indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchComputeIndirect called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchComputeIndirect called with null GL context");
pVulkanRenderer->DispatchComputeIndirect(indirect);
}
void MemoryBarrier(GLbitfield barriers) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MemoryBarrier called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MemoryBarrier called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MemoryBarrier called with null GL context");
pVulkanRenderer->MemoryBarrier(barriers);
}
@@ -682,130 +673,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(void)format;
}
// The two compute limits are the only indexed pnames a backend genuinely owns: they come
// from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it
// can raise the answer to the GL required minimum. The same six numbers are carried in
// DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from
// the same limits), which is their MGPCaps carrier once this entry retires - the
// AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND
// state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit
// bindings, the viewport rectangles, the indexed capabilities) and is answered there before
// the table is consulted, so the arms this function used to carry for
// GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were unreachable duplicates - and not
// even faithful ones: the frontend reports the range glBindBufferRange was ASKED for,
// verbatim, while these clamped it to the buffer's current storage.
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
if (!data) return;
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer");
if (index >= 3) {
*data = 0;
return;
}
switch (target) {
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]);
return;
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]);
return;
case GL_SHADER_STORAGE_BUFFER_BINDING: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint>(end - start);
return;
}
case GL_IMAGE_BINDING_NAME:
case GL_IMAGE_BINDING_LEVEL:
case GL_IMAGE_BINDING_LAYERED:
case GL_IMAGE_BINDING_LAYER:
case GL_IMAGE_BINDING_ACCESS:
case GL_IMAGE_BINDING_FORMAT: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
if (target == GL_IMAGE_BINDING_NAME) {
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
} else if (target == GL_IMAGE_BINDING_LEVEL) {
*data = imageBinding.Level;
} else if (target == GL_IMAGE_BINDING_LAYERED) {
*data = imageBinding.Layered;
} else if (target == GL_IMAGE_BINDING_LAYER) {
*data = imageBinding.Layer;
} else if (target == GL_IMAGE_BINDING_ACCESS) {
*data = static_cast<GLint>(imageBinding.Access);
} else {
*data = static_cast<GLint>(imageBinding.Format);
}
return;
}
default:
*data = 0;
return;
}
}
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
if (!data) return;
switch (target) {
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint64>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint64>(end - start);
return;
}
default:
*data = 0;
return;
}
}
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
if (!params) return;
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) {
params[0] = 0;
return;
}
switch (pname) {
case GL_COMPUTE_WORK_GROUP_SIZE: {
auto& cache = GetProgramResourceCache(*programObject);
params[0] = cache.computeWorkGroupSize[0];
params[1] = cache.computeWorkGroupSize[1];
params[2] = cache.computeWorkGroupSize[2];
return;
}
default:
params[0] = 0;
return;
}
}
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || storageBlockName == nullptr) return;
@@ -813,7 +714,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
: 0;
if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) {
MG_State::pGLContext->RecordError(
MGB_CTX->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
return;
@@ -838,24 +739,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ReadPixels called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ReadPixels called with null GL context");
pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels);
}
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTexImage called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTexImage called with null GL context");
pVulkanRenderer->GetTexImage(target, level, format, type, pixels);
}
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTextureImage called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTextureImage called with null GL context");
pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels);
}
void Clear(GLbitfield mask) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::Clear called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::Clear called with null GL context");
pVulkanRenderer->Clear(mask);
}
@@ -883,7 +784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices);
@@ -914,7 +815,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) {
if (count < 2) {
@@ -939,7 +840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
@@ -962,7 +863,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArrays called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArrays called with null GL context");
if (drawcount <= 0) {
return;
}
@@ -1007,7 +908,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
// declined the whole batch and painted nothing.)
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) {
@@ -1068,13 +969,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElements called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
@@ -1098,14 +999,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitFramebuffer called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::BlitFramebuffer called with null GL context");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::BlitFramebuffer called with null GL context");
pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
}
@@ -1206,6 +1107,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
// draws, where the renderer's reroute is armed (the affected driver's
// stream query counts nothing without an open capture; see
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
// slots above, which keep the span's XFB-active draws.
Vector<Uint32> rerouteSlots;
// Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame
@@ -1215,11 +1122,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// stale queries are always safe to delete.
Uint64 rendererGeneration = 0;
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
// capture saw, so a draw made while the span was paused is invisible to it -
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
// emitted regardless. The delta closes that gap at result time.
// query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
// counts only what the capture saw, so a draw made while the span was paused is
// invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
// processing stage emitted regardless. The delta closes that gap at result time.
Uint64 pausedPrimitiveSnapshot = 0;
// ...unless the GPU already counted those paused draws when the span opened -
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
// draw with no open capture, paused ones included) or, where the probe measured
// the stream query as counting capture-less draws, through the stream slot the
// paused draw still takes. Adding the CPU delta on top would count them twice,
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
// points write it and it answers 0 for GL_PATCHES.
Bool pausedPrimitivesCountedByGpu = false;
};
} // namespace
@@ -1313,13 +1228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
*outNanoseconds = primitives;
@@ -1366,7 +1282,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot =
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0;
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
// disarms: the answer is then what this span will actually do for every draw.
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
return query;
}
@@ -1377,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
query->rerouteSlots);
}
BackendQueryHandle BeginOcclusionQuery() {
@@ -1411,5 +1331,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Present() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
pVulkanRenderer->Present();
// THE frame boundary for the MGPipe counters, at the backend entry point rather
// than inside VulkanRenderer::Present: that function has an early return for the
// no-usable-swapchain case, and a suspended frame is still a frame the counters
// must close.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -95,8 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
@@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
@@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -44,6 +44,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
// pSampleMask always did.
//
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
// single Uint32 here let such a pipeline read one word past the member (the next
// struct field). The second word is all-ones: full coverage for samples 32..63, which
// is the only honest answer when GL has no state describing them.
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
@@ -77,6 +77,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
// Where a gl_PerVertex built-in output lives, resolved from the module's annotations.
// Named for gl_Position because the clip-space fixup is what it was written for, and it
// is still the only shape that pass accepts - but the transform-feedback capture pass
// resolves gl_PointSize through the same struct, in which case `vectorTypeId` /
// `vectorPtrTypeId` hold the SCALAR float type and its Output pointer rather than a vec4.
struct PositionTargetInfo {
Uint32 variableId = 0;
Uint32 vectorTypeId = 0;
@@ -102,6 +107,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// gl_PointSize's counterpart to IsVec4Float32. The two are the only shapes any
// gl_PerVertex member this file resolves can have, and each resolver takes whichever
// one its built-in is declared with, so a mismatched type declines rather than
// producing a mirror the driver would reject.
Bool IsFloat32Scalar(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
auto* floatInst = context->get_def_use_mgr()->GetDef(typeId);
if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false;
if (floatInst->GetSingleWordInOperand(0) != 32) return false;
if (outFloatTypeId) *outFloatTypeId = typeId;
return true;
}
// Which of the two shapes above a resolver should accept. A plain function pointer
// rather than a std::function: every call site is one of the two free functions.
using BuiltInTypeCheckFn = Bool (*)(spvtools::opt::IRContext*, Uint32, Uint32*);
spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) {
if (variable.type_description == nullptr) {
return SPVC_BASETYPE_UNKNOWN;
@@ -381,9 +403,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return used;
}
void ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex) {
// What a failed validation says, for a caller that wants to put it in its own message.
struct SpirvValidationFailure {
String message;
Int result = 0;
SizeT index = 0;
};
// Returns whether the module validates. The result used to be discarded everywhere: the
// call was DEBUG-or-env gated and only logged, so an invalid module produced by a backend
// transform went straight to vkCreateShaderModule. That is not a survivable outcome on
// this hardware - Mali r54 SIGSEGVs building the pipeline instead of returning an error,
// the same "not a validating entry point" behaviour PipelineFactory already documents for
// vkCreateGraphicsPipelines - so the callers that feed the driver now act on it.
//
// This function does NOT log the failure at E any more. It used to, unlatched, on the
// stated grounds that "reaching here already requires the validation switch to be armed,
// which bounds the volume" - and that premise died when the two GetOrCreateProgram call
// sites became unconditional: MGLOG_E is live at the production INFO level, and Log.h's
// own rule is that anything at W or E on a repeatable path must be latched or demoted.
// The failure text now travels back through `outFailure` so the LATCHED call-site
// messages carry the VUID instead of an unlatched inner one repeating it; what stays here
// is the D-level detail and the process-wide counter the test lanes assert on.
Bool ValidateTransformedSpirv(const Vector<Uint>& spirv, ShaderStage shaderStage, Uint programExternalIndex,
SpirvValidationFailure* outFailure = nullptr) {
if (outFailure != nullptr) *outFailure = {};
if (spirv.empty()) {
return;
return true;
}
spv_const_binary_t binary = {spirv.data(), spirv.size()};
@@ -405,18 +451,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
if (result != SPV_SUCCESS) {
// MGLOG_E, unlatched: reaching here already requires the validation switch to
// be armed, which bounds the volume, and each VUID names a different defect.
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
// compiled out of every INFO build.) The latch is what a test harness asserts on.
const char* message =
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>";
const SizeT index = diagnostic != nullptr ? diagnostic->position.index : 0;
// The test-lane signal (ShaderCompiler.h documents harnesses snapshotting it and
// asserting on the delta). Bumped for every failed validation, including one a
// caller goes on to recover from: a transform that produced an invalid module is
// a real defect whether or not this run survived it.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
MGLOG_E(
if (outFailure != nullptr) {
*outFailure = {String(message), static_cast<Int>(result), index};
}
MGLOG_D(
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
static_cast<Int>(shaderStage),
programExternalIndex,
static_cast<Int>(result),
diagnostic != nullptr ? diagnostic->position.index : 0,
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
index,
message);
}
MOBILEGL_ASSERT(
result == SPV_SUCCESS,
@@ -432,6 +484,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
spvDiagnosticDestroy(diagnostic);
spvValidatorOptionsDestroy(options);
spvContextDestroy(context);
return result == SPV_SUCCESS;
}
void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable,
@@ -743,8 +796,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId,
PositionTargetInfo* outTarget) {
Bool ResolveDirectBuiltInTarget(spvtools::opt::IRContext* context, Uint32 variableId,
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
auto* varInst = context->get_def_use_mgr()->GetDef(variableId);
if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false;
if (varInst->GetSingleWordInOperand(0) != static_cast<Uint32>(spv::StorageClass::Output)) return false;
@@ -756,7 +809,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
PositionTargetInfo target{};
target.variableId = variableId;
target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1);
if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false;
if (!typeCheck(context, target.vectorTypeId, &target.floatTypeId)) return false;
target.vectorPtrTypeId = varInst->type_id();
target.isMember = false;
@@ -771,15 +824,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return context->get_type_mgr()->GetTypeInstruction(&ptrType);
}
Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
PositionTargetInfo* outTarget) {
Bool ResolveMemberBuiltInTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex,
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId);
if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false;
if (memberIndex >= structInst->NumInOperands()) return false;
const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex);
Uint32 floatTypeId = 0;
if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false;
if (!typeCheck(context, vectorTypeId, &floatTypeId)) return false;
const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId);
if (vectorPtrTypeId == 0) return false;
@@ -807,27 +860,130 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
// The OUTPUT variable (or gl_PerVertex member) carrying `builtIn`, if the module
// declares one of the expected type. Annotations are the search space deliberately:
// they survive the link-time sanitize chain's interface delisting, which is the whole
// reason EnsureEntryPointInterface exists.
Bool FindBuiltInTarget(spvtools::opt::IRContext* context, spv::BuiltIn builtIn,
BuiltInTypeCheckFn typeCheck, PositionTargetInfo* outTarget) {
Vector<Pair<Uint32, Uint32>> memberCandidates;
constexpr auto kDecorationBuiltIn = static_cast<Uint32>(spv::Decoration::BuiltIn);
constexpr auto kBuiltInPosition = static_cast<Uint32>(spv::BuiltIn::Position);
const auto wantedBuiltIn = static_cast<Uint32>(builtIn);
for (auto& inst : context->module()->annotations()) {
if (inst.opcode() == spv::Op::OpDecorate) {
if (inst.NumInOperands() < 3) continue;
if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue;
if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true;
if (inst.GetSingleWordInOperand(2) != wantedBuiltIn) continue;
if (ResolveDirectBuiltInTarget(context, inst.GetSingleWordInOperand(0), typeCheck, outTarget)) {
return true;
}
} else if (inst.opcode() == spv::Op::OpMemberDecorate) {
if (inst.NumInOperands() < 4) continue;
if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue;
if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue;
if (inst.GetSingleWordInOperand(3) != wantedBuiltIn) continue;
memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1));
}
}
for (const auto& [structTypeId, memberIndex] : memberCandidates) {
if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true;
if (ResolveMemberBuiltInTarget(context, structTypeId, memberIndex, typeCheck, outTarget)) return true;
}
return false;
}
Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) {
return FindBuiltInTarget(context, spv::BuiltIn::Position, IsVec4Float32, outTarget);
}
// Put `variableId` back on `entryPoint`'s interface list if it is not already there.
//
// SPIR-V requires every Input/Output global an entry point statically uses to be listed on
// its OpEntryPoint, and spirv-val enforces it ("Interface variable id <N> is used by entry
// point 'main' id <M>, but is not listed as an interface"). The link-time sanitize chain
// DELISTS a variable nothing referenced yet - ShaderCompiler::SanitizeAndOptimizeBinary
// runs CreateAggressiveDCEPass(false), which may never delete an Output, followed by
// CreateRemoveUnusedInterfaceVariablesPass, which rebuilds the operand list from the
// variables actually referenced. A TES that redeclares `out gl_PerVertex { vec4
// gl_Position; }` and never writes it therefore reaches the backend with the OpVariable
// and its BuiltIn Position decoration intact and its interface slot gone. Any pass that
// then injects a reference has to put the slot back, or it hands the driver a module no
// validator accepts - and Mali r54 answers that with a SIGSEGV inside pipeline creation
// rather than an error return.
//
// No SPIR-V version gate here, unlike GlFragCoordYFlipPass's identical call for its
// injected PRIVATE global: Input and Output belong on the interface in every version,
// and only 1.4 widened it to the other storage classes.
Bool EnsureEntryPointInterface(spvtools::opt::IRContext* context, spvtools::opt::Instruction& entryPoint,
Uint32 variableId) {
// In-operands: 0 = execution model, 1 = entry function id, 2 = name, 3.. = interface.
constexpr Uint32 kFirstInterfaceOperand = 3;
if (variableId == 0) return false;
for (Uint32 operand = kFirstInterfaceOperand; operand < entryPoint.NumInOperands(); ++operand) {
if (entryPoint.GetSingleWordInOperand(operand) == variableId) return false;
}
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
context->AnalyzeUses(&entryPoint);
return true;
}
// Is `pointerId` the position target itself, or an access chain rooted at it?
Bool PointerReachesPositionTarget(spvtools::opt::IRContext* context, Uint32 pointerId,
const PositionTargetInfo& target) {
auto* defUse = context->get_def_use_mgr();
for (Uint32 current = pointerId; current != 0;) {
if (current == target.variableId) return true;
const auto* inst = defUse->GetDef(current);
if (inst == nullptr) return false;
switch (inst->opcode()) {
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpPtrAccessChain:
case spv::Op::OpInBoundsPtrAccessChain:
case spv::Op::OpCopyObject:
current = inst->GetSingleWordInOperand(0);
break;
default:
return false;
}
}
return false;
}
// Does anything in the module write the position target?
//
// Deliberately conservative - it answers "assume yes" for every shape it cannot read
// exactly, because a false "no" would silently drop the clip-space fixup from a shader
// that does write gl_Position, while a false "yes" only reinstates the behaviour this
// pass has always had. Scans every function rather than just the entry point's: a shader
// that assigns gl_Position inside a helper is still a shader that writes it, and passing
// the pointer to a call is a write as far as this can tell.
Bool ModuleWritesPositionTarget(spvtools::opt::IRContext* context, const PositionTargetInfo& target) {
for (auto& function : *context->module()) {
for (auto& block : function) {
for (const auto& inst : block) {
switch (inst.opcode()) {
case spv::Op::OpStore:
case spv::Op::OpCopyMemory:
case spv::Op::OpCopyMemorySized:
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(0), target)) {
return true;
}
break;
case spv::Op::OpFunctionCall:
// In-operand 0 is the callee; the rest are arguments.
for (Uint32 argument = 1; argument < inst.NumInOperands(); ++argument) {
if (PointerReachesPositionTarget(context, inst.GetSingleWordInOperand(argument),
target)) {
return true;
}
}
break;
default:
break;
}
}
}
}
return false;
}
@@ -914,6 +1070,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange;
// Nothing to remap in a Position the shader never writes. Declining is not just
// an optimisation: the fixup is load-modify-store, so on an unwritten Position it
// converts "undefined, never written" into "written with whatever the load
// returned", and the store is a reference to a variable the link-time sanitize
// chain has already delisted from the entry-point interface. glslang emits the
// OpVariable for every DECLARED interface block, so a redeclared-but-unwritten
// `out gl_PerVertex` is a shape real shaders have.
if (!ModuleWritesPositionTarget(context(), target)) {
MGLOG_D("gl-to-vulkan-position-fix: the shader never writes gl_Position; leaving it alone");
return Status::SuccessWithoutChange;
}
auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId);
if (!floatType) return Status::SuccessWithoutChange;
@@ -945,6 +1113,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1));
if (!function) continue;
Bool modifiedThisEntryPoint = false;
for (auto& bb : *function) {
for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) {
auto* inst = &*instIter;
@@ -953,10 +1122,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn);
if (!needsFixup) continue;
modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
modifiedThisEntryPoint |=
InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap,
doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270);
}
}
// Per entry point, and only for one this pass actually injected into: the
// injected load/store is a static use of the position variable, so the
// variable has to be on THIS entry point's interface list.
if (modifiedThisEntryPoint) {
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
}
modified |= modifiedThisEntryPoint;
}
if (!modified) return Status::SuccessWithoutChange;
@@ -1235,6 +1412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool needsPositionMirror = false;
Uint32 positionBufferIndex = 0;
Uint32 positionOffset = 0;
// gl_PointSize is a gl_PerVertex MEMBER, never a variable of its own, so the
// debug-name lookup below can never resolve it - it used to fall through to
// "no SPIR-V variable named 'gl_PointSize'" and leave the frontend's reserved
// slot unwritten, or, when it was the only capture, leave the module with no
// Xfb execution mode at all and the whole span declined.
Bool needsPointSizeMirror = false;
Uint32 pointSizeBufferIndex = 0;
Uint32 pointSizeOffset = 0;
for (const auto& varying : m_varyings) {
if (varying.name == "gl_Position") {
needsPositionMirror = true;
@@ -1242,6 +1427,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
positionOffset = varying.offsetBytes;
continue;
}
if (varying.name == "gl_PointSize") {
// A demoted module (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) no longer ACCESSES the
// built-in member - the value lives in the carrier variable the demotion
// named - so the capture binds to the carrier directly. The mirror below
// must not run for it: reading the now-unwritten member would capture
// garbage, and the read itself is the capability access the demotion
// exists to remove. Detected off the module's own debug names, so a
// composite built from another program's stage answers for the module it
// actually contains.
const auto carrierIt = idsByName.find(
MG_Util::ShaderTranspiler::ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME);
if (carrierIt != idsByName.end()) {
decorateForXfb(carrierIt->second, varying.bufferIndex, varying.offsetBytes);
modified = true;
continue;
}
needsPointSizeMirror = true;
pointSizeBufferIndex = varying.bufferIndex;
pointSizeOffset = varying.offsetBytes;
continue;
}
if (varying.blockMemberIndex >= 0) {
// glslang names the block's instance variable and its struct type
// separately; an anonymous instance leaves only the type named, so
@@ -1306,8 +1513,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (needsPositionMirror) {
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
positionOffset, decorateForXfb);
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
spv::BuiltIn::Position, IsVec4Float32,
"gl_Position", positionBufferIndex, positionOffset,
decorateForXfb);
}
if (needsPointSizeMirror) {
modified |= MirrorPerVertexBuiltInForCapture(entryFunctionId, *entryPoint,
spv::BuiltIn::PointSize, IsFloat32Scalar,
"gl_PointSize", pointSizeBufferIndex,
pointSizeOffset, decorateForXfb);
}
if (!modified) return Status::SuccessWithoutChange;
@@ -1347,19 +1562,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return 0;
}
// gl_Position and gl_PointSize are captured the same way and differ only in which
// built-in is looked up and what type it has, so one injector serves both. Anything
// else in gl_PerVertex would need its own type check before it could be added here.
template <typename DecorateFn>
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
Bool MirrorPerVertexBuiltInForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
spv::BuiltIn builtIn, BuiltInTypeCheckFn typeCheck,
const char* glslName, Uint32 bufferIndex, Uint32 offsetBytes,
const DecorateFn& decorateForXfb) {
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
using namespace spvtools::opt;
PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) {
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
if (!FindBuiltInTarget(context(), builtIn, typeCheck, &target)) {
MGLOG_E("XfbCaptureDecoratePass: %s capture requested but no such output found", glslName);
return false;
}
if (!target.isMember) {
// Standalone gl_Position variable: decorate it directly.
// Standalone built-in variable: decorate it directly. It still has to be
// on the interface - a transform-feedback decoration on a variable the entry
// point does not list captures nothing, and the sanitize chain delists an
// unwritten one (see EnsureEntryPointInterface).
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
return true;
}
@@ -1413,6 +1637,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
injected = true;
}
}
// The mirror was listed on the entry point above, but the loop just added a READ
// of the SOURCE block through an access chain, and the interface rule covers
// reads exactly as it covers writes. A built-in capture on a shader whose
// block the sanitize chain delisted - a TES that redeclares `out gl_PerVertex`
// and never writes it, which is what the tessellation_control_to_tessellation_
// evaluation.gl_MaxPatchVertices_Position_PointSize bodies do - produced an
// invalid module here for the same reason the position fixup did.
if (injected) {
EnsureEntryPointInterface(context(), entryPoint, target.variableId);
}
return injected;
}
@@ -3236,11 +3470,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
auto& spirv = program.GetGeneratedSpirv();
if (program.PointSizeDemoted()) {
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that pins
// MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because every
// rendering assertion above it stays green on a healthy driver whether the
// demotion ran or was silently disarmed. See PointSizeDemotionScenario.
MGLOG_I_ONCE("DirectVulkan is building programs whose tessellation/geometry gl_PointSize was "
"demoted to an ordinary varying, because this device cannot host the built-in "
"in those stages.");
}
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
if (enableSpirvValidation) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
}
// Unconditional now: the two ValidateTransformedSpirv calls below run in every build,
// not only when the switch is armed, so the validator's static tables have to be pinned
// against process exit in every build too.
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
const ShaderStage fixupStage = PickClipFixupStage(stages);
@@ -3264,6 +3508,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
// These two passes INJECT references - a store for the clip fixup, an access
// chain and a load for the gl_Position capture mirror - and a reference to a
// variable the link-time sanitize chain delisted from the entry-point interface
// is invalid SPIR-V that Mali r54 turns into a SIGSEGV inside pipeline creation
// rather than an error return. EnsureEntryPointInterface keeps them honest; this
// is the backstop.
//
// The fallback UNWINDS ONE PASS AT A TIME, which matters because the two passes
// are not equally optional. Rewinding straight to `spv` would also throw away the
// XfbBuffer/XfbStride/Offset decorations, the TransformFeedback capability and the
// Xfb execution mode - while the renderer decides to call
// vkCmdBeginTransformFeedbackEXT purely from GL state and never looks at the
// module. That ships a pipeline whose last pre-rasterization stage has no Xfb mode
// into a transform-feedback span, violating
// VUID-vkCmdBeginTransformFeedbackEXT-None-04128 on exactly the driver class this
// guard exists for. So: try the post-XFB, pre-clip-fixup module first, which keeps
// capture working and costs only the clip-space remap.
//
// Once per program on a cache miss, and only for the single stage that carries the
// fixups - not per draw and not per module.
SpirvValidationFailure fixupFailure{};
if (!ValidateTransformedSpirv(moduleSpirvs[i], stages[i], program.GetExternalIndex(),
&fixupFailure)) {
SpirvValidationFailure xfbFailure{};
if (fixupInput != &spv &&
ValidateTransformedSpirv(*fixupInput, stages[i], program.GetExternalIndex(), &xfbFailure)) {
MGLOG_E_ONCE("ProgramFactory: the clip fixup produced an invalid module for program %u "
"stage %d (%s); keeping the capture-decorated one, so this program draws "
"without the clip-space remap",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
fixupFailure.message.c_str());
moduleSpirvs[i] = *fixupInput;
} else {
MGLOG_E_ONCE("ProgramFactory: the clip/XFB fixups produced an invalid module for program %u "
"stage %d (%s); keeping the untransformed one",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
fixupFailure.message.c_str());
moduleSpirvs[i] = spv;
}
}
} else {
moduleSpirvs[i] = spv;
}
@@ -3472,15 +3756,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
#else
// Final module the driver receives; also checked in the INFO-level CI/test
// lanes, where the DEBUG gate above is compiled out.
if (enableSpirvValidation) {
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
// Last look at the exact bytes the driver receives, in EVERY build rather than only
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
// by here the descriptor bindings have been remapped and the layout about to be
// reflected describes the remapped module, so there is no module left that is both
// valid and consistent with it to fall back to. The recovery lives one step earlier,
// at the clip/XFB fixups (see the revert there) - which is where a transform can
// introduce a reference to a delisted interface variable, the failure this whole
// guard exists for. Anything that reaches this line names itself in the log of a
// shipping build instead of dying anonymously inside the driver.
SpirvValidationFailure finalFailure{};
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
"a backend transform after the clip/XFB fixups broke it (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
finalFailure.message.c_str());
}
// Does the stage the driver will treat as the last pre-rasterization one actually
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
// varying and changed nothing, anything later that might strip it. The renderer picks
// its capture commands from GL state alone and would otherwise open a span against a
// pipeline that cannot feed it.
if (stages[i] == fixupStage && (flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
program.GetTransformFeedbackVaryingCount() > 0 &&
!MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTransformFeedback(moduleSpv)) {
MGLOG_E_ONCE("ProgramFactory: program %u was built as a transform-feedback capture variant but its "
"stage %d carries no Xfb execution mode; its capture spans will be declined rather "
"than recorded against a pipeline that cannot feed them",
program.GetExternalIndex(), static_cast<Int>(stages[i]));
entry.xfbCaptureDeclined = true;
}
// Does this stage need a device feature the device did not give us? Asked ONLY when
// the feature is off, so a device that has it - the common case - pays nothing: the
// whole test is short-circuited before the module is parsed.
//
// gl_PointSize is an ordinary per-vertex output in desktop GL and any
// vertex-processing stage may write it, but Vulkan puts the built-in behind
// shaderTessellationAndGeometryPointSize in the tessellation and geometry stages
// (VUID-RuntimeSpirv-PointSize-06439). glslang emits TessellationPointSize /
// GeometryPointSize from the application's own access, so this program is legal GL
// that this device cannot run - the same shape the DirectGLES arm reports when a
// driver advertises neither EXT nor OES point-size extension, and it deserves the
// same named message rather than a pipeline the driver may fault on.
if (!m_tessellationAndGeometryPointSizeEnabled &&
(stages[i] == ShaderStage::TessControl || stages[i] == ShaderStage::TessEval ||
stages[i] == ShaderStage::Geometry) &&
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(
moduleSpv)) {
MGLOG_E_ONCE("ProgramFactory: program %u stage %d accesses gl_PointSize, but this device does not "
"support shaderTessellationAndGeometryPointSize; its draws are refused rather than "
"built into a pipeline the driver may fault on. Point size from a non-vertex stage "
"is not available on this device.",
program.GetExternalIndex(), static_cast<Int>(stages[i]));
entry.pointSizeCapabilityUnsupported = true;
}
#endif
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = moduleSpv.size() * sizeof(Uint);
@@ -3758,11 +4090,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
// links the program at both 430 and 460.
//
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
// feature, which this renderer does not enable, so a program whose evaluation stage reads
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
// this trades for not making every tessellated pipeline depend on an optional feature.
// Only gl_Position is written, and gl_PointSize is declared without being forwarded. That
// is a KNOWN GAP, not a design: GL 4.6 core 11.2.2 says the fixed-function pass-through
// hands the input patch to the evaluation stage unmodified, so an evaluation stage
// reading gl_in[].gl_PointSize should see the vertex stage's value and instead sees
// whatever this stage left in gl_out[] - which is nothing. A capture of it (the mirror in
// XfbCaptureDecoratePass) faithfully records that nothing.
//
// The reason this comment used to give - "the renderer does not enable
// shaderTessellationAndGeometryPointSize" - stopped being true when
// VulkanRenderer::CreateLogicalDeviceAndQueues started taking the feature wherever the
// device advertises it. Closing the gap is therefore possible now, but it is not free:
// the forwarding store has to be gated on that feature, because on a device without it
// the store is exactly the invalid usage the build-time refusal
// (VkProgramObject::pointSizeCapabilityUnsupported) exists to keep away from the driver -
// and this synthesized stage is not the application's, so refusing the program because
// MobileGL's own pass-through named a built-in would be the wrong trade. Nothing pins
// the shape either: every case in TessellationXfbCaptureScenario builds an explicit
// control stage, so a TES-without-TCS test has to come with the fix.
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
@@ -3862,14 +4207,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Vector<Uint>& spirv = binary.value().front();
{
// Still switch-gated, unlike the two in GetOrCreateProgram: this stage is synthesized
// by MobileGL from a fixed template rather than transformed from application SPIR-V,
// so a failure here is a MobileGL bug to catch in a validating lane, not something a
// shipping build can be handed by an application. The message is latched all the same
// - the pass-through cache is keyed on patchVertices, so a broken template would
// otherwise re-report once per distinct patch size.
Bool validateThisOne = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
validateThisOne = true;
#else
if (m_enableSpirvValidation) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
}
validateThisOne = m_enableSpirvValidation;
if (validateThisOne) MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
#endif
SpirvValidationFailure passthroughFailure{};
if (validateThisOne &&
!ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0, &passthroughFailure)) {
MGLOG_E_ONCE("ProgramFactory: the synthesized pass-through tessellation control stage for "
"patchVertices=%u does not validate (%s)",
patchVertices, passthroughFailure.message.c_str());
}
}
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smci.codeSize = spirv.size() * sizeof(Uint);
@@ -202,6 +202,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
// True when this program was built AS a transform-feedback capture variant but its
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
// renderer must decline the capture span instead of issuing
// vkCmdBeginTransformFeedbackEXT against it
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
//
// Two ways to get here, and neither is visible from GL state, which is all
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
// requested varyings and returned without changing anything (its own MGLOG_E path)
// while its runner still reported success. Both used to ship a non-Xfb module under
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
// key, so it was sticky for every later captured draw of the program, not a glitch.
Bool xfbCaptureDeclined = false;
// The program has a tessellation or geometry module declaring TessellationPointSize /
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
// is off, so a pipeline built from it is invalid usage
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
// implements one level up, and the counterpart of the DirectGLES arm that reports a
// driver with neither point-size extension by name.
//
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
@@ -426,12 +452,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
Bool shaderDrawParametersEnabled,
Bool unformattedFloatStorageImagesEnabled,
Bool tessellationAndGeometryPointSizeEnabled,
Bool enableSpirvValidation,
UpdateAfterBindLimits updateAfterBindLimits,
SubgroupLoweringPolicy subgroupPolicy)
: m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) {
@@ -591,6 +619,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
// GeometryPointSize is refused at build time (see VkProgramObject::
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
Bool m_tessellationAndGeometryPointSizeEnabled = false;
// Startup snapshot used only by internally synthesized shader modules, which do not
// originate from a ProgramLinkTask.
Bool m_enableSpirvValidation = false;
@@ -10,6 +10,7 @@
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/TextureState/TextureObject1D.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -20,6 +21,7 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
@@ -37,6 +39,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the
// id only has to stay clear of the application's, exactly like the sampled fallback's.
constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u;
// The multisample sampled fallbacks: one per (target, numeric domain), because unlike the
// single-sampled fallback they cannot be reinterpreted into another domain at view time
// (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the
// same reason as the two above - they must not collide with anything glGenTextures can
// hand out.
constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u;
constexpr Uint kFallbackMultisampleExternalIndexCount = 6u;
// MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can
// do reaches one, so anything keyed on the GL object an application bound - image-unit
// aliasing above all - has to leave them alone.
Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) return false;
const Uint index = static_cast<Uint>(texture->GetExternalIndex());
return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex ||
(index >= kFallbackMultisampleExternalIndexBase &&
index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount);
}
// The R32 member of each numeric class. Every one of the three is a MANDATORY-support
// format for uniform texel buffers, storage texel buffers and storage images alike
@@ -360,6 +380,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_textureManager = nullptr;
m_samplerManager = nullptr;
m_fallbackTexture2D.reset();
m_fallbackMultisampleTextures.clear();
}
void UniformManager::BeginFrame(Uint32 frameIndex) {
@@ -483,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// alive through the draw via GL binding state. Only the fallback path needs a SharedPtr to
// keep the fallback texture alive for the rest of this call.
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const auto& samplerOverride = textureUnit.GetSamplerObject();
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
@@ -496,7 +517,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texture = nullptr;
}
if (texture == nullptr) {
fallbackHolder = GetFallbackTexture(preferredTarget);
// The binding's sampler class, read here rather than through the `numericDomain`
// local further down (it is declared after this point): the multisample placeholder
// has to be built in the class the shader will read it in.
fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]);
texture = fallbackHolder.get();
if (texture == nullptr) {
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
@@ -529,7 +553,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (!IsValidSampledImageLayout(resource->layout)) {
auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
FramebufferAttachmentType attachmentType = FramebufferAttachmentType::None;
Int attachmentLevel = 0;
if (drawFbo &&
@@ -756,7 +780,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// filtering - which a single-level view can still have. Resolve the sampler exactly
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
const auto* effectiveSampler =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (effectiveSampler == nullptr) return false;
@@ -786,7 +810,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTexture: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTexture: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveSamplerTexture: sampler location binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
@@ -795,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
// The slot always holds at least the target's default texture (name 0). While that
@@ -811,7 +835,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element) {
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSamplerTextureRaw: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSamplerTextureRaw: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveSamplerTextureRaw: sampler location binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
@@ -821,7 +845,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element);
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
// touching the refcount (no atomic inc/dec per binding per draw).
@@ -966,7 +990,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferView& outBufferView) {
outBufferView = VK_NULL_HANDLE;
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageTexelBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageTexelBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(frameIndex < m_frames.size(),
"ResolveStorageTexelBufferDescriptor: frame index out of range");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
@@ -990,7 +1014,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
"ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding);
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
const auto& texture = imageBinding.Texture;
if (texture == nullptr) {
// An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero
@@ -1126,7 +1150,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorBufferInfo& outBufferInfo) const {
outBufferInfo = {};
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageBufferDescriptor: GL context is null");
MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(),
"ResolveStorageBufferDescriptor: binding %u out of range", binding);
@@ -1163,12 +1187,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
? static_cast<GLuint>(atomicCounterBinding)
: GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
const Uint32 bindingPointCount =
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(bufferTarget));
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(bufferTarget));
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
"ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'",
frontendBinding, blockName.c_str());
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, frontendBinding);
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(bufferTarget, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
if (bufferObject == nullptr) {
// NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a
@@ -1240,7 +1264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorImageInfo& outImageInfo) const {
outImageInfo = {};
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null");
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveStorageImageDescriptor: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveStorageImageDescriptor: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveStorageImageDescriptor: binding %u out of range", binding);
@@ -1269,7 +1293,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
auto& imageBinding = MGB_CTX->GetImageTextureBinding(imageUnit);
if (imageBinding.Texture == nullptr) {
// Legal GL: an image unit with no texture bound makes loads return zero and discards
// stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning
@@ -1367,18 +1391,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.imageView != VK_NULL_HANDLE;
}
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
// Report that there is no fallback and let the caller decline the draw - aborting the
// process over an unbound sampler is never the right answer.
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const {
// A multisample sampler cannot be served by the single-sampled 2D image below - its
// descriptor demands a multisample view - so it gets its own placeholder rather than no
// placeholder at all. Without one, ResolveSamplerDescriptor declined and
// BindProgramUniformBuffers dropped the WHOLE draw, which is how every
// sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a
// sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture
// unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an
// incomplete texture is undefined, not fatal, so the draw has to happen.
if (target == TextureTarget::Texture2DMultisample ||
target == TextureTarget::Texture2DMultisampleArray) {
return GetFallbackMultisampleTexture(target, numericDomain);
}
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
static_cast<Int>(target));
return nullptr;
}
// The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its
// image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can
// hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample
// image can never carry that bit, which is why the arm above needs one object per domain.
if (m_fallbackTexture2D == nullptr) {
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
@@ -1396,6 +1432,83 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return m_fallbackTexture2D;
}
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const {
// ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback.
//
// A descriptor whose image format is in a different numeric class than the sampler that
// reads it needs a format-reinterpreting view, and building one needs
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it:
// SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and
// the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8
// placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs
// mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped -
// the exact outcome the placeholder exists to prevent, just reached later. Matching the
// image's own format to the sampler's class instead means no reinterpreting view is
// needed at all.
const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray;
TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8;
Uint32 domainSlot = 0;
switch (numericDomain) {
case SamplerNumericDomain::SignedInteger:
internalFormat = TextureInternalFormat::RGBA8I;
domainSlot = 1;
break;
case SamplerNumericDomain::UnsignedInteger:
internalFormat = TextureInternalFormat::RGBA8UI;
domainSlot = 2;
break;
case SamplerNumericDomain::Float:
case SamplerNumericDomain::Unknown:
default:
// Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default:
// a shader whose sampler class could not be reflected is far likelier to be a plain
// sampler2DMS than an integer one, and a float view is the only one buildable without
// the mutable bit anyway.
break;
}
const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot;
auto cached = m_fallbackMultisampleTextures.find(key);
if (cached != m_fallbackMultisampleTextures.end()) {
return cached->second;
}
const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray
: TextureUploadTarget::Texture2DMultisample;
const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key;
SharedPtr<MG_State::GLState::TextureObjectMipmap> texture;
if (arrayed) {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisampleArray>(externalIndex);
} else {
texture = MakeShared<MG_State::GLState::TextureObject2DMultisample>(externalIndex);
}
texture->SetInternalFormat(internalFormat);
// TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with
// MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard
// VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder
// that re-created it would be worse than none.
texture->SetSamples(2);
texture->SetFixedSampleLocations(true);
// No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be
// written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike
// the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined,
// which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample
// texture that is not complete. The point of the placeholder is that the DRAW happens.
texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0});
texture->TruncateMipmapLevels(uploadTarget, 1);
texture->MarkStorageDirty(uploadTarget, 0, false);
// Worth knowing if it ever fires: an integer multisample format can legitimately support
// no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be
// VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this
// placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2)
// above exists to avoid. It already warns from there; nothing better is available - a
// one-sample integer image is still a draw, and declining is the outcome this whole
// placeholder replaced.
MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d",
static_cast<Int>(target), static_cast<Int>(numericDomain), static_cast<Int>(internalFormat));
return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second;
}
VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat,
SamplerNumericDomain numericDomain, Bool storage) {
MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null");
@@ -1536,7 +1649,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
// texture and the sampler override - this runs per binding per full-path draw,
// and program-alternating draw streams take the full path on every draw.
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSampledBinding: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveSampledBinding: GL context is null");
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
"ResolveSampledBinding: sampler location binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
@@ -1547,29 +1660,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
MG_State::GLState::ITextureObject* texture =
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
// The sampler in effect, resolved BEFORE the completeness test below rather than after:
// GL's completeness rules are a property of (texture, sampler in effect), so the test
// cannot be asked without it.
const auto& samplerOverride = textureUnit.GetSamplerObject();
const MG_State::GLState::SamplerObject* effectiveSampler =
samplerOverride ? samplerOverride.get()
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
// like ResolveSamplerTextureRaw reports it.
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
texture = nullptr;
}
// ...and so does a texture that fails the completeness rules for the filter in effect,
// because that is precisely what ResolveSamplerDescriptor does with it. The two used to
// disagree: this one asked only whether the default texture was UNDEFINED, so a default
// texture that had been given a base level but no mip chain - which is what the GL-CTS
// state reset between test cases leaves behind, and what any application that uploads to
// texture 0 has - stayed in the sampled set while the descriptor path swapped it for the
// fallback. SetupDraw then synced a texture no descriptor would use, the sync declined
// (GL calls it incomplete), and the null it returned was dereferenced one line later.
// Keeping the two predicates identical is the invariant; CollectSampledTextures exists to
// pre-sync exactly the textures the descriptors will hold.
if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) {
texture = nullptr;
}
if (texture == nullptr) {
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
// include it in the sampled set so the pre-render-pass sync/transition pass covers
// its first use instead of leaving that work to happen inside an active pass.
if (preferredTarget != TextureTarget::Texture2D &&
preferredTarget != TextureTarget::TextureRectangle) {
// Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew
// a multisample arm and the two must not drift apart.
texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get();
if (texture == nullptr) {
return false;
}
texture = GetFallbackTexture(preferredTarget).get();
// The substitution changed the texture, so the "no override" arm of the effective
// sampler has to follow it to the fallback's own.
if (!samplerOverride) {
effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr;
}
}
const auto& samplerOverride = textureUnit.GetSamplerObject();
outTexture = texture;
outSampler = samplerOverride ? samplerOverride.get()
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
outSampler = effectiveSampler;
return true;
}
@@ -1667,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
outTextures.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
MOBILEGL_ASSERT(MGB_CTX_LIVE,
"CollectStorageImageTextures: GL context is null");
// Same as the sampled walk: a declined program is refused at bind time, and its declined
// binding has no uniform location to reach an image unit through.
@@ -1711,7 +1848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
auto* texture = MGB_CTX->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
// ResolveStorageImageDescriptor will substitute the placeholder image for this
// binding; include it here for the same reason the sampled walk includes the
@@ -1749,7 +1886,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const {
outBindings.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
MOBILEGL_ASSERT(MGB_CTX_LIVE,
"CollectSamplerImageFeedback: GL context is null");
if (programObj.declinedDescriptors) return true;
@@ -1765,9 +1902,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
sampledTexture, sampledSampler) ||
sampledTexture == nullptr || sampledSampler == nullptr ||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
IsPlaceholderTexture(sampledTexture)) {
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
// alias the image-unit binding of the original texture.
// alias the image-unit binding of the original texture. The unbound and
// incomplete cases both arrive here AS that fallback now that
// ResolveSampledBinding applies the completeness rule itself, so the test is
// "is this one of ours" rather than a second completeness check.
continue;
}
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
@@ -1796,7 +1936,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
return false;
}
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
const auto& image = MGB_CTX->GetImageTextureBinding(imageUnit);
// A sampler view exposes all layers of its target; equal texture plus an
// overlapping mip therefore aliases the writable image subresource.
if (image.Texture.get() == sampledTexture &&
@@ -1826,7 +1966,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const void* outData = nullptr;
VkDeviceSize outSize = 0;
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveUniformBufferPayload: GL context is null");
MOBILEGL_ASSERT(MGB_CTX_LIVE, "ResolveUniformBufferPayload: GL context is null");
MOBILEGL_ASSERT(binding < programObj.bindingKinds.size(),
"ResolveUniformBufferPayload: binding %u out of range", binding);
MOBILEGL_ASSERT(programObj.bindingKinds[binding] == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic,
@@ -1871,12 +2011,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast<Uint32>(blockIndex));
const Uint32 uniformBindingPointCount =
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform));
static_cast<Uint32>(MGB_CTX->GetBufferBindingPointCount(BufferTarget::Uniform));
MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount,
"ResolveUniformBufferPayload: frontend UBO binding %u out of range for block '%s'",
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
MOBILEGL_ASSERT(bufferObject != nullptr,
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
@@ -1938,6 +2078,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
out.dynamicOffset = rangeStart;
}
}
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
// payload a split build would have to ship with set_shader_buffers. Espryt binds
// the frontend buffer to the driver and contributes nothing here, which is why
// the class is named for the payload and not for the call. Counted AFTER the
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
// here reported a copy that never happened.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
static_cast<Uint64>(outSize));
}
return true;
}
@@ -2145,6 +2295,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outBuffer = slice.buffer;
outRange = ubo.payloadSize;
outDynamicOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo && MG_Util::PipeStats::Enabled()) {
// Magma's half of stage-ubo-global, so the class means the same on both
// backends. The memo hit above returns before this, so a frame that reuses the
// slice correctly contributes nothing.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
static_cast<Uint64>(ubo.payloadSize));
}
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] =
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
@@ -179,7 +179,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
// class outright rather than being reinterpreted at view time.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
@@ -293,6 +303,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
@@ -130,7 +130,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// stale memo.
//
// Drawn from a process-wide source, never a per-instance counter: the VAO
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
// is destroyed and recreated on EGL surface release/re-create), so a fresh
// factory restarting at a dead factory's epoch value would honor its
// dangling entry pointers. The constructor takes a value strictly greater
@@ -10,6 +10,8 @@
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
#include "MG_Util/Metrics/PipeStats.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
@@ -229,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
(void)kind;
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
// absent: its bytes are counted by the caller, which is the only place that
// knows whether the payload is the default block (stage-ubo-global) or a named
// one repacked into the ring (stage-ubo-named), and counting here as well would
// double every uniform byte.
switch (kind) {
case BufferKind::Vertex:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Index:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Indirect:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
static_cast<Uint64>(size));
break;
case BufferKind::TextureBuffer:
case BufferKind::ShaderStorage:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
break;
case BufferKind::Uniform:
break;
}
}
return true;
}
Bool VkBufferManager::InitializeTransientArenas() {
@@ -339,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.pendingFullUpload = true;
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource.pendingFullUpload = false;
return true;
}
@@ -353,6 +388,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), 16, staging)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
// half of the same bytes and is not counted twice.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
if (commandBuffer == VK_NULL_HANDLE) {
return false;
@@ -422,6 +462,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
}
@@ -447,6 +489,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
return;
}
@@ -484,6 +529,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
return;
}
@@ -554,6 +602,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint8* seed = bufferObject.MappedData();
if (seed != nullptr) {
resource->buffer.Upload(seed, size, 0);
if (MG_Util::PipeStats::Enabled()) {
// The one-time seed of a persistent map. Everything the app writes AFTER
// this goes straight through the mapping and is persistent-map-push
// territory (unwired, D4/D-B4), not this class.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
}
resource->persistentMapped = true;
resource->pendingFullUpload = false;
@@ -602,6 +657,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->usageFlags = 0;
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource->pendingFullUpload = false;
}
@@ -681,6 +740,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outSlice)) {
return false;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource->transientSlice = outSlice;
resource->transientFrameSerial = m_frameSerial;
resource->transientChangeSerial = changeSerial;
@@ -13,6 +13,7 @@
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -54,7 +55,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo.
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
@@ -13,6 +13,7 @@
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <MG_Pipe/PipeInputsSwitch.h>
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
@@ -610,7 +611,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
auto& drawBuffers = fbo.GetDrawBuffers();
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
@@ -962,7 +963,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageLayout trackedRbLayout = rbResource->layout;
const Bool rbFramebufferSrgb =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat rbAttachmentFormat =
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
rbDesc.flags = 0;
@@ -1108,7 +1109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
textureResources.emplace_back(textureResource);
desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format,
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
attachmentSampleCount = textureResource->sampleCount;
trackedColorLayout = textureResource->layout;
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
@@ -11,8 +11,10 @@
#include "ProgramFactory.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include <Config.h>
#include <algorithm>
@@ -805,7 +807,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
const auto& liveTexture = MGB_CTX->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
aliveTexture = liveTexture;
} else {
@@ -948,7 +950,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat baseAttachmentFormat =
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
const VkFormat attachmentFormat =
@@ -3150,6 +3152,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
packBox(dst, item.regionLo, item.regionSize);
}
if (MG_Util::PipeStats::Enabled()) {
// Same shape split as Espryt's: one union box per item, or one job per rect of
// a refined rect list. The box/rect decision is invisible to SSIM and is what
// the +6 ms/frame Mali cliff of section 7.3 was, so it is counted apart from
// the bytes.
Uint64 boxEmissions = 0;
Uint64 rectEmissions = 0;
Uint64 jobs = 0;
for (const auto& item : uploadItems) {
if (item.rects.empty()) {
++boxEmissions;
jobs += isCombinedDepthStencil ? 2u : 1u;
} else {
++rectEmissions;
jobs += static_cast<Uint64>(item.rects.size());
}
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
static_cast<Uint64>(stagingSize));
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions,
static_cast<Uint64>(uploadItems.size()));
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, boxEmissions);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadRectEmissions, rectEmissions);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, jobs);
}
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags uploadSrcAccessMask = 0;
File diff suppressed because it is too large Load Diff
@@ -24,6 +24,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
#include <vk_mem_alloc.h>
#include "../VkIncludes.h"
@@ -563,7 +564,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Native subgroup topology, queried at device creation for the compute-module
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
// stage flag; 0 / false when the device has no usable compute subgroups or
// MOBILEGL_DISABLE_SUBGROUP forced them off.
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
Uint32 m_nativeSubgroupSize = 0;
Bool m_nativeSubgroupSupported = false;
Bool m_computeFullSubgroupsFeatureEnabled = false;
@@ -584,6 +585,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
// and capturable by name through transform feedback). Cached at device creation and
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
// that is invalid usage.
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
@@ -666,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{};
@@ -709,15 +728,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0;
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
// finds that defect with a working control - or
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
// GENERATED count through this pool instead, whose type the arming picks:
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
// query with its rasterizer-discard feature (exact semantics by definition -
// the extension exists because GL needs this count without a capture), else a
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
// per primitive reaching primitive clipping - after every vertex processing
// stage, before rasterizer discard - which is the same set).
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
// every draw with no open capture - a PAUSED span's draws included - takes a
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
// GL query span may therefore hold slots of both pools.
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
// VK_EXT_primitives_generated_query: base feature, and the
// ...WithRasterizerDiscard feature without which a discarding draw inside the
// query is invalid usage (so the reroute never picks the dedicated pool on a
// base-only device - GL applications toggle discard freely).
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
// tessellationShader was enabled at device creation (it is taken whenever the
// device advertises it); gates the probe's PATCHES shape.
Bool m_tessellationShaderFeatureEnabled = false;
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
MG_Util::SelfTest::PrimGenRerouteKind::None;
// The bring-up probe measured this device's stream query as counting draws made
// with no capture span open (the StreamCounts verdict) - so it counts the
// PAUSED-span ones too, through the stream slot they take when nothing is
// rerouted. Only the probe can know this, so it stays false wherever the probe
// is not consulted (the forced arms), which keeps those lanes' accounting as it
// was.
Bool m_primGenStreamCountsXfbInactiveDraws = false;
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
Uint32 m_primGenRerouteSlotCursor = 0;
Vector<Uint32> m_primGenRerouteActiveSlots;
Bool m_primGenRerouteSlotOpen = false;
Uint32 m_primGenRerouteOpenSlot = 0;
// Runs the bring-up probe (memoized per process) and decides
// m_primGenRerouteKind. Called at the end of device creation: it records on
// m_graphicsQueue, which nothing else is using yet.
void ArmPrimGenReroute();
public:
// Whether a GENERATED span opened now will have the draws made while the GL
// span is PAUSED counted on the GPU - through the reroute pool, which takes
// every draw with no open capture, or (where the reroute is not armed because
// the stream query was measured to count capture-less draws) through the stream
// slot such a draw still takes. The frontend's CPU paused-primitive counter
// must not be added on top of either: it would double count, and it cannot
// price the draws that matter anyway - only 3 of the ~15 draw entry points
// write it and it answers 0 for GL_PATCHES. Read once per span, after
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
Bool ArePausedDrawsGpuCounted() const;
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
Bool wantGenerated, Uint64& outPrimitives);
private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE;
@@ -768,9 +846,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// version: the version is monotonic and bumps on every pipeline-state
// change, so an unchanged (version, colorAttachmentCount) proves the state
// bytes are unchanged and the hash can be reused without re-reading them.
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) const;
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
// and the pipeline-state memo word so the two cannot disagree.
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
Uint m_pipelineStateHashVersion = 0;
Uint32 m_pipelineStateHashColorCount = 0;
// The sample count the cached hash was computed at. A pipeline-state input now depends on
// it (the effective sample mask), so a draw that changes only the target's sample count
// has to recompute rather than reuse.
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
Uint64 m_pipelineStateHash = 0;
Bool m_pipelineStateHashValid = false;
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
@@ -812,7 +899,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (lifetime id +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
// flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
@@ -823,6 +912,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
Uint64 m_lastSampledSetSamplingGeneration = 0;
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
// VkProgramObject::xfbCaptureDeclined.
Bool m_currentDrawXfbCaptureDeclined = false;
// Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
@@ -921,6 +1015,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// probe the pipeline memo after a state change without re-fetching the
// render-pass entry (the pass itself is pinned by renderPassHash above).
Uint32 renderPassColorCount = 0;
// Pinned with the colour count and for the same reason: the fast path recomputes the
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
VkPipeline pipeline = VK_NULL_HANDLE;
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
// the vertex-input pre-flight depend on the VAO only through this (plus the
@@ -1279,13 +1376,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
// Clears one layer of a colour image through a throwaway render pass whose entire content
// is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue);
Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
@@ -39,18 +39,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
!MG_Config::Features.DisableSubgroup;
!MG_Config::Features.MagmaDisableSubgroup;
}
inline Bool ShouldFixIterationRPSubgroupScratch() {
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
// grows one under-declared array; every other module passes through untouched.
return MG_Config::Features.FixIterationRPSubgroupScratch !=
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.IterationRPFixBarrier;
return MG_Config::Features.MagmaIterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
@@ -58,6 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// contract to hold, and the derived ceil() value is the one the renderer can pin
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
+179
View File
@@ -0,0 +1,179 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <cstdint>
#include <cstring>
namespace MobileGL::MG_Pipe {
const char* MGPipeVerbName(MGPipeVerb verb) {
const auto index = static_cast<SizeT>(verb);
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
}
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
#if MOBILEGL_PIPE_VERIFY
namespace {
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
// Every overload is declared up front: the array overloads recurse into their element
// type, and a call inside a template only sees what was declared before the template.
template <class T>
Bool StorageEqual(const T& a, const T& b);
template <class T>
Bool StorageEqual(T* const& a, T* const& b);
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
template <class T>
void CorruptStorage(T& v);
template <class T>
void CorruptStorage(T*& p);
template <class T>
void CorruptStorage(SharedPtr<T>& p);
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]);
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
void CorruptStorage(CurrentVertexAttributeValue& v);
// ---- equality over one field's storage ----
// O-class storage compares by identity: a raw pointer into the context, or the object a
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
// C arrays element-wise.
template <class T>
Bool StorageEqual(T* const& a, T* const& b) {
return a == b;
}
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
return a.get() == b.get();
}
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!StorageEqual(a[i], b[i])) return false;
}
return true;
}
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
}
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
// what turns a fourth member into a build break rather than a blind spot.
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
"CurrentVertexAttributeValue grew a member; update the comparator");
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
}
template <class T>
Bool StorageEqual(const T& a, const T& b) {
return MGPipeFieldEqual(a, b);
}
// ---- corruption of one field's storage ----
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
// flipped address with no control block, an array corrupts its first element, and any
// other struct has its first byte XOR'ed with 0x5A.
template <class T>
T* FlipPointer(T* p) {
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
}
template <class T>
void CorruptStorage(T*& p) {
p = FlipPointer(p);
}
template <class T>
void CorruptStorage(SharedPtr<T>& p) {
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
}
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]) {
CorruptStorage(a[0]);
}
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
CorruptStorage(c.Blend);
}
void CorruptStorage(CurrentVertexAttributeValue& v) {
v.floatValue[0] += 1.f;
}
template <class T>
void CorruptStorage(T& v) {
if constexpr (std::is_same_v<T, Bool>) {
v = !v;
} else if constexpr (std::is_enum_v<T>) {
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
} else if constexpr (std::is_arithmetic_v<T>) {
v = static_cast<T>(v + 1);
} else {
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
unsigned char first = 0;
std::memcpy(&first, &v, 1);
first ^= 0x5A;
std::memcpy(&v, &first, 1);
}
}
} // namespace
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
// A forwarded field has no storage and is equal by definition; VisitStorage answers
// false for it, hence the explicit sticky test first.
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
}
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
MGPipeInputField* outField) {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
if (outField != nullptr) *outField = field;
return false;
}
return true;
}
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
CorruptStorage(x);
return true;
});
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace MobileGL::MG_Pipe
+714
View File
@@ -0,0 +1,714 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <MG_Pipe/MGPipe.h>
// The frontend types the accessors return. Allowed here: P13 keeps this include for the
// verify arm (ARCHITECTURE.md 9.5). This header spells no MG_State global - every read of
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_State/GLState/Core.h>
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
// there without dragging MG_Remote in.
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
MOBILEGL_PIPE_VERIFY)
#define MOBILEGL_PIPE_POISON 1
#else
#define MOBILEGL_PIPE_POISON 0
#endif
namespace MobileGL::MG_Pipe {
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
// MOBILEGL_ASSERT, which is inert in INFO builds.
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
const char* MGPipeVerbName(MGPipeVerb verb);
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
// a read of a field whose stamp is older than the current verb serial is
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
#if MOBILEGL_PIPE_POISON
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
} \
} while (0)
#else
#define MGP_INPUT_CHECK(Field) ((void)0)
#endif
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
// against the stored value, and reports the FIRST divergence as
// Fatal{PipeVerifyDiffer, "Field@Verb", verb=<serial>, where=read} (the indices go in a
// preceding MGLOG_E). Only the live block (gPipeInputs) is verified; a snapshot's own
// accessors are plain loads. Off in every other build.
struct PipeInputs;
#if MOBILEGL_PIPE_VERIFY
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1);
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) \
::MobileGL::MG_Pipe::MGPipeVerifyReadHook(*this, (Field), static_cast<Uint>(Index0), static_cast<Uint>(Index1))
#else
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
#endif
// The V/O storage of every field that has storage, by field id. The seven F-class
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
// is what keeps the comparator and the corruption injector one function each instead of
// two sixty-way switches.
// clang-format off
#define MGP_INPUT_STORAGE_LIST(X) \
X(GetActiveTextureUnit, m_activeTextureUnit) \
X(GetBlendColor, m_blendColor) \
X(GetBlendEquationIndexed, m_blendEquation) \
X(GetBlendFuncIndexed, m_blendFunc) \
X(GetBoundTransformFeedbackName, m_boundTransformFeedbackName) \
X(GetBoundVertexArray, m_boundVertexArray) \
X(GetBufferBindingSlot, m_bufferBindingSlot) \
X(GetBufferBindingPoint, m_bufferBindingPointBase) \
X(GetTouchedBufferBindingPointCount, m_touchedBindingPointCount) \
X(GetClampReadColor, m_clampReadColor) \
X(GetClearColor, m_clearColor) \
X(GetClearDepth, m_clearDepth) \
X(GetClearStencil, m_clearStencil) \
X(GetColorMaskIndexed, m_colorMask) \
X(GetCullFaceMode, m_cullFaceMode) \
X(GetCurrentVertexAttribute, m_currentVertexAttribute) \
X(GetDepthFunc, m_depthFunc) \
X(GetDepthMask, m_depthMask) \
X(GetDepthRangeIndexed, m_depthRange) \
X(GetFramebufferBindingSlot, m_framebufferBindingSlot) \
X(GetImageTextureBinding, m_imageTextureBindingBase) \
X(GetLineWidth, m_lineWidth) \
X(GetLogicOp, m_logicOp) \
X(GetMaxTouchedTextureUnit, m_maxTouchedTextureUnit) \
X(GetMinSampleShadingValue, m_minSampleShadingValue) \
X(GetPatchDefaultInnerLevel, m_patchDefaultInnerLevel) \
X(GetPatchDefaultOuterLevel, m_patchDefaultOuterLevel) \
X(GetPatchVertices, m_patchVertices) \
X(GetPipelineStateVersion, m_pipelineStateVersion) \
X(GetPixelStoreParameters, m_pixelStore) \
X(GetPolygonModeFront, m_polygonModeFront) \
X(GetPolygonOffsetFactor, m_polygonOffsetFactor) \
X(GetPolygonOffsetUnits, m_polygonOffsetUnits) \
X(GetPrimitiveRestartIndex, m_primitiveRestartIndex) \
X(GetProgramForDispatch, m_programForDispatch) \
X(GetProgramForDraw, m_programForDraw) \
X(GetProvokingVertexMode, m_provokingVertexMode) \
X(GetRenderStateParameters, m_renderState) \
X(GetRenderStateParametersVersion, m_renderStateParametersVersion) \
X(GetSamplingResolutionGeneration, m_samplingResolutionGeneration) \
X(GetScissorBox, m_scissorBox) \
X(GetStencilState, m_stencil) \
X(GetTextureBindGeneration, m_textureBindGeneration) \
X(GetTextureContextId, m_textureContextId) \
X(GetTextureUnitObject, m_textureUnitBase) \
X(GetTransformFeedbackCapturedVertices, m_transformFeedbackCapturedVertices) \
X(GetTransformFeedbackGeneration, m_transformFeedbackGeneration) \
X(GetTransformFeedbackPausedPrimitiveCounter, m_transformFeedbackPausedPrimitiveCounter) \
X(GetTransformFeedbackProgram, m_transformFeedbackProgram) \
X(GetViewport, m_viewport) \
X(GetViewportIndexed, m_viewportIndexed) \
X(IsCapabilityEnabled, m_capability) \
X(IsCapabilityEnabledIndexed, m_capabilityIndexed) \
X(IsTransformFeedbackActive, m_transformFeedbackActive) \
X(IsTransformFeedbackPaused, m_transformFeedbackPaused) \
X(GetBoundTransformFeedbackLifetimeId, m_boundTransformFeedbackLifetimeId)
// clang-format on
// The seven F-class fields, for the arithmetic below and for the sticky table's proof.
// The forwarded set IS the sticky set (PipeFields.def marks the same seven rows F and
// sticky), so an eighth sticky row without a forwarder is refused here, not by a test.
inline constexpr SizeT kMGPipeForwardedFieldCount = 7;
static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount,
"the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows");
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
// sed is type-neutral:
//
// V (value) copied out of GLContext at fill time by calling the same accessor;
// no derivation logic is re-implemented here, which is what keeps the
// copy semantically identical by construction.
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
// slot/array for the accessors that return a non-const reference into
// the context. Identity is what phase C turns into a handle.
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
// context may be spelled). Sticky: stamped once by the first fill that
// sees a live context.
//
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
struct PipeInputs {
using GLContext = MG_State::GLState::GLContext;
using BufferObject = MG_State::GLState::BufferObject;
using BufferTarget = ::MobileGL::BufferTarget;
using FramebufferObject = MG_State::GLState::FramebufferObject;
using FramebufferTarget = ::MobileGL::FramebufferTarget;
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
using ProgramObject = MG_State::GLState::ProgramObject;
using ITextureObject = MG_State::GLState::ITextureObject;
using TextureUnit = MG_State::GLState::TextureUnit;
using ImageTextureBinding = MG_State::GLState::ImageTextureBinding;
using CurrentVertexAttributeValue = MG_State::GLState::CurrentVertexAttributeValue;
static constexpr SizeT kBufferTargetCount = static_cast<SizeT>(BufferTarget::BufferTargetCount);
static constexpr SizeT kFramebufferTargetCount = static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount);
static constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS;
static constexpr SizeT kStencilFaceCount = static_cast<SizeT>(StencilFace::StencilFaceCount);
// IsCapabilityEnabledIndexed's two indexed capabilities, the only ones GLContext keeps
// indexed state for (RenderState::IsCapabilityEnabledIndexed).
struct IndexedCapabilities {
Bool Blend[kMGMaxDrawBuffers];
Bool ScissorTest[kMaxViewports];
};
// ---- identity / liveness (not fields) ----
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
// must be true as soon as a context exists, fill or no fill, which is what today's
// null-context guards test.
Bool IsLive() const;
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
const void* ContextIdentity() const { return m_contextIdentity; }
// The verb of the last fill, kVerbCount before the first one.
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
#if MOBILEGL_PIPE_POISON
const MGPipeFilledState& FilledState() const { return m_filled; }
#endif
// ---- V: values ----
Int GetActiveTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetActiveTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetActiveTextureUnit, 0, 0);
return m_activeTextureUnit;
}
const FloatVec4& GetBlendColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendColor, 0, 0);
return m_blendColor;
}
void GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendEquationIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendEquationIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend equation index out of range: %u", index);
return;
}
color = m_blendEquation[index][0];
alpha = m_blendEquation[index][1];
}
void GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
BlendFactor& dstAlpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendFuncIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendFuncIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend func index out of range: %u", index);
return;
}
srcRGB = m_blendFunc[index][0];
dstRGB = m_blendFunc[index][1];
srcAlpha = m_blendFunc[index][2];
dstAlpha = m_blendFunc[index][3];
}
// Dead field: filled, read by no backend since the D21 XFB counter-slot rekey; kept so
// the vendored inventory row keeps its mapping (Coverage.def).
Uint GetBoundTransformFeedbackName() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackName);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackName, 0, 0);
return m_boundTransformFeedbackName;
}
SizeT GetTouchedBufferBindingPointCount(BufferTarget target) const {
MGP_INPUT_CHECK(MGPipeInputField::GetTouchedBufferBindingPointCount);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTouchedBufferBindingPointCount, static_cast<Uint>(target), 0);
return m_touchedBindingPointCount[static_cast<SizeT>(target)];
}
GLenum GetClampReadColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClampReadColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClampReadColor, 0, 0);
return m_clampReadColor;
}
const FloatVec4& GetClearColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearColor, 0, 0);
return m_clearColor;
}
Float GetClearDepth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearDepth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearDepth, 0, 0);
return m_clearDepth;
}
Uint32 GetClearStencil() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearStencil);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearStencil, 0, 0);
return m_clearStencil;
}
BoolVec4 GetColorMaskIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetColorMaskIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetColorMaskIndexed, index, 0);
return m_colorMask[index];
}
CullFaceMode GetCullFaceMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetCullFaceMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCullFaceMode, 0, 0);
return m_cullFaceMode;
}
const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetCurrentVertexAttribute);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCurrentVertexAttribute, index, 0);
if (index >= kMaxVertexAttribs) {
static const CurrentVertexAttributeValue defaultValue{};
MGLOG_E_ONCE("PipeInputs::GetCurrentVertexAttribute: index %u is out of range", index);
return defaultValue;
}
return m_currentVertexAttribute[index];
}
DepthTestFunc GetDepthFunc() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthFunc);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthFunc, 0, 0);
return m_depthFunc;
}
Bool GetDepthMask() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthMask);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthMask, 0, 0);
return m_depthMask;
}
const FloatVec2& GetDepthRangeIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthRangeIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthRangeIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
return m_depthRange[0];
}
return m_depthRange[index];
}
Float GetLineWidth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLineWidth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLineWidth, 0, 0);
return m_lineWidth;
}
LogicOperation GetLogicOp() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLogicOp);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLogicOp, 0, 0);
return m_logicOp;
}
Int GetMaxTouchedTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMaxTouchedTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMaxTouchedTextureUnit, 0, 0);
return m_maxTouchedTextureUnit;
}
Float GetMinSampleShadingValue() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMinSampleShadingValue);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMinSampleShadingValue, 0, 0);
return m_minSampleShadingValue;
}
const FloatVec2& GetPatchDefaultInnerLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultInnerLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultInnerLevel, 0, 0);
return m_patchDefaultInnerLevel;
}
const FloatVec4& GetPatchDefaultOuterLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultOuterLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultOuterLevel, 0, 0);
return m_patchDefaultOuterLevel;
}
Uint GetPatchVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchVertices, 0, 0);
return m_patchVertices;
}
Uint GetPipelineStateVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPipelineStateVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPipelineStateVersion, 0, 0);
return m_pipelineStateVersion;
}
Uint GetRenderStateParametersVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParametersVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
return m_renderStateParametersVersion;
}
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
return m_pixelStore[isUnpack ? 1 : 0];
}
GLenum GetPolygonModeFront() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonModeFront);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonModeFront, 0, 0);
return m_polygonModeFront;
}
Float GetPolygonOffsetFactor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetFactor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetFactor, 0, 0);
return m_polygonOffsetFactor;
}
Float GetPolygonOffsetUnits() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetUnits);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetUnits, 0, 0);
return m_polygonOffsetUnits;
}
Uint32 GetPrimitiveRestartIndex() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPrimitiveRestartIndex);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPrimitiveRestartIndex, 0, 0);
return m_primitiveRestartIndex;
}
ProvokingVertexMode GetProvokingVertexMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetProvokingVertexMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProvokingVertexMode, 0, 0);
return m_provokingVertexMode;
}
const RenderStateParameters& GetRenderStateParameters() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParameters, 0, 0);
return m_renderState;
}
Uint64 GetSamplingResolutionGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
return m_samplingResolutionGeneration;
}
const IntVec4& GetScissorBox() const {
MGP_INPUT_CHECK(MGPipeInputField::GetScissorBox);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetScissorBox, 0, 0);
return m_scissorBox;
}
const StencilFaceState& GetStencilState(StencilFace face) const {
MGP_INPUT_CHECK(MGPipeInputField::GetStencilState);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetStencilState, static_cast<Uint>(face), 0);
return m_stencil[face == StencilFace::Back ? 1 : 0];
}
Uint64 GetTextureBindGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
return m_textureBindGeneration;
}
Uint64 GetTextureContextId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
return m_textureContextId;
}
Uint64 GetTransformFeedbackCapturedVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackCapturedVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackCapturedVertices, 0, 0);
return m_transformFeedbackCapturedVertices;
}
Uint64 GetTransformFeedbackGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackGeneration, 0, 0);
return m_transformFeedbackGeneration;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter, 0, 0);
return m_transformFeedbackPausedPrimitiveCounter;
}
Uint64 GetBoundTransformFeedbackLifetimeId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackLifetimeId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackLifetimeId, 0, 0);
return m_boundTransformFeedbackLifetimeId;
}
IntVec4 GetViewport() const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewport);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewport, 0, 0);
return m_viewport;
}
const FloatVec4& GetViewportIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewportIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewportIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
return m_viewportIndexed[0];
}
return m_viewportIndexed[index];
}
Bool IsCapabilityEnabled(CapabilityInput cap) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabled);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabled, static_cast<Uint>(cap), 0);
const auto index = static_cast<SizeT>(cap);
return index < kCapabilityCount ? m_capability[index] : false;
}
// Blend and ScissorTest are the only indexed capabilities GLContext keeps; no backend
// asks for another (VulkanRenderer asks Blend). Any other cap is a read the fill cannot
// have served: Fatal{UnmigratedPipeInput} naming the field and the verb, the cap in a
// preceding MGLOG_E.
Bool IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabledIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabledIndexed, static_cast<Uint>(cap), index);
if (cap == CapabilityInput::Blend) {
return index < kMGMaxDrawBuffers ? m_capabilityIndexed.Blend[index] : false;
}
if (cap == CapabilityInput::ScissorTest) {
return index < kMaxViewports ? m_capabilityIndexed.ScissorTest[index] : false;
}
MGLOG_E("PipeInputs::IsCapabilityEnabledIndexed: no indexed storage for cap=%d (index=%u)",
static_cast<int>(cap), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::IsCapabilityEnabledIndexed, m_currentVerb);
}
Bool IsTransformFeedbackActive() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackActive);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackActive, 0, 0);
return m_transformFeedbackActive;
}
Bool IsTransformFeedbackPaused() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackPaused);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackPaused, 0, 0);
return m_transformFeedbackPaused;
}
// ---- O: object references ----
const SharedPtr<VertexArrayObject>& GetBoundVertexArray() {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundVertexArray);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundVertexArray, 0, 0);
return m_boundVertexArray;
}
// A target the fill left null (one outside GlobalBufferTargets / BufferBindPointTargets,
// or a read before any fill) is a read the fill cannot have served: the poison Fatal,
// the target in a preceding MGLOG_E.
BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb);
}
return *m_bufferBindingSlot[index];
}
BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget target, Uint index) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast<Uint>(target), index);
const auto targetIndex = static_cast<SizeT>(target);
if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)",
static_cast<int>(target), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb);
}
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
// (BufferState.h), so base[index] is the live slot GLContext would hand out.
return m_bufferBindingPointBase[targetIndex][index];
}
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetFramebufferBindingSlot, m_currentVerb);
}
return *m_framebufferBindingSlot[index];
}
ImageTextureBinding& GetImageTextureBinding(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const ImageTextureBinding& GetImageTextureBinding(Int unit) const {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const SharedPtr<ProgramObject>& GetProgramForDispatch() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0);
return m_programForDispatch;
}
const SharedPtr<ProgramObject>& GetProgramForDraw() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0);
return m_programForDraw;
}
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackProgram);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackProgram, 0, 0);
return m_transformFeedbackProgram;
}
TextureUnit& GetTextureUnitObject(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureUnitObject);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureUnitObject, static_cast<Uint>(unit), 0);
if (m_textureUnitBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetTextureUnitObject, m_currentVerb);
}
return m_textureUnitBase[unit];
}
// ---- F: forwarded to the live context (MG_Impl/Pipe/PipeFill.cpp); sticky ----
// Each takes an argument that is not verb state - a GL name, a lifetime id, a target -
// i.e. it is a lookup or a reverse-channel write, not a state read; there is no value
// the filler could copy and no verb whose fill could make it stale. Phase C replaces
// them with handle tables and callbacks.
// They carry no MGP_INPUT_CHECK / MGP_INPUT_VERIFY_READ (the declared exception to
// P1 brief D4's "every accessor body"): a forward is a live call, not a stored value,
// and InvalidateCompileEnv is reached from backend initialisation before any verb has
// filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp
// is therefore consulted by no accessor; the tests pin it through
// MGPipeInputFieldIsFresh directly.
SizeT GetBufferBindingPointCount(BufferTarget target) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
void InvalidateCompileEnv();
Bool ValidateProgramName(Uint index) const;
// Dropped with an MGLOG_E_ONCE when no context is live; today's guarded sites never
// reach it without one.
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
// ---- the storage visitor ----
// Calls fn(a.<member>, b.<member>) for the field's storage and returns its result; returns
// false without calling fn for a forwarded field, which has none. The comparator's
// per-field equality and the verify corruption injector are both one call of this.
template <class Fn>
static Bool VisitStorage(MGPipeInputField field, PipeInputs& a, PipeInputs& b, Fn&& fn) {
switch (field) {
#define MGP_INPUT_VISIT(Field, Member) \
case MGPipeInputField::Field: \
return fn(a.Member, b.Member);
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
#undef MGP_INPUT_VISIT
default:
return false;
}
}
template <class Fn>
static Bool VisitStorage(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b, Fn&& fn) {
switch (field) {
#define MGP_INPUT_VISIT(Field, Member) \
case MGPipeInputField::Field: \
return fn(a.Member, b.Member);
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
#undef MGP_INPUT_VISIT
default:
return false;
}
}
private:
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
// the filler's per-field copies and stamps, and the verify snapshot.
friend struct MGPipeFillAccess;
// ---- identity ----
const void* m_contextIdentity = nullptr;
Bool m_live = false;
MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount;
#if MOBILEGL_PIPE_POISON
MGPipeFilledState m_filled{};
#endif
// ---- V ----
Int m_activeTextureUnit = 0;
FloatVec4 m_blendColor{};
BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]{};
BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]{};
Uint m_boundTransformFeedbackName = 0;
SizeT m_touchedBindingPointCount[kBufferTargetCount]{};
GLenum m_clampReadColor = 0;
FloatVec4 m_clearColor{};
Float m_clearDepth = 0.f;
Uint32 m_clearStencil = 0;
BoolVec4 m_colorMask[kMGMaxDrawBuffers]{};
CullFaceMode m_cullFaceMode{};
CurrentVertexAttributeValue m_currentVertexAttribute[kMaxVertexAttribs]{};
DepthTestFunc m_depthFunc{};
Bool m_depthMask = false;
FloatVec2 m_depthRange[kMaxViewports]{};
Float m_lineWidth = 0.f;
LogicOperation m_logicOp{};
Int m_maxTouchedTextureUnit = -1;
Float m_minSampleShadingValue = 0.f;
FloatVec2 m_patchDefaultInnerLevel{};
FloatVec4 m_patchDefaultOuterLevel{};
Uint m_patchVertices = 0;
Uint m_pipelineStateVersion = 0;
Uint m_renderStateParametersVersion = 0;
PixelStoreParameters m_pixelStore[2]{}; // [0] = pack, [1] = unpack
GLenum m_polygonModeFront = 0;
Float m_polygonOffsetFactor = 0.f;
Float m_polygonOffsetUnits = 0.f;
Uint32 m_primitiveRestartIndex = 0;
ProvokingVertexMode m_provokingVertexMode{};
RenderStateParameters m_renderState{};
Uint64 m_samplingResolutionGeneration = 0;
Uint64 m_textureBindGeneration = 0;
Uint64 m_textureContextId = 0;
IntVec4 m_scissorBox{};
StencilFaceState m_stencil[kStencilFaceCount]{};
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_boundTransformFeedbackLifetimeId = 0;
IntVec4 m_viewport{};
FloatVec4 m_viewportIndexed[kMaxViewports]{};
Bool m_capability[kCapabilityCount]{};
IndexedCapabilities m_capabilityIndexed{};
Bool m_transformFeedbackActive = false;
Bool m_transformFeedbackPaused = false;
// ---- O ----
SharedPtr<VertexArrayObject> m_boundVertexArray;
BindingSlot<BufferObject>* m_bufferBindingSlot[kBufferTargetCount]{};
BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[kBufferTargetCount]{};
BindingSlot<FramebufferObject>* m_framebufferBindingSlot[kFramebufferTargetCount]{};
ImageTextureBinding* m_imageTextureBindingBase = nullptr;
SharedPtr<ProgramObject> m_programForDispatch;
SharedPtr<ProgramObject> m_programForDraw;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
TextureUnit* m_textureUnitBase = nullptr;
};
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
// variable: no .cpp is needed for the definition.
inline PipeInputs gPipeInputs{};
// Every field has storage or is forwarded, and nothing else.
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
"MGP_INPUT_STORAGE_LIST plus the seven forwarded fields is not the PipeInputs field set");
#undef MGP_INPUT_COUNT_ONE
// The docs budget ~20 KB; the block is a few KB.
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
#if MOBILEGL_PIPE_VERIFY
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
// always equal (no storage).
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b);
// PipeInputs.cpp. The entry compare: every field in `mask` of the pushed block against the
// snapshot, first differing field out. Exported from the shared library on purpose - the
// retrace-verify CI job proves it swapped in a verify build by finding this symbol with
// nm -D, so a "green" run against a library without the comparator cannot happen.
#if defined(__GNUC__) || defined(__clang__)
__attribute__((visibility("default")))
#endif
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
MGPipeInputField* outField);
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
// scalar, ^0x5A the first byte of a struct, flip a pointer's low bits - never
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
// which has nothing to corrupt.
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
#endif
} // namespace MobileGL::MG_Pipe
+69 -2
View File
@@ -11,6 +11,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include "../Getter/GL_Getter.h"
namespace MobileGL::MG_Impl::GLImpl {
@@ -146,6 +147,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
// Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
// them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
// line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
// the default arm did - made AccountTransformFeedbackPrimitives bail before it had
// recorded anything, so an adjacency capture advanced neither the captured-vertex
// counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
// that routes the transform feedback queries to the driver's own counter.
case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
// GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
// on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
// to the driver's own counter, which is the only correct source for a patch capture.
default: return 0;
}
}
@@ -172,11 +187,17 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_LINES:
case GL_LINE_STRIP:
case GL_LINE_LOOP:
// An adjacency primitive delivers the same line/triangle to the geometry stage; the
// adjacent vertices are context, not part of the primitive.
case GL_LINES_ADJACENCY:
case GL_LINE_STRIP_ADJACENCY:
verticesPerPrimitive = 2;
break;
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
case GL_TRIANGLES_ADJACENCY:
case GL_TRIANGLE_STRIP_ADJACENCY:
verticesPerPrimitive = 3;
break;
default:
@@ -381,11 +402,21 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
// The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
// This arm is only reached when the program has NO geometry or tessellation
// evaluation stage, and without a geometry stage the adjacent vertices are simply
// ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
// so the combination is legal and must capture. Omitting them raised a spurious
// GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
// with its pre-draw bytes. The geometry-stage input table above already carries the
// same four arms; this is the second table catching up with it.
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
break;
default:
break;
@@ -497,6 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(Clear);
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
}
@@ -505,6 +537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElements);
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
}
@@ -514,6 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElements);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
}
@@ -523,6 +557,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex);
}
@@ -532,6 +567,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawArrays);
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
}
@@ -540,6 +576,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArrays);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
}
@@ -549,6 +586,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
}
@@ -558,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsIndirect);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
@@ -566,6 +605,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArraysIndirect);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
}
@@ -575,6 +615,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsIndirectCount);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride);
}
@@ -585,6 +626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArraysIndirectCount);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride);
}
@@ -595,6 +637,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawRangeElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex);
}
@@ -605,6 +648,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawRangeElements);
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
}
@@ -615,6 +659,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsInstancedBaseVertexBaseInstance);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
mode, count, type, indices, instancecount, basevertex, baseinstance);
}
@@ -625,6 +670,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsInstancedBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
basevertex);
}
@@ -635,6 +681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsInstancedBaseInstance);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
instancecount, baseinstance);
}
@@ -645,6 +692,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsInstanced);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
}
@@ -653,6 +701,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsIndirect);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
}
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -661,6 +710,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawArraysInstancedBaseInstance);
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
baseinstance);
}
@@ -670,6 +720,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawArraysInstanced);
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
}
@@ -678,6 +729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawArraysIndirect);
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
}
@@ -709,6 +761,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DispatchCompute);
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
}
@@ -761,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DispatchComputeIndirect);
dispatchComputeIndirect(indirect);
}
@@ -782,6 +836,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
MGP_FILL(PatchParameteri);
patchParameteri(pname, value);
}
}
@@ -852,6 +907,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
return;
}
MGP_FILL(MemoryBarrier);
memoryBarrier(barriers);
}
@@ -873,6 +929,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
return;
}
MGP_FILL(MemoryBarrier);
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
}
@@ -886,6 +943,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support regional memory barriers."));
return;
}
MGP_FILL(MemoryBarrierByRegion);
memoryBarrierByRegion(barriers);
}
@@ -1208,6 +1266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
MGP_FILL(BeginTransformFeedback);
beginXfb(primitiveMode);
}
}
@@ -1290,6 +1349,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// Closed while the capture state is still active: a backend that captures
// through its own driver reads the capture program and buffer bindings here.
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
MGP_FILL(EndTransformFeedback);
endXfb();
}
MG_State::pGLContext->EndTransformFeedback();
@@ -1298,9 +1358,12 @@ namespace MobileGL::MG_Impl::GLImpl {
// the GPU work is all that is required.
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
MGP_FILL(FenceSync);
if (auto sync = backendGL.FenceSync()) {
MGP_FILL(ClientWaitSync);
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
if (backendGL.DeleteSync) {
MGP_FILL(DeleteSync);
backendGL.DeleteSync(sync);
}
}
@@ -1319,6 +1382,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->SetTransformFeedbackPaused(true);
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
MGP_FILL(PauseTransformFeedback);
pauseXfb();
}
}
@@ -1333,6 +1397,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->SetTransformFeedbackPaused(false);
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
MGP_FILL(ResumeTransformFeedback);
resumeXfb();
}
}
@@ -1538,6 +1603,7 @@ namespace MobileGL::MG_Impl::GLImpl {
continue;
}
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
MGP_FILL(DeleteTransformFeedback);
deleteXfb(id);
}
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
@@ -1569,6 +1635,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MG_State::pGLContext->BindTransformFeedbackObject(id);
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
MGP_FILL(BindTransformFeedback);
bindXfb(id);
}
}
@@ -15,6 +15,7 @@
#include <MG_Impl/GLImpl/Texture/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
@@ -616,6 +617,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
MGP_FILL(BlitFramebuffer);
MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
mask, filter);
}
@@ -629,6 +631,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
return;
}
MGP_FILL(BlitNamedFramebuffer);
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter);
}
@@ -640,6 +643,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
return;
}
MGP_FILL(ClearNamedFramebufferfv);
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
}
@@ -650,6 +654,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
return;
}
MGP_FILL(ClearNamedFramebufferfi);
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
}
@@ -660,6 +665,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
MGP_FILL(ClearNamedFramebufferiv);
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
}
@@ -670,6 +676,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
MGP_FILL(ClearNamedFramebufferuiv);
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
}
@@ -2729,24 +2736,28 @@ namespace MobileGL::MG_Impl::GLImpl {
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferfi);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
}
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferfv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
}
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferuiv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
}
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferiv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
}
@@ -2994,6 +3005,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
MGP_FILL(ReadPixels);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels);
}
@@ -30,6 +30,7 @@
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
@@ -1173,6 +1174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: GetMinComputeWorkGroupSize(index);
GLint backendValue = 0;
if (getIntegeri) {
MGP_FILL(GetIntegeri_v);
getIntegeri(target, index, &backendValue);
}
*data = std::max(backendValue, minimum);
@@ -1353,6 +1355,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
MGP_FILL(GetGpuTimestampNs);
timestamp = getGpuTimestampNs();
}
}
@@ -2263,6 +2266,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
MGP_FILL(GetGpuTimestampNs);
timestamp = getGpuTimestampNs();
}
}
@@ -21,6 +21,7 @@
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
// The flattened uniform type these helpers used to take as a raw glslang::TType*
@@ -3398,6 +3399,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support shader storage block binding."));
return;
}
MGP_FILL(ShaderStorageBlockBinding);
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
}
+53 -9
View File
@@ -12,6 +12,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -64,21 +65,43 @@ namespace MobileGL::MG_Impl::GLImpl {
// and none of them has any state beyond "which object is counting".
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
// ADVERTISED list rather than from a capability bit for the same reason
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
// support" and "the tessellation-conditional API surface is open" the same fact by
// construction, so the day a backend starts advertising the string the surface below opens
// with it and no second edit is owed.
Bool AdvertisesTessellationShaderExtension() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
}
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core
// since 4.6 and there is no query by which an application could learn otherwise before
// calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1
// provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the
// spec's own signal that the counter is unsupported and its results indeterminate. That
// is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a
// core entry point, is both non-conformant AND less informative.
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
// by which an application could learn otherwise before calling. MobileGL instruments none of
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
// zero for these targets, which is the spec's own signal that the counter is unsupported and
// its results indeterminate. That is an honest zero, not an advertised capability - the
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
// informative.
//
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
// makes them conditional on tessellation support rather than unconditional, and the only
// thing an application (or the conformance suite) can read to decide whether an
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
// tessellation-conditional token while withholding the string that announces the condition
// is self-contradictory, and it is the contradiction the suite catches
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) {
case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_SHADER_INVOCATIONS:
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
case GL_FRAGMENT_SHADER_INVOCATIONS:
@@ -86,6 +109,9 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_CLIPPING_INPUT_PRIMITIVES:
case GL_CLIPPING_OUTPUT_PRIMITIVES:
return true;
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
return AdvertisesTessellationShaderExtension();
default:
return false;
}
@@ -139,6 +165,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void ResetQueryObjectLocked(QueryObject* queryObject) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -153,6 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
if (endTimeElapsedQuery && queryObject->backendHandle) {
MGP_FILL(EndTimeElapsedQuery);
endTimeElapsedQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -232,6 +260,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
Uint64 result = 0;
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (queryObject->backendHandle && getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
// Not ready. The whole point of the no-wait form is that the caller's
@@ -246,6 +275,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -261,6 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
MGP_FILL(IsQueryResultAvailable);
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
return true;
}
@@ -272,6 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Uint64 result = 0;
if (queryObject->backendHandle) {
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
// The backend could not produce the result YET (e.g. a
@@ -292,6 +324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads.
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -397,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -416,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -494,6 +529,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
MGP_FILL(BeginXfbPrimitivesQuery);
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
@@ -502,9 +538,11 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
} else if (isOcclusionQuery) {
MGP_FILL(BeginOcclusionQuery);
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
MGP_FILL(BeginTimeElapsedQuery);
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
@@ -554,6 +592,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
MGP_FILL(EndXfbPrimitivesQuery);
endXfbPrimitivesQuery(queryObject->backendHandle);
}
}
@@ -563,6 +602,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -579,6 +619,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -617,6 +658,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
MGP_FILL(QueryCounterTimestamp);
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true;
@@ -746,6 +788,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
MGP_FILL(IsTimerQuerySupported);
const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0;
@@ -887,6 +930,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
+7
View File
@@ -9,6 +9,7 @@
#include "GL_Sync.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -56,6 +57,7 @@ namespace MobileGL::MG_Impl::GLImpl {
syncObject->condition = condition;
syncObject->flags = flags;
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
MGP_FILL(FenceSync);
syncObject->backendHandle = backendFenceSync();
}
const GLsync handle = reinterpret_cast<GLsync>(syncObject);
@@ -94,6 +96,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!backendClientWaitSync || !syncObject->backendHandle) {
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
}
MGP_FILL(ClientWaitSync);
return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
}
@@ -119,6 +122,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
if (backendWaitSync && syncObject->backendHandle) {
MGP_FILL(WaitSync);
backendWaitSync(syncObject->backendHandle, flags, timeout);
}
}
@@ -139,6 +143,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
if (backendDeleteSync && syncObject->backendHandle) {
MGP_FILL(DeleteSync);
backendDeleteSync(syncObject->backendHandle);
}
delete syncObject;
@@ -174,6 +179,7 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
case GL_SYNC_STATUS: {
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
MGP_FILL(GetSyncStatus);
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
backendGetSyncStatus(syncObject->backendHandle);
value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
@@ -227,6 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
for (const auto& [_, syncObject] : orphans) {
if (backendDeleteSync && syncObject->backendHandle) {
MGP_FILL(DeleteSync);
backendDeleteSync(syncObject->backendHandle);
}
delete syncObject;
+79 -21
View File
@@ -30,6 +30,7 @@
#include <MG_Impl/GLImpl/Sampler/Validators.h>
#include <MG_Util/Math/FixedPointConversion.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
@@ -1076,6 +1077,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Vector<Uint8> scratch(static_cast<SizeT>(width) * static_cast<SizeT>(height) * bytesPerTexel);
{
ScopedNeutralPackState neutralPack;
MGP_FILL(ReadPixels);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data());
}
@@ -1619,6 +1621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GenerateMipmap_Backend(GLenum target) {
MGP_FILL(GenerateMipmap);
MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
}
@@ -4024,6 +4027,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void CopyTexSubImage2D_Backend(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
MGP_FILL(CopyTexSubImage2D);
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
}
@@ -4040,6 +4044,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support image-to-image copies."));
return;
}
MGP_FILL(CopyImageSubData);
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
dstY, dstZ, srcWidth, srcHeight, srcDepth);
}
@@ -4461,6 +4466,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void CopyTexImage2D_Backend(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border) {
MGP_FILL(CopyTexImage2D);
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D(target, level, internalformat, x, y, width, height,
border);
}
@@ -5071,6 +5077,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetTexImage_Backend(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
MGP_FILL(GetTexImage);
MG_Backend::gBackendFunctionsTable.GL.GetTexImage(target, level, format, type, pixels);
}
@@ -5078,9 +5085,22 @@ namespace MobileGL::MG_Impl::GLImpl {
// The half of the GetTexImage/GetTextureImage error set (GL 4.6 core 8.11) that depends on the
// resolved texture object rather than on how it was named. Shared because the by-name entry
// point does not route through GetTexImage_State and so used to enforce none of it.
// A cube map's six faces are six independent images, and both readback spellings name one of
// them: glGetTexImage through the TARGET token, glGetTextureSubImage through zoffset. Both then
// have to tell the size checks below that ONE image is coming back, not six.
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ;
}
// `imagesQueried` is how many of the texture's upload-target images the query hands back, and
// exists for the destination-size check at the bottom. Zero means "all of them", which is what
// the whole-level forms return - every face of a cube map. glGetTextureSubImage naming ONE cube
// face passes 1: sizing that request against six faces' worth would reject the only buffer a
// single-face read has any reason to pass.
Bool ValidateTextureImageQuery(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
TextureInputFormat textureInputFormat, TexturePixelDataType texturePixelDataType,
GLsizei bufSize, const void* pixels, const char* caller) {
GLsizei bufSize, const void* pixels, const char* caller,
SizeT imagesQueried = 0) {
if (!TextureImpl::ValidateTextureObject(textureObject)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -5196,12 +5216,14 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// Tightly packed, and summed over every face because a cube map query returns all
// six. Pack pixel-store state only ever grows this, so a request rejected here
// could not have fit under any packing.
// Tightly packed, and summed over every face because a whole-level cube map query
// returns all six - unless the caller named a single face, which is what a non-zero
// imagesQueried says. Pack pixel-store state only ever grows this, so a request
// rejected here could not have fit under any packing.
const SizeT imageCount = imagesQueried != 0 ? imagesQueried : uploadTargets.size();
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
texturePixelDataType, texelSize) *
uploadTargets.size();
imageCount;
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
MG_State::pGLContext->RecordError(
@@ -5274,9 +5296,14 @@ namespace MobileGL::MG_Impl::GLImpl {
isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
: bindingSlot.GetBoundObject();
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit".
// glGetTexImage has no bufSize argument: -1 stands for "no client-side limit". That skips
// the destination-size branch but NOT the pixel-pack-buffer one, which measures the same
// `required` against the bound PBO's real size - so a cube FACE query has to say it returns
// one image here too, or a PBO sized for the one face this call packs is refused as too
// small while the copy that follows writes exactly that much into it.
return ValidateTextureImageQuery(textureObject, level, textureInputFormat, texturePixelDataType, -1, pixels,
"GetTexImage_State");
"GetTexImage_State",
IsCubeMapFaceUploadTarget(textureUploadTarget) ? 1u : 0u);
}
// What this helper can and cannot answer.
@@ -6423,6 +6450,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// The half glGetTextureImage and glGetTextureSubImage share: which of the two readbacks answers,
// for ONE named upload target. Factored out so the sub-image form can name a cube FACE - the
// by-name spelling of the face token glGetTexImage takes - instead of re-deriving the target and
// silently landing on the +X face the way the delegation it replaces did.
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
if (MG_Backend::pActiveBackendObject != nullptr &&
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
}
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
}
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
auto textureObject = GetTextureObjectByName(texture, __func__);
if (!textureObject) return;
@@ -6431,16 +6476,8 @@ namespace MobileGL::MG_Impl::GLImpl {
__func__)) {
return;
}
const auto uploadTarget = GetPrimaryUploadTarget(textureObject);
if (MG_Backend::pActiveBackendObject != nullptr &&
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
}
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels,
__func__);
GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
bufSize, pixels, __func__);
}
void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels) {
@@ -6484,9 +6521,19 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && zoffset == 0 &&
width == texelSize.x() && height == texelSize.y() &&
depth == texelSize.z();
// On a cube map, z is the FACE axis. A cube map's level is stored per face, so its level
// size reads z = 1 whichever face named it - but GL 4.6 core 8.11.4 addresses the six faces
// of a cube map through zoffset/depth, exactly the six layers a face token names for
// glGetTexImage. Without this arm the z range was measured against that 1 and only zoffset 0
// (the +X face) was expressible; the other five were rejected as a partial read.
//
// Only ONE face at a time. depth > 1 would have to concatenate faces into the destination,
// which is the same unimplemented multi-image packing the check below still refuses.
const Bool isSingleCubeFaceRead = textureObject->GetTarget() == TextureTarget::TextureCubeMap &&
depth == 1 && zoffset < 6;
const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && width == texelSize.x() &&
height == texelSize.y() &&
(isSingleCubeFaceRead || (zoffset == 0 && depth == texelSize.z()));
if (!isFullLevelRead) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -6495,7 +6542,17 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
GetTextureImage(texture, level, format, type, bufSize, pixels);
const TextureUploadTarget readUploadTarget =
isSingleCubeFaceRead ? static_cast<TextureUploadTarget>(
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + zoffset)
: uploadTarget;
if (!ValidateTextureImageQuery(textureObject, level, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type), bufSize, pixels, __func__,
isSingleCubeFaceRead ? 1u : 0u)) {
return;
}
GetTextureImageForUploadTarget(textureObject, readUploadTarget, level, format, type, bufSize, pixels,
__func__);
}
// A buffer texture carries none of the sampler or level state these queries report. Reached by
@@ -6608,6 +6665,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
.Bind(textureObject, level, layered, layer, access, format);
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
MGP_FILL(BindImageTexture);
bindImageTexture(unit, texture, level, layered, layer, access, format);
}
+614
View File
@@ -0,0 +1,614 @@
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.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 client side of the PipeInputs block (ARCHITECTURE.md 9.2 phase A): the only place in
// the push arm that reads MG_State::pGLContext. Holds the per-verb filler, the F-class
// forwarders, IsLive, the MOBILEGL_PIPE_POISON_OMIT knob and - in a verify build - the
// second arm (SnapshotFromGLContext), the entry compare, the compare-at-read hook and the
// MOBILEGL_PIPE_VERIFY_CORRUPT / _FATAL knobs. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there).
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Pipe/PipeMutation.h>
#include <Config.h>
#include <atomic>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Pipe {
using GLContext = MG_State::GLState::GLContext;
// The one door into PipeInputs' storage on the client side. A struct rather than a
// list of friend functions so the header names exactly one friend.
struct MGPipeFillAccess {
// Copies ONE field's storage out of the live context by calling the GLContext
// accessor of the same name (P1 brief D4: no derivation logic is re-implemented
// here, which is what keeps the copy semantically identical by construction).
// A forwarded field has no storage and copies nothing.
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
using F = MGPipeInputField;
using MG_State::GLState::BufferBindPointTargets;
using MG_State::GLState::GlobalBufferTargets;
switch (field) {
case F::GetActiveTextureUnit:
dst.m_activeTextureUnit = ctx.GetActiveTextureUnit();
break;
case F::GetBlendColor:
dst.m_blendColor = ctx.GetBlendColor();
break;
case F::GetBlendEquationIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
ctx.GetBlendEquationIndexed(i, dst.m_blendEquation[i][0], dst.m_blendEquation[i][1]);
}
break;
case F::GetBlendFuncIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
ctx.GetBlendFuncIndexed(i, dst.m_blendFunc[i][0], dst.m_blendFunc[i][1], dst.m_blendFunc[i][2],
dst.m_blendFunc[i][3]);
}
break;
case F::GetBoundTransformFeedbackName:
dst.m_boundTransformFeedbackName = ctx.GetBoundTransformFeedbackName();
break;
case F::GetBoundVertexArray:
dst.m_boundVertexArray = ctx.GetBoundVertexArray();
break;
case F::GetBufferBindingSlot:
// Every global target has a slot; Index stays null - GLContext resolves it
// through the bound VAO's element-buffer slot (Core.cpp), a derivation no
// FillPoints.def row can copy - and a read of it is the poison Fatal in the
// accessor. No backend reads it today (every slot read is DrawIndirect,
// DispatchIndirect, Parameter or PixelPack).
for (const auto target : GlobalBufferTargets) {
dst.m_bufferBindingSlot[static_cast<SizeT>(target)] = &ctx.GetBufferBindingSlot(target);
}
break;
case F::GetBufferBindingPoint:
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
// (BufferState.h), so the address of point 0 is the base of that target's row.
for (const auto target : BufferBindPointTargets) {
dst.m_bufferBindingPointBase[static_cast<SizeT>(target)] = &ctx.GetBufferBindingPoint(target, 0);
}
break;
case F::GetTouchedBufferBindingPointCount:
for (const auto target : BufferBindPointTargets) {
dst.m_touchedBindingPointCount[static_cast<SizeT>(target)] =
ctx.GetTouchedBufferBindingPointCount(target);
}
break;
case F::GetClampReadColor:
dst.m_clampReadColor = ctx.GetClampReadColor();
break;
case F::GetClearColor:
dst.m_clearColor = ctx.GetClearColor();
break;
case F::GetClearDepth:
dst.m_clearDepth = ctx.GetClearDepth();
break;
case F::GetClearStencil:
dst.m_clearStencil = ctx.GetClearStencil();
break;
case F::GetColorMaskIndexed:
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
dst.m_colorMask[i] = ctx.GetColorMaskIndexed(i);
}
break;
case F::GetCullFaceMode:
dst.m_cullFaceMode = ctx.GetCullFaceMode();
break;
case F::GetCurrentVertexAttribute:
for (Uint i = 0; i < PipeInputs::kMaxVertexAttribs; ++i) {
dst.m_currentVertexAttribute[i] = ctx.GetCurrentVertexAttribute(i);
}
break;
case F::GetDepthFunc:
dst.m_depthFunc = ctx.GetDepthFunc();
break;
case F::GetDepthMask:
dst.m_depthMask = ctx.GetDepthMask();
break;
case F::GetDepthRangeIndexed:
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_depthRange[i] = ctx.GetDepthRangeIndexed(i);
}
break;
case F::GetFramebufferBindingSlot:
for (SizeT i = 0; i < PipeInputs::kFramebufferTargetCount; ++i) {
dst.m_framebufferBindingSlot[i] =
&ctx.GetFramebufferBindingSlot(static_cast<PipeInputs::FramebufferTarget>(i));
}
break;
case F::GetImageTextureBinding:
// Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0's
// address is the base.
dst.m_imageTextureBindingBase = &ctx.GetImageTextureBinding(0);
break;
case F::GetLineWidth:
dst.m_lineWidth = ctx.GetLineWidth();
break;
case F::GetLogicOp:
dst.m_logicOp = ctx.GetLogicOp();
break;
case F::GetMaxTouchedTextureUnit:
dst.m_maxTouchedTextureUnit = ctx.GetMaxTouchedTextureUnit();
break;
case F::GetMinSampleShadingValue:
dst.m_minSampleShadingValue = ctx.GetMinSampleShadingValue();
break;
case F::GetPatchDefaultInnerLevel:
dst.m_patchDefaultInnerLevel = ctx.GetPatchDefaultInnerLevel();
break;
case F::GetPatchDefaultOuterLevel:
dst.m_patchDefaultOuterLevel = ctx.GetPatchDefaultOuterLevel();
break;
case F::GetPatchVertices:
dst.m_patchVertices = ctx.GetPatchVertices();
break;
case F::GetPipelineStateVersion:
dst.m_pipelineStateVersion = ctx.GetPipelineStateVersion();
break;
case F::GetPixelStoreParameters:
dst.m_pixelStore[0] = ctx.GetPixelStoreParameters(false);
dst.m_pixelStore[1] = ctx.GetPixelStoreParameters(true);
break;
case F::GetPolygonModeFront:
dst.m_polygonModeFront = ctx.GetPolygonModeFront();
break;
case F::GetPolygonOffsetFactor:
dst.m_polygonOffsetFactor = ctx.GetPolygonOffsetFactor();
break;
case F::GetPolygonOffsetUnits:
dst.m_polygonOffsetUnits = ctx.GetPolygonOffsetUnits();
break;
case F::GetPrimitiveRestartIndex:
dst.m_primitiveRestartIndex = ctx.GetPrimitiveRestartIndex();
break;
case F::GetProgramForDispatch:
dst.m_programForDispatch = ctx.GetProgramForDispatch();
break;
case F::GetProgramForDraw:
dst.m_programForDraw = ctx.GetProgramForDraw();
break;
case F::GetProvokingVertexMode:
dst.m_provokingVertexMode = ctx.GetProvokingVertexMode();
break;
case F::GetRenderStateParameters:
dst.m_renderState = ctx.GetRenderStateParameters();
break;
case F::GetRenderStateParametersVersion:
dst.m_renderStateParametersVersion = ctx.GetRenderStateParametersVersion();
break;
case F::GetSamplingResolutionGeneration:
dst.m_samplingResolutionGeneration = ctx.GetSamplingResolutionGeneration();
break;
case F::GetScissorBox:
dst.m_scissorBox = ctx.GetScissorBox();
break;
case F::GetStencilState:
dst.m_stencil[0] = ctx.GetStencilState(StencilFace::Front);
dst.m_stencil[1] = ctx.GetStencilState(StencilFace::Back);
break;
case F::GetTextureBindGeneration:
dst.m_textureBindGeneration = ctx.GetTextureBindGeneration();
break;
case F::GetTextureContextId:
dst.m_textureContextId = ctx.GetTextureContextId();
break;
case F::GetTextureUnitObject:
// Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> (TextureState.h): unit 0 is the base.
dst.m_textureUnitBase = &ctx.GetTextureUnitObject(0);
break;
case F::GetTransformFeedbackCapturedVertices:
dst.m_transformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
break;
case F::GetTransformFeedbackGeneration:
dst.m_transformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
break;
case F::GetTransformFeedbackPausedPrimitiveCounter:
dst.m_transformFeedbackPausedPrimitiveCounter = ctx.GetTransformFeedbackPausedPrimitiveCounter();
break;
case F::GetTransformFeedbackProgram:
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
break;
case F::GetViewport:
dst.m_viewport = ctx.GetViewport();
break;
case F::GetViewportIndexed:
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_viewportIndexed[i] = ctx.GetViewportIndexed(i);
}
break;
case F::IsCapabilityEnabled:
// Every capability, FramebufferSrgb included: it copies today's constant false
// (MEASUREMENTS.md), so no value changes.
for (SizeT i = 0; i < PipeInputs::kCapabilityCount; ++i) {
dst.m_capability[i] = ctx.IsCapabilityEnabled(static_cast<CapabilityInput>(i));
}
break;
case F::IsCapabilityEnabledIndexed:
// The only two indexed capabilities GLContext keeps (RenderState).
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
dst.m_capabilityIndexed.Blend[i] = ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
}
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
dst.m_capabilityIndexed.ScissorTest[i] =
ctx.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i);
}
break;
case F::IsTransformFeedbackActive:
dst.m_transformFeedbackActive = ctx.IsTransformFeedbackActive();
break;
case F::IsTransformFeedbackPaused:
dst.m_transformFeedbackPaused = ctx.IsTransformFeedbackPaused();
break;
case F::GetBoundTransformFeedbackLifetimeId:
dst.m_boundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
break;
// The seven forwarded fields: nothing to copy.
case F::GetBufferBindingPointCount:
case F::GetProgramObject:
case F::GetTextureObject:
case F::HasOpenTransformFeedbackSpan:
case F::InvalidateCompileEnv:
case F::ValidateProgramName:
case F::RecordError:
case F::kFieldCount:
break;
}
}
static void SetIdentity(PipeInputs& inputs, GLContext* ctx) {
inputs.m_live = ctx != nullptr;
inputs.m_contextIdentity = ctx;
}
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
#if MOBILEGL_PIPE_POISON
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
#endif
};
namespace {
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
template <class T>
const SharedPtr<T>& NullShared() {
static const SharedPtr<T> null;
return null;
}
[[noreturn]] void BadKnob(const char* knob, const char* value, const char* why) {
MGLOG_F("MGPipe: Fatal{PipeVerifyBadKnob, \"%s=%s\": %s}", knob, value, why);
std::abort();
}
// ---- MOBILEGL_PIPE_POISON_OMIT (negative control B, P1 brief D6) ----
// The filler skips the STAMP (never the value) of one (verb, field) pair: an omission
// indistinguishable from a forgotten FillPoints.def row, so that verb's read of the
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected.
struct PoisonOmission {
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
PoisonOmission g_omission;
Bool g_omissionKnobParsed = false;
String g_omissionKnobValue; // the value the last parse saw
// Parsed on the first fill and again only when the value changes. A lane loads
// Features once, before any fill, so that is one parse per process there; a forked
// test child that sets Features after its parent already filled gets its own parse,
// which is what puts the parser and its Fatal{PipeVerifyBadKnob} under a unit test.
// An empty value never clears an omission a test armed through MGPipeSetPoisonOmission.
void ParsePoisonOmissionKnob() {
const String& knob = MG_Config::Features.PipePoisonOmit;
if (g_omissionKnobParsed && knob == g_omissionKnobValue) return;
g_omissionKnobParsed = true;
g_omissionKnobValue = knob;
if (knob.empty()) return;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) {
BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "expected <Verb>:<FieldName>");
}
const String verbName = knob.substr(0, colon);
const String fieldName = knob.substr(colon + 1);
const auto verb = MGPipeFindVerb(verbName.c_str());
if (!verb) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such verb in kMGPipeVerbNames");
const auto field = MGPipeFindInputField(fieldName.c_str());
if (!field) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such field in kMGPipeInputFieldNames");
MGPipeSetPoisonOmission(verbName.c_str(), fieldName.c_str());
}
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
}
#if MOBILEGL_PIPE_VERIFY
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
// once per verb (the pushed block against a second snapshot of the live context,
// taken at the same instant - tautological until P2 gives the first arm a real
// filler, and kept falsifiable by MOBILEGL_PIPE_VERIFY_CORRUPT), and the
// COMPARE-AT-READ in every accessor (the stored value against a fresh read of the
// live context at the moment the backend reads it - the arm that is real in P1: it
// catches a value that changed between the verb boundary and the read).
PipeInputs g_snapshot{}; // the second arm
PipeInputs g_readScratch{}; // where the compare-at-read re-read lands
// The read hook arms at the first fill (ArmVerify below), so it cannot see a read
// made before that. That window is covered by the poison instead: MGP_INPUT_CHECK
// precedes MGP_INPUT_VERIFY_READ in every accessor and a stamp of 0 is never fresh,
// so such a read is Fatal{UnmigratedPipeInput, "<Field>@<none>"} before the hook
// could matter - which holds only while a verify build always carries the poison.
static_assert(MOBILEGL_PIPE_POISON, "the compare-at-read hook relies on the poison for reads before the first fill");
struct VerifyState {
Bool Parsed = false;
Bool Enabled = false;
Bool Fatal = true;
Bool InHook = false; // a re-read that re-enters an accessor is not re-verified
Optional<MGPipeInputField> Corrupt;
String CorruptKnob; // the MOBILEGL_PIPE_VERIFY_CORRUPT value the last arm saw
std::atomic<Uint64> Divergences{0};
~VerifyState() {
const Uint64 count = Divergences.load(std::memory_order_relaxed);
if (count != 0) {
MGLOG_E("MGPipe: verify summary - %llu divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0",
static_cast<unsigned long long>(count));
}
}
};
VerifyState g_verify;
// Armed on the first fill and re-armed when any of the three verify knobs'
// Features value (PipeVerify, PipeVerifyFatal, PipeVerifyCorrupt) differs from what
// the last arm latched (the same reason as ParsePoisonOmissionKnob: one arm per lane
// process, a fresh arm for a forked test child that turns a knob after its parent
// filled). Cost: two Bool compares and one String compare per fill, verify builds only.
void ArmVerify() {
const auto& features = MG_Config::Features;
if (g_verify.Parsed && g_verify.Enabled == features.PipeVerify && g_verify.Fatal == features.PipeVerifyFatal &&
g_verify.CorruptKnob == features.PipeVerifyCorrupt) {
return;
}
g_verify.Parsed = true;
g_verify.Enabled = features.PipeVerify;
g_verify.Fatal = features.PipeVerifyFatal;
g_verify.CorruptKnob = features.PipeVerifyCorrupt;
g_verify.Corrupt = Optional<MGPipeInputField>{};
if (!g_verify.Enabled) return;
const String& corrupt = g_verify.CorruptKnob;
if (!corrupt.empty()) {
const auto field = MGPipeFindInputField(corrupt.c_str());
if (!field) {
BadKnob("MOBILEGL_PIPE_VERIFY_CORRUPT", corrupt.c_str(), "no such field in kMGPipeInputFieldNames");
}
g_verify.Corrupt = field;
}
// The lanes grep for this line: a verify run whose log lacks it never armed.
MGLOG_I("MGPipe: verify armed - %u fields, %u verbs, fatal=%d", static_cast<unsigned>(kMGPipeInputFieldCount),
static_cast<unsigned>(kMGPipeVerbCount), g_verify.Fatal ? 1 : 0);
if (g_verify.Corrupt) {
MGLOG_I("MGPipe: verify corruption armed - %s", kMGPipeInputFieldNames[static_cast<SizeT>(*g_verify.Corrupt)]);
}
}
void ReportDivergence(MGPipeInputField field, const char* where) {
const Uint64 serial = MGPipeFillAccess::Filled(gPipeInputs).CurrentVerbSerial;
MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(gPipeInputs.CurrentVerb()),
static_cast<unsigned long long>(serial), where);
if (g_verify.Fatal) std::abort();
g_verify.Divergences.fetch_add(1, std::memory_order_relaxed);
}
void EntryCompare(PipeInputs& inputs, const MGPipeFieldMask& mask) {
if (!g_verify.Enabled) return;
SnapshotFromGLContext(g_snapshot, mask);
// Negative control A: perturb the SNAPSHOT arm, so a green run goes red naming the
// field. A field outside this verb's mask is not compared and stays untouched.
if (g_verify.Corrupt && MGPipeFieldMaskHas(mask, *g_verify.Corrupt)) {
MGPipeApplyVerifyCorruption(g_snapshot, *g_verify.Corrupt);
}
MGPipeInputField differing = MGPipeInputField::kFieldCount;
if (!MGPipeVerifyInputs(inputs, g_snapshot, mask, &differing)) ReportDivergence(differing, "entry");
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace
#if MOBILEGL_PIPE_VERIFY
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask) {
auto* ctx = LiveContext();
MGPipeFillAccess::SetIdentity(snapshot, ctx);
MGPipeFillAccess::SetVerb(snapshot, gPipeInputs.CurrentVerb());
if (ctx == nullptr) return;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field) || kMGPipeInputFieldSticky[i]) continue;
MGPipeFillAccess::CopyField(snapshot, *ctx, field);
}
}
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
if (&self != &gPipeInputs || !g_verify.Enabled || g_verify.InHook) return;
const auto index = static_cast<SizeT>(field);
if (kMGPipeInputFieldSticky[index]) return;
auto* ctx = LiveContext();
if (ctx == nullptr) return;
// The whole field is re-read and compared - a superset of "the same indices", so a
// divergence in an index the backend did not ask for is still a divergence between
// the boundary value and the live value. The indices only decorate the report. The
// cost is per backend read (GetRenderStateParameters re-copies and compares the whole
// struct; GetProgramForDraw re-joins the pending link), inside the verify budget and
// to be kept in mind when reading the verify lane's wall time.
// InHook: the re-read calls the same GLContext accessor the filler calls, and
// GetProgramForDraw's join can re-enter a backend and with it another gPipeInputs
// accessor; that inner read is a plain load rather than a second hook, so the hook
// never recurses (and never reports the inner read against a half-copied scratch).
g_verify.InHook = true;
MGPipeFillAccess::CopyField(g_readScratch, *ctx, field);
const Bool equal = MGPipeInputsFieldEqual(field, self, g_readScratch);
g_verify.InHook = false;
if (equal) return;
MGLOG_E("MGPipe: verify read of %s (index %u, %u) differs from the live context", kMGPipeInputFieldNames[index],
index0, index1);
ReportDivergence(field, "read");
}
#endif // MOBILEGL_PIPE_VERIFY
// ---- push on mutation (P1 lane finding F2) ----
// A backend that writes a frontend object inside its own verb moves a value the verb
// boundary already copied: Magma's ResolveSamplerDescriptor synthesises a fallback
// texture for an unbound sampler and its AllocateStorage/SetInternalFormat bump the
// context's sampling-resolution generation, so every read of that field after the
// fallback differs from the live context (the two SampledSetStaleness / six
// UnboundImageDescriptor entries the verify lane aborted on). The frontend mutator
// spells MGP_NOTE_MUTATION(Field) at the point of the move and lands here.
//
// Only the value is refreshed. The stamp is deliberately left alone: a field whose stamp
// this verb withheld (negative control B) must stay stale, and a field the verb never
// filled must stay Fatal{UnmigratedPipeInput} on the next read rather than be healed by
// an unrelated frontend write.
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
PipeInputs& inputs = gPipeInputs;
auto* ctx = LiveContext();
if (ctx == nullptr) return;
const auto verb = inputs.CurrentVerb();
if (verb == MGPipeVerb::kVerbCount) return; // nothing has filled the block yet
const auto index = static_cast<SizeT>(field);
if (kMGPipeInputFieldSticky[index]) return; // forwarded: no storage to refresh
const MGPipeFieldMask& mask =
kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
if (!MGPipeFieldMaskHas(mask, field)) return; // this verb never pushed it
MGPipeFillAccess::CopyField(inputs, *ctx, field);
}
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
if (verb == nullptr || field == nullptr) {
g_omission = PoisonOmission{};
return;
}
const auto v = MGPipeFindVerb(verb);
const auto f = MGPipeFindInputField(field);
if (!v || !f) BadKnob("MOBILEGL_PIPE_POISON_OMIT", verb, "unknown verb or field");
g_omission.Armed = true;
g_omission.Verb = *v;
g_omission.Field = *f;
#if MOBILEGL_PIPE_POISON
MGLOG_I("MGPipe: poison omission armed - %s@%s", field, verb);
#else
MGLOG_W_ONCE("MGPipe: poison omission %s@%s requested but the poison is not compiled in "
"(MOBILEGL_PIPE_POISON=0): no stamp exists to omit",
field, verb);
#endif
}
// ---- liveness ----
Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }
// ---- the seven F-class forwarders ----
SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const {
const auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0;
}
const SharedPtr<PipeInputs::ProgramObject>& PipeInputs::GetProgramObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared<ProgramObject>();
}
const SharedPtr<PipeInputs::ITextureObject>& PipeInputs::GetTextureObject(Uint index) {
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared<ITextureObject>();
}
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
}
void PipeInputs::InvalidateCompileEnv() {
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
}
Bool PipeInputs::ValidateProgramName(Uint index) const {
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->ValidateProgramName(index);
}
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
auto* ctx = LiveContext();
if (ctx == nullptr) {
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
return;
}
ctx->RecordError(code, Move(info));
}
void MGPipeLeaveVerb() {
PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_PIPE_POISON
// Same bump the next fill would make, without a verb to fill from: no field is
// stamped, so every stamp this verb made falls behind the serial.
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
#endif
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
}
// ---- the filler ----
void MGPipeFillForVerb(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
ArmVerify();
#else
// The runtime knob without the compiled comparator is a no-op that would look green;
// this warning is what a lane's arming assertion turns into red.
if (MG_Config::Features.PipeVerify) {
MGLOG_W_ONCE("MGPipe: MOBILEGL_PIPE_VERIFY=1 requested but the comparator is not compiled in "
"(configure with -DMOBILEGL_PIPE_VERIFY=ON)");
}
#endif
#if MOBILEGL_PIPE_POISON
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
// Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh refuses
// it on both branches, so a read before this first bump is
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
++filled.CurrentVerbSerial;
#endif
MGPipeFillAccess::SetVerb(inputs, verb);
auto* ctx = LiveContext();
MGPipeFillAccess::SetIdentity(inputs, ctx);
if (ctx == nullptr) return;
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
#if MOBILEGL_PIPE_POISON
if (kMGPipeInputFieldSticky[i]) {
// Stamped once by the first fill that sees a live context; fresh through the
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
continue;
}
#else
if (kMGPipeInputFieldSticky[i]) continue;
#endif
MGPipeFillAccess::CopyField(inputs, *ctx, field);
#if MOBILEGL_PIPE_POISON
// The value is copied either way; only the stamp is withheld for the omitted pair.
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
#endif
}
#if MOBILEGL_PIPE_VERIFY
EntryCompare(inputs, mask);
#endif
}
} // namespace MobileGL::MG_Pipe
+55
View File
@@ -0,0 +1,55 @@
// MobileGL - MobileGL/MG_Impl/Pipe/PipeFill.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
// The fill point (ARCHITECTURE.md 9.2, P1 brief D7). MG_Impl spells MGP_FILL(Verb); as the
// statement immediately before every call through gBackendFunctionsTable.GL - after every
// early return the call is behind, inside the loop body for a call made in a loop - so the
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
// it.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
struct PipeInputs;
// PipeFill.cpp. Bumps the per-verb serial, records the verb and the context identity,
// and copies every field in the verb class's may-read mask (kMGPipeClassFieldMask) out
// of the live GLContext, stamping each with the new serial. In a verify build it then
// runs the entry compare against a second snapshot (P1 brief D8).
void MGPipeFillForVerb(MGPipeVerb verb);
// Ends the verb in flight without starting another: bumps the serial, so every field the
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
// it aborts as Fatal{UnmigratedPipeInput, "<Field>@<none>"} - which is what such a read
// is - instead of naming whichever verb happened to be filled last. Nothing in the GL
// entry points calls this: a real verb is always followed by the next verb's fill. It
// exists for a caller that drives a backend helper directly and wants its declaration to
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
void MGPipeLeaveVerb();
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
// the value - of `field` at `verb`, so that verb's read of it is
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
// MOBILEGL_PIPE_POISON_OMIT knob ("<Verb>:<FieldName>") calls this once, on the first
// fill; tests call it directly. Both null clears the omission. An unknown name is
// Fatal{PipeVerifyBadKnob}.
void MGPipeSetPoisonOmission(const char* verb, const char* field);
#if MOBILEGL_PIPE_VERIFY
// PipeFill.cpp. The second arm of the comparator (P1 brief D8, ARCHITECTURE.md 13.2-2):
// fills `snapshot` from the live GLContext the old way, for every field in `mask`. This
// is the branch that survives P13, which is why it is its own function rather than the
// filler's loop.
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
#endif
} // namespace MobileGL::MG_Pipe
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
#else
#define MGP_FILL(Verb) ((void)0)
#endif
+307 -4
View File
@@ -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
@@ -58,6 +59,9 @@ add_executable(MobileGLIntegrationTest
Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/UnwrittenPositionOutputScenario.cpp
Scenarios/SampleMaskScopeScenario.cpp
Scenarios/SampledSetStalenessScenario.cpp
Scenarios/ThreeChannelAttachmentScenario.cpp
Scenarios/SnormAttachmentScenario.cpp
Scenarios/PipelineFailureScenario.cpp
@@ -88,6 +92,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
Scenarios/UnlocatedIoBlockScenario.cpp
Scenarios/TessellationDrawModeScenario.cpp
Scenarios/GeometryDrawModeScenario.cpp
Scenarios/PostLinkAttachScenario.cpp
@@ -97,15 +102,21 @@ add_executable(MobileGLIntegrationTest
Scenarios/VertexAttribBindingScenario.cpp
Scenarios/XfbCaptureBufferReuseScenario.cpp
Scenarios/XfbPrimitiveQueryScenario.cpp
Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
Scenarios/XfbRepeatedCaptureScenario.cpp
Scenarios/TessellationXfbCaptureScenario.cpp
Scenarios/PointSizeDemotionScenario.cpp
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/CopyImagePacked16Scenario.cpp
Scenarios/TextureViewScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredAttachmentShapeScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
Scenarios/AtomicCounterScenario.cpp
Scenarios/LargeArenaAdoptionScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/SpirvShaderBinaryScenario.cpp
@@ -115,6 +126,9 @@ add_executable(MobileGLIntegrationTest
Scenarios/IntegerBorderColorScenario.cpp
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp
Scenarios/DualSourceBlendScenario.cpp
Scenarios/PipeVerifyArmingScenario.cpp
Scenarios/PoisonOmissionScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -280,9 +294,9 @@ if (MOBILEGL_ITEST_VK_ICD)
if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe")
message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on")
list(APPEND MGL_ITEST_VULKAN_ENV
"MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_ITERATIONRP_FIX_BARRIER=1")
"MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1"
"MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1"
"MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1")
endif()
endif()
@@ -345,7 +359,37 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1"
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
# MOBILEGL_LOG_FILE_PATH alongside the pin, because the arming assertion needs somewhere to
# read the library's own report from. The strip's arming signal is a latched MGLOG_I and there
# is no other way for a test process to learn that it fired - MG_Config is not reachable from
# this module on Android, where it links the shipping library. The path is per-lane so nothing
# else appends to it, and the case only trusts the bytes written after it started.
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
# Same shape as the UnlocatedIoBlocks entry: the log path is where the reroute's latched
# MGLOG_I lands, and the arming case only trusts the bytes written after it started.
mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
${MGL_ITEST_VULKAN_ENV})
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
# consumes it differently (Espryt respells the driver-side capture request, Magma binds the
# SPIR-V Xfb decorations to the carrier).
mgl_itest_join_environment(MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_POINT_SIZE_DEMOTION=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-gles.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_POINT_SIZE_DEMOTION=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/point-size-demotion-vulkan.log"
${MGL_ITEST_VULKAN_ENV})
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
set(MGL_ITEST_TIMEOUT 120)
@@ -412,6 +456,23 @@ gtest_discover_tests(MobileGLIntegrationTest
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
)
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
# reason the depth/stencil entry above pins its emulation: without it this scenario is
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
# emulation never runs - the ambient registration would be exercising the un-stripped path
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
# location and the assertion is about the spelling the device gets.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
TEST_FILTER "UnlocatedIoBlockScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
)
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
#
# Not a duplicate of what the two ambient registrations already run: they run whatever
@@ -508,3 +569,245 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
)
# PrimitivesGeneratedNoXfbScenario again, with the GL_PRIMITIVES_GENERATED statistics
# reroute PINNED ON. The ambient DirectVulkan registration runs the same cases under the
# bring-up probe's Auto verdict, so between the two entries both accounting paths answer
# the same GL questions and must produce the same numbers - the "two pools must agree"
# gate this machine can hold that the affected device cannot. The pinned entry is also
# the only one whose arming case runs: it asserts the renderer's latched MGLOG_I, so a
# silently-disarmed reroute (an inverted override mapping, a lost gate) fails here
# instead of leaving every equality case vacuously green. DirectVulkan only - the flag
# steers nothing on DirectGLES.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.PrimGenReroute."
TEST_FILTER "PrimitivesGeneratedNoXfbScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
)
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
# registrations above cover the narrow storage - on every CI driver the widening's POST
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
# DirectVulkan, which has always stored these formats widened.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.WidenedPacked16."
TEST_FILTER "CopyImagePacked16Scenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
)
# PointSizeDemotionScenario with the demotion PINNED ON, per backend, for the reason every
# pinned lane above exists: llvmpipe and lavapipe both HOST gl_PointSize in tessellation and
# geometry stages, so the ambient registrations run these captures through the built-in and
# the demotion - the path every affected Mali device actually takes - would execute nowhere.
# The ambient runs stay the negative control: same scenario, same CPU-computed bytes, native
# path. Both backends, because the demotion is shared phase-B work with two different
# consumers (the ESSL capture respelling vs the SPIR-V Xfb carrier binding).
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.PointSizeDemotion."
TEST_FILTER "PointSizeDemotionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.PointSizeDemotion."
TEST_FILTER "PointSizeDemotionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
)
# --- the third CI mode: MOBILEGL_PIPE_VERIFY -----------------------------------
#
# ARCHITECTURE.md 13.2-(2) asks for a THIRD build mode next to pull and push: two state models in
# one address space, compared field by field at every verb boundary and again at every accessor
# read, 5-10x slower and never shipped. These entries are that mode's lane. They exist only when
# the library was configured with -DMOBILEGL_PIPE_VERIFY=ON, which is deliberate and is half of
# what makes the lane falsifiable: `ctest -L integration-verify --no-tests=error` in a build that
# forgot the option matches NO tests and fails, instead of reporting a green run of nothing.
#
# The other half is PipeVerifyArmingScenario.Armed, which asserts the library's own arming line -
# because MOBILEGL_PIPE_VERIFY=1 in the environment of a library that never compiled the
# comparator in is a silent no-op that looks exactly like a clean pass.
#
# Three things about the ENVIRONMENT properties below, each of which has already gone wrong once
# in this file:
# * every list APPENDS ${MGL_ITEST_COMMON_ENV} / ${MGL_ITEST_VULKAN_ENV}. A ctest ENVIRONMENT
# entry overrides the job environment for the names it lists, so an entry that named only its
# own knobs would lose the EGL vendor and Vulkan ICD pinning and run against whichever driver
# the loader found first.
# * the ambient Verify. entries name NEITHER MOBILEGL_PIPE_VERIFY_CORRUPT NOR
# MOBILEGL_PIPE_POISON_OMIT. That is what lets CI's two always-on negative-control steps
# export those knobs in the JOB environment and have them reach the test processes; a
# property entry of the same name would silently win and the controls would prove nothing.
# * MOBILEGL_LOG_FILE_PATH is per lane, and "per lane" is the exact limit of what it proves. It
# is the only channel a test process has for reading the library's own report (MG_Config is not
# reachable from this module), but the log is opened fopen(path, "w"), so every process in a
# lane TRUNCATES it: after an ambient lane of 400-odd entries the file holds the LAST process
# and nothing else. Reading it is therefore only sound in a filtered, one-entry lane - which is
# why the arming case has a lane and a log of its own below, and why neither this file nor CI
# may read the ambient logs as evidence about the entries that ran before the last one. The
# ambient path is kept for post-mortems (and to keep library chatter out of ctest's capture).
if (MOBILEGL_PIPE_VERIFY)
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
# boundary and again at every accessor read, which the design budgets at 5-10x.
set(MGL_ITEST_VERIFY_TIMEOUT 900)
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
# The arming assertion's own lane, one case per backend, with a log path nothing else writes to.
#
# PipeVerifyArmingScenario.Armed reads the library's log, and the log is a per-LANE resource: it
# is opened fopen(path, "w"), so every process in a lane truncates it. In the ambient Verify.
# lane that is 400-odd processes on one path, run `-j 4` in CI, and a whole-file read there
# races a neighbour's bring-up. Every other log-reading scenario in this file (UnlocatedIoBlocks,
# the primgen reroute, the point-size demotion) is registered exactly like this for the same
# reason. MGITEST_PIPE_ARMING_LANE is a harness marker - the library never reads it - and it is
# what makes the case skip in the ambient lane instead of racing there.
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
# Negative control A (G4). MOBILEGL_PIPE_VERIFY_FATAL=0 so the process SURVIVES its own
# divergence and the case can read the report back out of the log; the CI step that exports
# the same corruption against the ambient lane, where FATAL keeps its default of 1, asserts
# the other half - that a divergence aborts and reds the entry.
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
# Negative control B (G5). The omission skips the STAMP of one field for one verb while still
# copying its value, which is indistinguishable from a fill row nobody wrote; the scenario
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane.
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log"
${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
# The whole suite again, per backend, with the comparator armed. Same scenarios, same
# assertions, but every backend read of frontend state is now checked against a snapshot taken
# from the live context at the verb boundary - which is what "the 742 integration entries
# prove push equals pull" means. Labelled integration-gpu as well so a verify build's
# `ctest -L integration-gpu` still describes the whole registration set.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Verify."
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.Verify."
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT}"
)
# The arming assertion, one entry per backend. This is the entry that fails a lane whose library
# never armed: it runs the same library and the same MOBILEGL_PIPE_VERIFY=1 as the ambient
# entries above, but unlike them it cannot be green against a library with no comparator
# compiled in. Its log is its own, so `-j 4` cannot make it flake.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.VerifyArming."
TEST_FILTER "PipeVerifyArmingScenario.Armed"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.VerifyArming."
TEST_FILTER "PipeVerifyArmingScenario.Armed"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT}"
)
# One case each: the knobs are process-wide, so a corrupted or poisoned process cannot also be
# running the ambient assertions. These four entries are the ones that assert the RED - they
# pass when the comparator and the poison report, and go red when either stops.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.VerifyCorrupted."
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.VerifyCorrupted."
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.PoisonOmitted."
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.PoisonOmitted."
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-verify"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT}"
)
endif()
@@ -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
@@ -225,4 +225,43 @@ void main() {
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
}
// A CPU glBufferSubData issued AFTER a dispatch, read back with NO further GPU work in
// between. Each backend has its own way to invert this pair, and both are pinned here.
// DirectGLES queues app SubData ranges for the draw-time staged-copy flush (the upload
// ring) instead of uploading in place, and readback of a GPU-written buffer overwrites
// the frontend shadow with the driver copy - so if the readback path forgets to flush the
// queued range first, the newer CPU write is REVERTED by the readback and offset 0 reads
// the dispatch's value instead of the reseed. DirectVulkan adopts the buffer into
// coherent GPU memory the moment the dispatch resolves its descriptor, so the SubData
// write lands in the very bytes the GPU reads - while the dispatch still sits recorded in
// the deferred frame command buffer. Unless the frontend retires that pending work before
// writing the adopted store (BufferObject::UploadSubData), the dispatch executes ON TOP
// of the reseed and offset 0 reads reseed + increments instead of the reseed. Offset 4
// pins the other direction for both: the upload must leave bytes outside its range - the
// dispatch's results - untouched.
TEST_F(AtomicCounterScenario, SubDataAfterDispatchSurvivesAnImmediateReadback) {
if (!Ready() || IsSkipped()) return;
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
MakeCounterBuffer(1, {0u});
ASSERT_EQ(FirstGLError(), 0u);
Dispatch();
const unsigned int reseed = 4242u;
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), &reseed);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
const std::vector<unsigned int> values = ReadCounters(zero, 2);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(values[0], reseed)
<< "offset 0 read back " << values[0] << "; the dispatch's value (" << kInvocations
<< ") means the readback ran before the queued SubData range was flushed and reverted it";
EXPECT_EQ(values[1], 2 * kInvocations)
<< "offset 4 read back " << values[1] << "; the SubData flush must leave bytes outside its "
<< "range untouched";
}
} // namespace MGITest
@@ -0,0 +1,375 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
//
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
// texel-block move - between a mirrored allocation and a plain one lands the fields
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
// image are clean (the driver decodes its own layout consistently), which is why only the
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
// checks always passed.
//
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
// the driver's layout.
//
// The repair under test is the packed16 storage widening
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
// encodes to UNorm8 and back losslessly for every n <= 8.
//
// This scenario runs in BOTH configurations, and both must hand back identical client words:
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
// covered);
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
// which is the storage every affected device will actually run - without it the repair
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
// client-invisible: same words in, same words out, on every leg the failing bodies used.
//
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
// means the widening (or the narrow path it replaces) is.
#include <algorithm>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kBaseSize = 30; // array level 0; level 1 is 15x15
constexpr int kLevel1Size = kBaseSize / 2;
constexpr int kLayers = 12;
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
constexpr int kRegion = kFlatSize;
constexpr int kArrayOffset = 8;
struct PackedFormatCase {
GLenum internalFormat; // the spelling the CTS uses
GLenum transferFormat;
GLenum transferType;
const char* name;
};
// Per-texel varying words, every field inside its width, so a swapped field order (or
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
GLushort MakeWord(GLenum type, int i) {
switch (type) {
case GL_UNSIGNED_SHORT_5_6_5: {
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
return static_cast<GLushort>((r << 11) | (g << 5) | b);
}
case GL_UNSIGNED_SHORT_4_4_4_4: {
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
}
case GL_UNSIGNED_SHORT_5_5_5_1: {
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
}
default:
return 0;
}
}
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
std::vector<GLushort> words(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) {
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
}
return words;
}
class CopyImagePacked16Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
// every odd-width row of the 15x15 level and shear the comparisons.
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
glPixelStorei(GL_PACK_ALIGNMENT, 2);
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_renderbuffer != 0) {
glDeleteRenderbuffers(1, &m_renderbuffer);
m_renderbuffer = 0;
}
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
// below can pass by reading a level that was never written.
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
const std::vector<GLushort>& level1) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
kLayers, 0, format.transferFormat, format.transferType, level0.data());
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
const int level2Size = kLevel1Size / 2;
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
return texture;
}
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
// every assertion reads.
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
format.transferFormat, format.transferType, texels.data());
for (int level = 1; level <= 2; ++level) {
const int size = std::max(kFlatSize >> level, 1);
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
format.transferFormat, format.transferType, fill.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
const PackedFormatCase& format, size_t texelCount) {
std::vector<GLushort> words(texelCount, 0);
glBindTexture(target, texture);
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
glBindTexture(target, 0);
return words;
}
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
// image equals the corresponding source word, and every word outside it still holds
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
// regression into a one-line diagnosis.
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
const std::vector<GLushort>& fill, const char* what) {
for (int y = 0; y < width; ++y) {
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
const bool inRegion =
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
const GLushort actual = got[static_cast<size_t>(y * width + x)];
const GLushort expected =
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
(x - x0))]
: fill[static_cast<size_t>(y * width + x)];
EXPECT_EQ(actual, expected)
<< what << ": texel (" << x << ", " << y << ")"
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
<< ", expected 0x" << expected;
if (actual != expected) return; // one texel names the defect; 224 more would bury it
}
}
}
std::vector<GLuint> m_textures;
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
};
const PackedFormatCase kFormats[] = {
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
};
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
// the array-as-destination direction of 12 of the 18 failing bodies.
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint source = MakeFlatTexture(format, flat);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
(std::string("2d->2d_array level 1, ") + format.name).c_str());
// The source must not have moved - the CTS asserts this before it ever looks at
// the destination, and it is what pins the corruption to the copy itself.
const auto sourceAfter =
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
(std::string("source after 2d->2d_array, ") + format.name).c_str());
}
}
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint destination = MakeFlatTexture(format, fill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
}
}
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
// that requires the renderbuffer's ES storage to move together with the textures' -
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
// reached textures alone would break exactly here.
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const GLuint array = MakeArrayTexture(format, level0, level1);
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< format.name << ": the renderbuffer is not attachable";
// Field values picked to encode exactly in the narrow fields AND in their
// UNorm8 expansions, so the expected word is the same whichever storage the
// configuration picked - which is the point of the whole scenario.
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
static_cast<GLfloat>(max - 2) / max, 1.0f};
// The context is shared with every scenario in this process; a scissor left on
// would clip the clear and hand the copy undefined renderbuffer texels.
glDisable(GL_SCISSOR_TEST);
glClearBufferfv(GL_COLOR, 0, clearColor);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
GLushort clearedWord = 0;
switch (format.transferType) {
case GL_UNSIGNED_SHORT_5_6_5:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
break;
case GL_UNSIGNED_SHORT_5_5_5_1:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
break;
case GL_UNSIGNED_SHORT_4_4_4_4:
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
break;
default:
break;
}
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
}
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,238 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
//
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
// the capability IS there both backends translate the factors properly, and that has always
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
// application asking for a blend factor the device cannot do is a picture problem, never a reason
// to take the process down.
//
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
// scenario pins that it lands on one of them and never on a crash:
//
// capability present - src0 * src1 + dst * (1 - src1)
// capability absent - src0, written straight through
//
// What each CI lane actually reaches: lavapipe has dualSrcBlend, so the DirectVulkan lane runs the
// whole sequence and measures the blend. Mesa's GLES front end on llvmpipe has no
// GL_EXT_blend_func_extended, so the ESSL stage carrying `layout(index = 1)` never compiles and the
// program renders nothing - the DirectGLES lane therefore SKIPS on the capability probe in SetUp
// rather than measuring a picture the driver never produced. The DECLINE arm itself - the path this
// scenario exists for - is unit-tested against stubbed capabilities in
// MG_Test/Framebuffer/FramebufferTest.cpp (DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing),
// which is the only place it can be reached without a driver that lacks the extension.
//
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
// would still be invalid pipeline state.
#include <cstdint>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kExtent = 16;
constexpr const char* kVertexSource = R"(#version 330 core
void main()
{
switch (gl_VertexID)
{
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
}
}
)";
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
// harness's compile-and-link helper.
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
uniform vec4 uSrc0;
uniform vec4 uSrc1;
layout(location = 0, index = 0) out vec4 fragColor0;
layout(location = 0, index = 1) out vec4 fragColor1;
void main()
{
fragColor0 = uSrc0;
fragColor1 = uSrc1;
}
)";
class DualSourceBlendScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
std::string error;
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
m_programError = error;
glViewport(0, 0, kExtent, kExtent);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// Capability probe, not an assertion. A GL link that succeeded is not proof that
// the BACKEND can run the program: DirectGLES transpiles to ESSL lazily at first
// use, and GLSL ES has no `index` layout qualifier outside
// GL_EXT_blend_func_extended, so on a driver without it the stage never compiles
// and the draw renders nothing. One unblended white draw tells the two apart, and
// the cases skip rather than measure a picture the driver never produced.
if (m_program != 0) {
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/1.0f, /*src1=*/1.0f);
glFinish();
const Image probe = ReadPixels(kExtent, kExtent);
m_programRenders =
!probe.Empty() && static_cast<int>(probe.At(kExtent / 2, kExtent / 2).r) > 245;
}
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
void TearDown() override {
if (!Ready()) return;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
}
void Draw(float src0, float src1) {
glUseProgram(m_program);
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
// Both cases share this gate: nothing below can be measured on a backend that cannot
// run a dual-source fragment program at all. Returns the skip reason, empty when the
// program runs - NOT a void helper that calls GTEST_SKIP itself, because GTEST_SKIP
// expands to a `return` and would leave only the HELPER, letting the case run its
// assertions anyway and report Failed instead of Skipped.
std::string WhyTheProgramCannotRun() const {
if (m_program == 0) {
return "this driver cannot build a dual-source fragment shader: " + m_programError;
}
if (!m_programRenders) {
return "this backend links a dual-source fragment program but renders nothing with it "
"(GLSL ES has no `index` layout qualifier without GL_EXT_blend_func_extended)";
}
return {};
}
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
GLuint m_vao = 0;
unsigned int m_program = 0;
bool m_programRenders = false;
std::string m_programError;
};
} // namespace
// The whole point of the scenario: this sequence used to be a process kill on any device
// without the capability, and it has to be a picture either way.
//
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
// declined = 1.0 -> 255
// Anything else means the factors were mistranslated rather than either honoured or declined.
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
glFinish();
glDisable(GL_BLEND);
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
const int red = static_cast<int>(centre.r);
const bool blended = red > 100 && red < 160;
const bool declined = red > 245;
EXPECT_TRUE(blended || declined)
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
<< "straight-through source (255) - the SRC1 factors were mistranslated";
Gl().EndFrame();
}
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
// source either way, so this case is really "no crash, no error, no surprise".
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyTheProgramCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glDisable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
glFinish();
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
const Image image = ReadPixels(kExtent, kExtent);
ASSERT_FALSE(image.Empty());
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
EXPECT_GT(static_cast<int>(centre.r), 245)
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
Gl().EndFrame();
}
} // namespace MGITest
@@ -137,7 +137,7 @@ namespace MGITest {
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
// the pack itself replays through the MagmaFixIterationRPSubgroupScratch patch, which
// this probe deliberately does not model. The width only gates the domain;
// lane placement and group counts still come from observed values alone.
bool SubgroupWidthInSourceDomain() const {
@@ -0,0 +1,278 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - MESH-ARENA-SIZED BUFFERS, END TO END.
//
// A buffer store of at least 16MiB is adopted into the backend's persistently and
// coherently mapped GPU storage the moment it is defined (BufferObject::
// TryAdoptLargeStorage): the CPU shadow is dropped and every later write lands
// directly in GPU-visible memory with no per-write driver call. Minecraft 26.3
// streams chunk meshes into 128MB vertex arenas with plain glNamedBufferSubData -
// on Mali, every driver-mediated route for that write into a busy mutable store
// either parks the calling thread or ghost-copies the whole arena on a driver
// worker (~167ms per touched arena: the recurring in-world hiccup this adoption
// removed). Every existing buffer scenario uses stores far below the threshold,
// so without this file the adopted path would have zero coverage.
//
// What is pinned, deliberately through the same API mix Minecraft uses:
// * a glBufferSubData written AFTER the arena was drawn (in flight) reaches the
// next draw - the write-visibility contract adoption must not weaken;
// * GetBufferSubData reads back the latest CPU write - the shadow IS the map;
// * a compute-shader write through an SSBO binding of the same arena is read
// back - the GPU-written path for adopted stores (glFinish + direct read).
#include <array>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Comfortably past the 16MiB adoption threshold, and the vertex payload sits
// deep inside the store so an implementation that quietly clamped or aliased
// the adopted range would miss it.
constexpr GLsizeiptr kArenaBytes = GLsizeiptr(24) * 1024 * 1024;
constexpr GLintptr kVertexOffset = GLintptr(20) * 1024 * 1024;
constexpr const char* kVertexSource = R"(#version 430 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec3 a_color;
out vec3 v_color;
void main() {
v_color = a_color;
gl_Position = vec4(a_pos, 0.0, 1.0);
}
)";
constexpr const char* kFragmentSource = R"(#version 430 core
in vec3 v_color;
out vec4 o_color;
void main() { o_color = vec4(v_color, 1.0); }
)";
constexpr const char* kMarkerComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Arena { uint word; };
void main() { word = 0xC0FFEEu; }
)";
struct Vertex {
float x, y;
float r, g, b;
};
// A full-viewport quad, colored uniformly so one center readback speaks for
// the whole draw.
std::vector<Vertex> QuadVertices(float r, float g, float b) {
return {
{-1.f, -1.f, r, g, b}, {1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b},
{-1.f, -1.f, r, g, b}, {1.f, 1.f, r, g, b}, {-1.f, 1.f, r, g, b},
};
}
class LargeArenaAdoptionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_program = LinkProgram(kVertexSource, kFragmentSource);
ASSERT_NE(m_program, 0u) << m_buildLog;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_arena);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
// The NULL-data definition is the adoption point (and Minecraft's
// arena-creation idiom).
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(kVertexOffset + 2 * sizeof(float)));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_arena != 0) glDeleteBuffers(1, &m_arena);
if (m_program != 0) glDeleteProgram(m_program);
if (m_compute != 0) glDeleteProgram(m_compute);
m_vao = 0;
m_arena = 0;
m_program = 0;
m_compute = 0;
}
unsigned int CompileStage(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
return shader;
}
unsigned int LinkProgram(const char* vs, const char* fs) {
const GLuint v = CompileStage(GL_VERTEX_SHADER, vs);
if (v == 0) return 0;
const GLuint f = CompileStage(GL_FRAGMENT_SHADER, fs);
if (f == 0) {
glDeleteShader(v);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, v);
glAttachShader(program, f);
glLinkProgram(program);
glDeleteShader(v);
glDeleteShader(f);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_buildLog = std::string("program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
void UploadQuad(float r, float g, float b) {
const auto vertices = QuadVertices(r, g, b);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
}
void DrawQuad() {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
std::array<unsigned char, 4> CenterPixel() {
std::array<unsigned char, 4> px = {0, 0, 0, 0};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
px.data());
return px;
}
unsigned int m_program = 0;
unsigned int m_compute = 0;
unsigned int m_vao = 0;
unsigned int m_arena = 0;
std::string m_buildLog;
};
} // namespace
// The Minecraft shape: the arena is drawn, the frame retires, and a
// glBufferSubData rewrites the SAME vertex bytes while the previous frame's
// draw may still be in flight. The next draw must show the NEW bytes.
TEST_F(LargeArenaAdoptionScenario, SubDataAfterAnInFlightDrawReachesTheNextDraw) {
if (!Ready() || IsSkipped()) return;
UploadQuad(1.f, 0.f, 0.f);
DrawQuad();
auto px = CenterPixel();
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(px[0], 200) << "the first draw from the adopted arena never landed";
EXPECT_LT(px[1], 50);
Gl().EndFrame();
UploadQuad(0.f, 1.f, 0.f);
DrawQuad();
px = CenterPixel();
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(px[1], 200) << "the cross-frame rewrite of the adopted arena did not reach the draw; "
"the old color means the write went to bytes the draw no longer reads";
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
}
// The shadow IS the mapping: a readback straight after a CPU write must hand
// back exactly those bytes.
TEST_F(LargeArenaAdoptionScenario, ReadbackSeesTheLatestCpuWrite) {
if (!Ready() || IsSkipped()) return;
const auto vertices = QuadVertices(0.25f, 0.5f, 0.75f);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
std::vector<Vertex> read(vertices.size());
glGetBufferSubData(GL_ARRAY_BUFFER, kVertexOffset,
GLsizeiptr(read.size() * sizeof(Vertex)), read.data());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(0, std::memcmp(read.data(), vertices.data(), read.size() * sizeof(Vertex)))
<< "GetBufferSubData of the adopted arena returned different bytes than the SubData wrote";
}
// A GPU write through an SSBO binding of the adopted arena must be visible to
// a CPU readback - the path that waits out the GPU and reads the coherent
// mapping directly.
TEST_F(LargeArenaAdoptionScenario, GpuWriteIntoTheArenaIsReadBack) {
if (!Ready() || IsSkipped()) return;
GLint maxComputeStorageBlocks = 0;
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
if (maxComputeStorageBlocks < 1) {
GTEST_SKIP() << "no compute shader storage blocks on this driver";
}
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
ASSERT_NE(compute, 0u) << m_buildLog;
m_compute = glCreateProgram();
glAttachShader(m_compute, compute);
glLinkProgram(m_compute);
glDeleteShader(compute);
GLint linked = 0;
glGetProgramiv(m_compute, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
const unsigned int seed = 0u;
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(seed), &seed);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
glUseProgram(m_compute);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
unsigned int marker = 0;
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(marker, 0xC0FFEEu)
<< "the compute write into the adopted arena did not reach the CPU readback";
}
} // namespace MGITest
@@ -32,14 +32,20 @@
// too, so the per-slice branch that exists for exactly this case was unreachable and every
// slice above z = 0 came back VK_NULL_HANDLE.
//
// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
// The first seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array
// with its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and
// 1D ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
// not merely survival: what a layer receives is a function of its own index, so an attachment that
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
// never landed here" reads differently from "the wrong layer landed here".
//
// Case (8) is the same collapse one step downstream, and case (6) is what found it: the READBACK
// of a cube map ignored the face it was asked for and answered +X for all six. Every case here
// that reads a layered target back depends on the readback addressing the layer it names, so it
// belongs beside them - and case (6) had to be written around it, which is the strongest argument
// there is that it was never pinned.
//
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
@@ -96,6 +102,10 @@ namespace MGITest {
// mismatch means a real miss rather than rounding.
constexpr Rgba8 kClearColor{17, 68, 187, 255};
// The six cube faces in the order GL numbers them, which is also the order Vulkan keeps
// them in as array layers (GL 4.6 core 8.5.3 / VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT).
const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
// says which layer it came from) and b names the PASS (so "the second draw was not
// rejected" is distinguishable from "the first draw never happened").
@@ -357,6 +367,27 @@ void main()
return texture;
}
// A cube map whose six faces are UPLOADED with their own colours - the same
// ExpectedColor(face, 0) the painted cube of case (8) ends up holding, so both can be
// checked with one expectation. Uploaded rather than rendered means the CPU shadow and
// the image agree, which is the premise the BY-NAME readback needs; see case (8).
GLuint MakeFaceColoredCubeMap() {
const GLuint texture = TrackTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (int face = 0; face < 6; ++face) {
const std::vector<Rgba8> seed(static_cast<std::size_t>(kExtent) * kExtent,
ExpectedColor(face, 0));
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return texture;
}
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
// gives 1 however many layers there are.
@@ -487,6 +518,34 @@ void main()
}
}
// Every texel of one cube FACE is that face's own colour. When it is not, the message
// says whose colour answered instead - which is the whole point here: a readback that
// ignores the face token does not return garbage, it returns another face's perfectly
// plausible texels, and "+X's colour came back for -Y" is the sentence that names the
// defect. `what` is the spelling under test, since three of them read the same faces.
void ExpectFaceColor(const std::vector<Rgba8>& texels, int face, const char* what) {
const Rgba8 expected = ExpectedColor(face, 0);
for (std::size_t i = 0; i < texels.size(); ++i) {
const Rgba8 actual = texels[i];
if (actual == expected) continue;
std::string blame;
if (actual.r == kPoison && actual.g == kPoison) {
blame = " - the poison, so nothing was ever written to this face";
} else {
for (int other = 0; other < 6; ++other) {
if (other != face && actual == ExpectedColor(other, 0)) {
blame = std::string(" - which is face ") + kFaceNames[other] + "'s colour";
break;
}
}
}
ADD_FAILURE() << what << ": face " << kFaceNames[face] << " texel " << i << " is "
<< Describe(actual) << ", expected " << Describe(expected) << blame;
// One message per face is enough to say what happened.
break;
}
}
::testing::AssertionResult FramebufferIsComplete() {
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
@@ -883,13 +942,16 @@ void main()
// Every face, read back through an FBO that names THAT face.
//
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
// where only +X had been cleared, that spelling returned the cleared colour for all
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
// else, and the pending clear is long gone by now (materialised and popped above), so
// this readback cannot alter what it is measuring.
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): when this case was written
// that spelling could not see per-face state on DirectVulkan at all - measured against
// a tree where only +X had been cleared it returned the cleared colour for all six
// faces - so a case built on it would have been unfalsifiable. That is a readback
// defect rather than an attachment one, and case (8) below is where it is pinned and
// fixed; this case keeps the independent spelling deliberately, because it must go on
// measuring the CLEAR whatever the readback does. glFramebufferTexture2D +
// glReadPixels names one face and nothing else, and the pending clear is long gone by
// now (materialised and popped above), so this readback cannot alter what it is
// measuring.
for (int face = 0; face < 6; ++face) {
const GLuint faceFbo = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
@@ -958,5 +1020,114 @@ void main()
Gl().EndFrame();
}
// (8) THE CUBE FACE TOKEN A READBACK IS GIVEN, AND WHETHER IT HONOURS IT.
//
// Case (6) above had to route around glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face)
// entirely: measured against a tree where only the +X face had been cleared, that spelling
// returned +X's colour for all six face tokens. This case is that observation turned into
// an assertion, and it is about the READBACK, not the attachment.
//
// THE DEFECT. DirectVulkan's GetTextureImage derived its copy geometry from the IMAGE's
// target alone. A plain GL_TEXTURE_CUBE_MAP is not one of the array targets, so the layer
// count collapsed to one - correct, one face IS one layer - but nothing ever turned the
// face the TARGET TOKEN named into the copy's baseArrayLayer, which stayed 0. All six face
// tokens therefore read array layer 0 and answered +X: five of a cube map's six faces were
// unreadable through the entry point GL provides for reading them. Nothing announces it -
// the call succeeds, raises no error, and hands back entirely plausible texels from the
// wrong face. The conversion it was missing already existed twice over, as the clear and
// render-pass managers' ResolveAttachmentBaseArrayLayer.
//
// glGetTextureSubImage is the same question asked by name: GL 4.6 core 8.11.4 addresses a
// cube map's faces through zoffset. That spelling was not merely reading the wrong face,
// it could not read ANY face - measured pre-fix, all six returned INVALID_OPERATION on
// both backends. Two independent reasons, and it took both to make even zoffset 0 fail:
// the z range was measured against the level's z, which is one face's 1, so five of the
// six looked like a partial read; and the destination-size check summed all six faces, so
// the one face's worth of buffer a single-face read has any reason to pass was rejected as
// too small.
//
// Each face is painted its OWN colour, so a collapse onto layer 0 does not merely read
// "wrong": the failure names the face that answered. The cube is poisoned first and then
// painted through the GPU, so an answer served from the stale CPU shadow is also called out
// by name rather than passing. And the per-face FBO + glReadPixels read is the control: it
// names one face and nothing else, so if IT disagrees the defect is in how the faces were
// written and this case is measuring the wrong thing.
//
// DirectGLES attaches the named face to a scratch FBO and reads that, so it answers the
// face token correctly throughout - a red there means this case is wrong. Its by-name
// readback is a different matter and gets a texture of its own; see the third block.
TEST_F(LayeredAttachmentShapeScenario, CubeMapFaceReadbackAnswersTheFaceItWasAskedFor) {
if (!Ready()) return;
const GLuint cube = MakePoisonedCubeMap();
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
// Paint every face its own colour through an FBO that names that one face. A clear
// rather than a draw, so nothing here depends on a shader stage being present.
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glViewport(0, 0, kExtent, kExtent);
GLuint faceFbos[6] = {};
for (int face = 0; face < 6; ++face) {
faceFbos[face] = TrackFramebuffer();
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
glReadBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
const Rgba8 want = ExpectedColor(face, 0);
glClearColor(want.r / 255.0f, want.g / 255.0f, want.b / 255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "painting the six faces errored";
// The control. If this is red, the faces do not hold six different values and the two
// readbacks below are being measured against a premise that is not true.
for (int face = 0; face < 6; ++face) {
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
EXPECT_EQ(FirstGLError(), 0u) << "the control read of face " << kFaceNames[face] << " errored";
ExpectFaceColor(texels, face, "control: per-face FBO + glReadPixels");
}
// The subject: the face TOKEN.
glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
for (int face = 0; face < 6; ++face) {
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
glGetTexImage(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, GL_RGBA,
GL_UNSIGNED_BYTE, texels.data());
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage of face " << kFaceNames[face] << " errored";
ExpectFaceColor(texels, face, "glGetTexImage(GL_TEXTURE_CUBE_MAP_<face>)");
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
// The same question by name, where zoffset is the face.
//
// On a cube map UPLOADED face by face rather than the painted one above, because the
// by-name readback has no backend entry outside DirectVulkan and answers from the CPU
// shadow there - a separate, pre-existing gap that has nothing to do with which face
// gets read. Asking it about GPU-painted content would make this red on DirectGLES for
// a reason the case is not about; asking it about uploaded content leaves exactly one
// thing either backend can get wrong, which is the face. DirectVulkan still answers
// this one out of the image, so the layer collapse is just as visible here.
const GLuint uploaded = MakeFaceColoredCubeMap();
ASSERT_EQ(FirstGLError(), 0u) << "uploading the six faces failed";
for (int face = 0; face < 6; ++face) {
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
glGetTextureSubImage(uploaded, 0, 0, 0, face, kExtent, kExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
static_cast<GLsizei>(texels.size() * sizeof(Rgba8)), texels.data());
EXPECT_EQ(FirstGLError(), 0u) << "glGetTextureSubImage of face " << kFaceNames[face] << " errored";
ExpectFaceColor(texels, face, "glGetTextureSubImage(zoffset = face)");
}
Gl().EndFrame();
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,269 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - THE MOBILEGL_PIPE_VERIFY COMPARATOR IS ARMED, AND SAYS SO, AND CAN GO RED.
//
// The third CI mode (ARCHITECTURE.md 13.2-(2)) runs the whole integration suite with two state
// models in one address space: the PipeInputs block the frontend fills at every verb boundary,
// and a SnapshotFromGLContext() taken from the live GLContext. A green run of that mode is only
// worth something if the comparator was actually RUNNING - and "MOBILEGL_PIPE_VERIFY=1 against a
// library that was not built with -DMOBILEGL_PIPE_VERIFY=ON" is a no-op that looks exactly like a
// clean pass. That is the failure mode this scenario exists to make impossible:
//
// Armed - the environment says the comparator is on for this process, so the
// library must SAY it armed. It asserts a library observable against
// the environment, the same shape UnlocatedIoBlockScenario's arming
// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration
// use. A lane whose library never armed FAILS here; it never passes.
// CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With
// MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is
// perturbed before the entry compare, so a comparator that works must
// report Fatal{PipeVerifyDiffer, "<Field>@<Verb>"}. A comparator that
// compares nothing stays quiet and this case goes red.
//
// The observable is the library's own log, because MG_Config is not reachable from this module
// (on Android it links the SHIPPING libMobileGL.so, built -fvisibility=hidden) and the arming
// signal is a latched MGLOG_I. The ctest entry sets MOBILEGL_LOG_FILE_PATH; this only reads it.
//
// Note on scope, and why Armed runs in a lane of its own. The log file is opened with
// fopen(path, "w") at the first log write of a process (MG_Util/Debug/Log.cpp, InitFile), so each
// process TRUNCATES it. That is fine for one process and false for many: in the ambient Verify.
// lane, 400-odd sibling entries share the one MOBILEGL_LOG_FILE_PATH, and CI runs that lane with
// `ctest -j 4`, so a neighbour's bring-up can truncate the file between this case's draw and its
// read. Every existing scenario in this suite that reads the library log (UnlocatedIoBlockScenario,
// the primgen reroute, the point-size demotion) is registered in a FILTERED lane with a log path of
// its own for exactly that reason, and this case now follows them: it runs in the VerifyArming.
// entries, which set MGITEST_PIPE_ARMING_LANE=1 and their own log, and skips everywhere else.
//
// What that proves, stated honestly: the arming line is a property of (this library, this
// environment), not of an individual test body, and the VerifyArming. entry runs the same library
// with the same MOBILEGL_PIPE_VERIFY=1 as its ~400 ambient siblings. One process per backend is
// therefore the whole of the evidence available for "the lane armed" - the per-process claim the
// shared log CANNOT support, because it only ever holds the last writer.
//
// Within the process: the arming line is latched at the FIRST fill, which may be the harness
// bring-up rather than this test's draw, so the arming search is whole-file on purpose; the
// divergence search is restricted to the bytes this case appended, which is where a differ belongs.
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// The three strings the comparator contracts to print (the brief's D8 reporting shape).
// They are spelled here once so a rename of either half is one compile-visible edit.
constexpr const char* kArmedLine = "MGPipe: verify armed";
constexpr const char* kDifferPrefix = "Fatal{PipeVerifyDiffer";
constexpr const char* kUnmigratedPrefix = "Fatal{UnmigratedPipeInput";
// Set by the VerifyArming. ctest entries and by nothing else. It is a HARNESS variable, not
// a library knob (hence the MGITEST_ prefix): the library never reads it. It exists because
// this case reads a log file, and a log file is a per-LANE resource - see the note at the
// top of the file.
constexpr const char* kArmingLaneMarker = "MGITEST_PIPE_ARMING_LANE";
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFS = R"(#version 330 core
out vec4 o_color;
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
)";
// Reads the environment the way MG_ConfigLoader does (ScenarioFixture.h documents the
// rule); a string knob is "set" when it is present and non-empty, which is exactly what
// MG_ConfigLoader's QueryEnvVariable turns into a non-empty Features member.
bool StringKnobIsSet(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && *value != '\0';
}
class PipeVerifyArmingScenario : public ScenarioTest {
protected:
// The library log this process is writing, or an empty path when none was configured.
static std::filesystem::path LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
: std::filesystem::path();
}
static std::uintmax_t LibraryLogSize() {
std::error_code ec;
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return 0;
const std::uintmax_t size = std::filesystem::file_size(path, ec);
return ec ? 0 : size;
}
static std::string LibraryLogSince(std::uintmax_t offset) {
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
file.seekg(static_cast<std::streamoff>(offset));
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}
static std::string LibraryLog() { return LibraryLogSince(0); }
// One frame that crosses several verb boundaries: a clear (kClear), a draw (kDraw) and
// a readback (kReadback). Three of the nine fill classes, so an entry compare that only
// ran for one of them still has something to say.
void DrawOneFrame() {
HeadlessGL& gl = Gl();
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0;
GLuint vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
BindDefaultFramebuffer();
glViewport(0, 0, gl.Width(), gl.Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
Rgba8 pixel{};
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
m_centre = pixel;
}
Rgba8 m_centre{};
};
// THE CASE THAT FAILS A LANE WHOSE LIBRARY NEVER ARMED.
//
// Every other entry in the integration-verify lane renders the same frames it renders in the
// ambient lane and would be just as green against a library with no comparator compiled in -
// which is precisely how a verify lane goes green having verified nothing. This case is the
// one that cannot: the environment pins MOBILEGL_PIPE_VERIFY=1, therefore the library must
// have said "MGPipe: verify armed" in its own log, and if it did not, the mode is not running.
TEST_F(PipeVerifyArmingScenario, Armed) {
if (!Ready()) return;
if (!StringKnobIsSet(kArmingLaneMarker)) {
GTEST_SKIP() << "this case reads the library's log file, so it runs in the VerifyArming. "
"lane, which owns a log path no other entry writes to. In the ambient "
"Verify. lane 400-odd entries share one path and each truncates it "
"(Log.cpp opens it \"w\"), so a whole-file read here would race a "
"neighbour under ctest -j 4. Set by the ctest entry, never by hand.";
}
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
GTEST_SKIP() << "this case needs MOBILEGL_PIPE_VERIFY=1 for the whole process, which is "
"what the Verify. ctest entries set; with the variable unset the "
"comparator is dormant even in a build that compiled it in";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY is pinned on but MOBILEGL_LOG_FILE_PATH is not "
"set, so the library has nowhere to record that it armed; the Verify. "
"ctest entries set both";
}
if (StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is armed in this process, so a divergence "
"is the EXPECTED outcome and asserting on its absence here would be "
"backwards; the VerifyCorrupted. lane owns that half";
}
const std::uintmax_t before = LibraryLogSize();
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
EXPECT_EQ(FirstGLError(), 0u);
// Whole file, not just the appended bytes: the arming line is latched at the FIRST fill
// of the process, which may already have happened during the harness bring-up. The file
// is truncated at this process's first log write, so it still carries nothing else.
const std::string whole = LibraryLog();
EXPECT_NE(whole.find(kArmedLine), std::string::npos)
<< "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and "
"read back, and the library never reported arming the comparator. Either this "
"library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane "
"is verifying nothing), or the arming MGLOG_I is gone. Log:\n"
<< whole;
const std::string appended = LibraryLogSince(before);
EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos)
<< "the comparator reported a push/pull divergence on an ordinary frame:\n"
<< appended;
EXPECT_EQ(appended.find(kUnmigratedPrefix), std::string::npos)
<< "a backend read a field the verb's fill table does not list (add the row to "
"MG_Pipe/FillPoints.def, never mark the field sticky):\n"
<< appended;
}
// NEGATIVE CONTROL A (gate G4): a deliberately corrupted snapshot field must turn a green
// verify run red, naming that field and the verb it diverged on.
//
// It runs in its own lane (VerifyCorrupted.) because the knob is process-wide, and with
// MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and this case can
// read the report back out of the log. The CI step that runs the SAME knob against the
// ambient lane - where FATAL keeps its default - asserts the other half: there, the
// divergence must abort and ctest must go red.
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReported) {
if (!Ready()) return;
if (!StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
GTEST_SKIP() << "this case is the comparator's negative control and needs "
"MOBILEGL_PIPE_VERIFY_CORRUPT=<FieldName> for the whole process, which "
"is what the VerifyCorrupted. ctest entries set";
}
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is set but MOBILEGL_PIPE_VERIFY is not, so "
"the comparator is dormant and there is nothing to corrupt";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to report "
"the divergence; the VerifyCorrupted. ctest entries set both";
}
const std::string knob = std::getenv("MOBILEGL_PIPE_VERIFY_CORRUPT");
const std::uintmax_t before = LibraryLogSize();
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
const std::string appended = LibraryLogSince(before);
const std::string expected = std::string(kDifferPrefix) + ", \"" + knob + "@";
EXPECT_NE(appended.find(expected), std::string::npos)
<< "MOBILEGL_PIPE_VERIFY_CORRUPT=" << knob
<< " perturbs that field in the snapshot arm before every entry compare, so a working "
"comparator must have reported " << expected << "...\". It reported nothing, which "
"means the comparator is not comparing - and every green entry in this lane is "
"green for no reason. Log appended by this case:\n"
<< appended;
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,522 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PointSizeDemotionScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - THE gl_PointSize DEMOTION IS CLIENT-INVISIBLE, AND IT ACTUALLY ARMS.
//
// On a device that hosts the built-in in tessellation/geometry stages (llvmpipe and
// lavapipe both do), gl_PointSize travels as itself; on one that does not (the Mali
// devices this exists for), phase B demotes it to an ordinary varying
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram) and the capture
// machinery follows it there. This scenario runs in BOTH configurations and must hand
// back identical bytes: the ambient registrations take the native path, and the
// PointSizeDemotion. registrations pin MOBILEGL_POINT_SIZE_DEMOTION=1 so the demotion
// runs on the same healthy drivers - CopyImagePacked16Scenario's dual-configuration
// contract, applied to a value chain instead of a storage format.
//
// The VALUE is the whole contract: every case writes gl_PointSize in one stage, reads it
// back out of gl_in[] in the next, and captures it by name under rasterizer discard, so
// one wrong link anywhere in VS -> TCS -> TES -> GS -> capture lands in the readback.
// The RASTERIZED size is deliberately not asserted anywhere: with the built-in unhosted
// it falls back to 1.0 by spec on both targets, which is exactly the honest residue the
// demotion documents (point_rendering-style bodies keep failing truthfully).
//
// The assertions are on the captured BYTES against a CPU-computed reference, never on
// the absence of a GL error: every failure this guards against is silent.
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr float kPoison = -987654.0f;
const char* const kFragmentSource = R"(#version 460 core
layout(location = 0) out vec4 fragColor;
void main()
{
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
// The full chain, with per-vertex VARIATION seeded in the vertex stage so a control
// invocation that read or wrote the wrong slot changes the sum: 2,3,4 arrive, 3,4,5
// leave, the evaluation stage sums its patch to 12, the geometry stage doubles what
// it read to 24.
const char* const kChainVertexSource = R"(#version 460 core
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 2.0 + float(gl_VertexID);
}
)";
const char* const kChainTessControlSource = R"(#version 460 core
layout(vertices = 3) out;
void main()
{
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
}
)";
const char* const kChainTessEvalSource = R"(#version 460 core
layout(triangles, equal_spacing, cw, point_mode) in;
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize;
}
)";
const char* const kChainGeometrySource = R"(#version 460 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main()
{
gl_Position = gl_in[0].gl_Position;
gl_PointSize = gl_in[0].gl_PointSize * 2.0;
EmitVertex();
EndPrimitive();
}
)";
// The geometry-only chain: no tessellation required of the stack at all.
const char* const kPointVertexSource = R"(#version 460 core
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 7.0;
}
)";
const char* const kPointGeometrySource = R"(#version 460 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main()
{
gl_Position = gl_in[0].gl_Position;
gl_PointSize = gl_in[0].gl_PointSize + 1.0;
EmitVertex();
EndPrimitive();
}
)";
// A capture stage that only READS the incoming point size and never writes its own.
// Legal GL, and the shape that separates "the demotion arms" from "the demotion knows
// a capture is coming": with the built-in gone, only the capture request can put a
// carrier back for a by-name capture to bind to.
const char* const kReadOnlyGeometrySource = R"(#version 460 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float g_echo;
void main()
{
gl_Position = gl_in[0].gl_Position;
g_echo = gl_in[0].gl_PointSize;
EmitVertex();
EndPrimitive();
}
)";
const char* const kEchoFragmentSource = R"(#version 460 core
in float g_echo;
layout(location = 0) out vec4 fragColor;
void main()
{
fragColor = vec4(g_echo, 0.0, 0.0, 1.0);
}
)";
class PointSizeDemotionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
DrainErrors();
}
void TearDown() override {
if (Ready()) {
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
const std::vector<const char*>& varyings) {
m_buildLog.clear();
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
ok = false;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()),
varyings.data(), GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
program = 0;
}
}
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (program != 0) m_programs.push_back(program);
return program;
}
// One capture span over `vertexCount` vertices of `drawMode`, recorded as
// GL_POINTS. The buffer is poison-filled first so bytes the capture never wrote
// name themselves.
std::vector<float> RunCaptureSpan(GLuint program, GLenum drawMode, GLsizei vertexCount,
std::size_t capturedFloats) {
const std::vector<float> poison(capturedFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
poison.data(), GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(drawMode, 0, vertexCount);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> readback(capturedFloats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)),
readback.data());
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
return readback;
}
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data,
std::size_t index, float expected,
float epsilon = 1e-4f) {
if (index >= data.size()) {
return ::testing::AssertionFailure()
<< "component " << index << " is past the capture buffer";
}
const float actual = data[index];
if (actual == kPoison) {
return ::testing::AssertionFailure()
<< "component " << index << " still holds the poison value - the capture "
<< "never reached these bytes (expected " << expected << ")";
}
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
return ::testing::AssertionFailure()
<< "component " << index << " is " << actual << ", expected " << expected;
}
return ::testing::AssertionSuccess();
}
// The library log, for the arming case. Same machinery and same reasoning as
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, the file
// is appended to by every process in the lane, and only bytes appended after the
// snapshot may satisfy an assertion.
static std::filesystem::path LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
: std::filesystem::path();
}
static std::uintmax_t LibraryLogSize() {
std::error_code ec;
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return 0;
const std::uintmax_t size = std::filesystem::file_size(path, ec);
return ec ? 0 : size;
}
static std::string LibraryLogSince(std::uintmax_t offset) {
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
file.seekg(static_cast<std::streamoff>(offset));
return std::string((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
std::string m_buildLog;
private:
GLuint m_vao = 0;
std::vector<GLuint> m_programs;
};
// The five-stage chain. 24.0 can only arrive if the vertex mirror, both control-stage
// redirects (read AND write), the evaluation stage's three gl_in reads and the
// geometry stage's read all carried the right value - one wrong link and the sum
// moves. point_mode with every level at 1 emits three points; the first record proves
// the mechanism, exactly as TessellationXfbCaptureScenario reasons.
TEST_F(PointSizeDemotionScenario, TheValueSurvivesTheFiveStageChainIntoTheCapture) {
if (!Ready()) return;
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 3);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
{GL_GEOMETRY_SHADER, kChainGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
EXPECT_TRUE(ComponentIs(captured, 0, 24.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The same chain without a geometry stage: the capture then binds to the evaluation
// stage's value (the sum, 12.0) - which is also the boundary where a demoted program
// switches its capture carrier from the Io chain to the capture name.
TEST_F(PointSizeDemotionScenario, TheEvaluationStageOwnsTheCaptureWithoutAGeometryStage) {
if (!Ready()) return;
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 3);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kChainVertexSource},
{GL_TESS_CONTROL_SHADER, kChainTessControlSource},
{GL_TESS_EVALUATION_SHADER, kChainTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
const std::vector<float> captured = RunCaptureSpan(program, GL_PATCHES, 3, 3);
EXPECT_TRUE(ComponentIs(captured, 0, 12.0f));
// The GL query surface keeps the truthful spelling whatever the backends renamed
// underneath: reflection is a phase-A product and the demotion happens after it.
char varyingName[64] = {};
GLsizei nameLength = 0;
GLsizei varyingSize = 0;
GLenum varyingType = 0;
glGetTransformFeedbackVarying(program, 0, sizeof(varyingName), &nameLength, &varyingSize,
&varyingType, varyingName);
EXPECT_STREQ(varyingName, "gl_PointSize");
EXPECT_EQ(varyingType, static_cast<GLenum>(GL_FLOAT));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The geometry-only chain: gl_in[0].gl_PointSize read straight off the vertex stage,
// no tessellation involved - the VS -> GS boundary of the demotion on its own.
TEST_F(PointSizeDemotionScenario, AGeometryOnlyChainCarriesTheVertexValue) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
{GL_GEOMETRY_SHADER, kPointGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// THE CAPTURE-REQUEST PATH, END TO END - the half no unit test can reach, because the
// request travels from glTransformFeedbackVaryings through phase A's resolved capture
// set and the phase-B handoff before it reaches the demotion.
//
// The geometry stage READS gl_in[0].gl_PointSize and never writes gl_PointSize, which
// is enough to arm the demotion (glslang declares GeometryPointSize on a read) but not
// enough to create an output carrier on its own. Only the capture request can, and if
// that request never arrives the program does not merely lose the point-size column:
// DirectGLES respells the driver-side capture to a name no stage declares and the
// WHOLE capture set fails to link, while DirectVulkan mirrors a built-in the demotion
// just removed and can unwind far enough to drop the Xfb execution mode. Either way
// g_echo - an ordinary varying with nothing to do with point size - comes back poison,
// which is what this asserts. gl_PointSize itself is captured but never asserted: no
// stage writes it, so GL leaves its value undefined.
TEST_F(PointSizeDemotionScenario, ACaptureSurvivesAStageThatOnlyReadsThePointSize) {
if (!Ready()) return;
// The NATIVE Espryt path cannot do this at all, and never could: with the built-in
// hosted, the geometry stage's ESSL simply does not declare gl_PointSize unless it
// writes it, so the driver rejects the capture request with "varying undeclared"
// and the program becomes unusable. That is a pre-existing ES limitation the
// demotion happens to REPAIR - the carrier is a real, seeded, declared varying -
// so this case has something to assert only where the demotion is armed. Magma
// consumes SPIR-V and answers on both paths, which keeps the negative control.
if (Gl().BackendName() == "DirectGLES" &&
AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
GTEST_SKIP() << "Espryt cannot capture a gl_PointSize its capture stage never "
"writes without the demotion; the PointSizeDemotion. ctest entry "
"runs this same case with MOBILEGL_POINT_SIZE_DEMOTION=1";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
{GL_GEOMETRY_SHADER, kReadOnlyGeometrySource},
{GL_FRAGMENT_SHADER, kEchoFragmentSource}},
{"g_echo", "gl_PointSize"});
ASSERT_NE(program, 0u)
<< "the capture set failed to link. On a demoting configuration this is the "
"capture request never reaching the demotion, so the point-size capture was "
"respelled to a carrier no stage declares. Build log: "
<< m_buildLog;
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 2);
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f))
<< "the unrelated varying captured alongside gl_PointSize did not survive; the "
"point-size capture took the whole set with it";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// THE ONE CASE THAT CAN FAIL WHEN THE DEMOTION SILENTLY STOPS BEING ARMED.
//
// Everything above captures the right bytes on llvmpipe and lavapipe whether the
// demotion ran or not - these machines host the built-in - so those cases pin that
// the demotion does no HARM and can say nothing about whether it happened. The
// arming is where the cheap mistake lives: MOBILEGL_POINT_SIZE_DEMOTION maps onto
// the two Supports*PointSize capability bits INVERTED (forcing the demotion on
// means declaring the built-in UNHOSTED), and a swap of those arms - or a dropped
// env bit anywhere between ConfigLoader, the backend init, CompileEnv and the L1
// key - would disable the device repair with every rendering case still green.
//
// Same machinery as UnlocatedIoBlockScenario's arming case: the environment says
// the demotion is pinned on, therefore the library must SAY it demoted something.
// The observable is the latched MGLOG_I each backend emits when it first builds a
// demoted program; both spell "demoted to an ordinary varying", so this one case
// covers both pinned lanes without a backend gate.
TEST_F(PointSizeDemotionScenario, TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_POINT_SIZE_DEMOTION") != AmbientQuirk::On) {
GTEST_SKIP() << "this case needs the demotion pinned ON for the whole process, which "
"is what the PointSizeDemotion. ctest entries do with "
"MOBILEGL_POINT_SIZE_DEMOTION=1; with the variable unset the detected "
"capabilities decide, and on this machine the built-in is hosted - so "
"there would be nothing to observe";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_POINT_SIZE_DEMOTION is pinned on but MOBILEGL_LOG_FILE_PATH "
"is not set, so the library has nowhere to record that it demoted "
"anything; the PointSizeDemotion. ctest entries set both";
}
// Taken BEFORE the program is built, so the line this looks for can only be one
// this process wrote.
const std::uintmax_t before = LibraryLogSize();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPointVertexSource},
{GL_GEOMETRY_SHADER, kPointGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
// Drawn as well as built, so a stack that defers its backend program to first
// use still reaches the build the latched line fires in - and the capture must
// STILL be right through the carrier.
const std::vector<float> captured = RunCaptureSpan(program, GL_POINTS, 1, 1);
EXPECT_TRUE(ComponentIs(captured, 0, 8.0f))
<< "the pinned-on lane did not even capture correctly";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
const std::string appended = LibraryLogSince(before);
EXPECT_NE(appended.find("demoted to an ordinary varying"), std::string::npos)
<< "MOBILEGL_POINT_SIZE_DEMOTION is pinned ON, a geometry program reading and "
"writing gl_PointSize was built and captured, and no backend ever reported "
"demoting it. The demotion is not armed - check the override mapping in the "
"backend inits (it is inverted on purpose), the CompileEnv accessors, and "
"ProgramSpirvTask's verdict plumbing. Log appended by this test:\n"
<< appended;
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,413 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PoisonOmissionScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - NEGATIVE CONTROL B (gate G5): AN OMITTED FILL POINT ABORTS ON THAT VERB, AND ONLY THERE.
//
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
// still copying the value, which is indistinguishable from a row that was never written.
//
// The scenario asserts both halves of "on THAT verb, and only there":
//
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
// mask, and the draw's own fields are stamped normally) and the
// following glGenerateMipmap must abort naming exactly that pair.
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
// no Fatal at all. Without this half, "it aborted" would say nothing
// about WHY: a poison that fired on every verb would look just as red.
//
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
// entries, the second in the ambient Verify. entries (it skips when the knob IS set).
//
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
// sequence cannot run in the test process, and the harness's own bring-up pre-flight
// (Harness/HeadlessGL.cpp) already establishes the shape: run it where a SIGABRT is a datum in
// waitpid() instead of a dead lane. But that pre-flight forks BEFORE any context exists, and this case
// cannot: the fixture has already brought one up. A bare fork() of a process holding a live Vulkan
// device inherits the driver's mutexes with no threads to release them, and the child wedges on its
// first submit - measured here as a 120s timeout on DirectVulkan and a clean pass on DirectGLES, which
// is exactly the kind of backend-shaped flake a control must not have. So the child immediately
// execve()s a fresh copy of this same test binary, filtered to the worker case below, which brings up
// its own context from scratch and knows nothing about the parent's.
//
// The child gets its OWN MOBILEGL_LOG_FILE_PATH for the same reason: the library opens its log with
// fopen(path, "w"), so a child sharing the parent's path would truncate the file the parent is about
// to read.
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
#define MGITEST_POISON_HAVE_FORK 1
#include <csignal>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
extern char** environ;
#else
#define MGITEST_POISON_HAVE_FORK 0
#endif
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// What the PoisonOmitted. ctest entry and the CI negative-control step name. The pair is
// spelled here so the assertion below is about the exact string the poison contracts to
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
constexpr const char* kOmittedVerb = "GenerateMipmap";
constexpr const char* kOmittedField = "GetActiveTextureUnit";
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
// Set only in the re-executed child, so the worker case below runs in that process and skips
// everywhere else (including in the ambient lanes, where it is registered like any other case).
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
constexpr const char* kWorkerFilter =
"--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFS = R"(#version 330 core
out vec4 o_color;
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
)";
bool StringKnobIsSet(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && *value != '\0';
}
std::filesystem::path LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
: std::filesystem::path();
}
// Where the child is told to write ITS log. Empty when the lane configured no log path at
// all, in which case the signal is the only evidence and the text assertions are skipped.
std::string ChildLogPath() {
const std::filesystem::path parent = LibraryLogPath();
if (parent.empty()) return {};
return (parent.string() + ".poison-child");
}
std::string ReadWholeFile(const std::string& path) {
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}
class PoisonOmissionScenario : public ScenarioTest {
protected:
// The sequence under test. Deliberately in this order: the DRAW comes first and must
// survive - if the poison fired there, the "only that verb" half would be false and the
// SIGABRT the parent waits for would prove nothing.
void RunSequence() {
HeadlessGL& gl = Gl();
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
// A two-level texture, so glGenerateMipmap has real work to do and cannot be
// short-circuited into a no-op by a backend that inspects the level count first.
GLuint texture = 0;
glGenTextures(1, &texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
unsigned char pixels[8 * 8 * 4];
for (std::size_t i = 0; i < sizeof(pixels); ++i) {
pixels[i] = static_cast<unsigned char>(i);
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0;
GLuint vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
BindDefaultFramebuffer();
glViewport(0, 0, gl.Width(), gl.Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glFinish();
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glFinish();
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
// The sequence is the WHOLE datum this child reports, so a GL error in it must be
// part of the answer rather than something only a human reading stderr would see.
// WithoutOmissionCompletes reads the child's exit status, and the status is built
// from HasFailure() below - so this EXPECT is what turns "the mipmap was rejected"
// into a red parent instead of a vacuous "it exited 0, the poison did not fire".
EXPECT_EQ(FirstGLError(), 0u)
<< "the draw + glGenerateMipmap sequence the poison controls are about raised a "
"GL error, so neither control is measuring what it claims to measure";
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteTextures(1, &texture);
}
#if MGITEST_POISON_HAVE_FORK
// fork() + execve() of this same binary, filtered to the worker case, with the marker and
// the child's own log path added to the environment. Everything that allocates happens
// BEFORE the fork; between fork and execve only async-signal-safe work is done.
static bool RunSequenceInAChildProcess(int& outStatus, std::string& outReason) {
std::vector<std::string> env;
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
const std::string text(*entry);
if (text.rfind("MOBILEGL_LOG_FILE_PATH=", 0) == 0) continue;
if (text.rfind(std::string(kChildMarker) + "=", 0) == 0) continue;
env.push_back(text);
}
env.push_back(std::string(kChildMarker) + "=1");
const std::string childLog = ChildLogPath();
if (!childLog.empty()) {
std::error_code ec;
std::filesystem::remove(childLog, ec);
env.push_back("MOBILEGL_LOG_FILE_PATH=" + childLog);
}
std::vector<char*> envp;
envp.reserve(env.size() + 1);
for (std::string& entry : env) envp.push_back(entry.data());
envp.push_back(nullptr);
std::string exe = "/proc/self/exe";
std::string arg0 = "MobileGLIntegrationTest";
std::string filter = kWorkerFilter;
char* argv[] = {arg0.data(), filter.data(), nullptr};
std::fflush(nullptr);
const pid_t child = fork();
if (child < 0) {
outReason = "fork() failed";
return false;
}
if (child == 0) {
execve(exe.c_str(), argv, envp.data());
// execve only returns on failure; _exit, never exit(), because every atexit
// handler in this address space belongs to the parent's copy of the world.
std::fprintf(stderr, "[itest] poison child: execve(/proc/self/exe) failed\n");
_exit(127);
}
constexpr int kTimeoutMs = 120000;
int waitedMs = 0;
for (;;) {
const pid_t reaped = waitpid(child, &outStatus, WNOHANG);
if (reaped == child) return true;
if (reaped < 0) {
outReason = "waitpid on the poison worker failed";
return false;
}
if (waitedMs >= kTimeoutMs) {
kill(child, SIGKILL);
(void)waitpid(child, &outStatus, 0);
outReason = "the poison worker made no progress in 120s and was killed";
return false;
}
timespec nap{0, 10 * 1000 * 1000};
nanosleep(&nap, nullptr);
waitedMs += 10;
}
}
static std::string DescribeStatus(int status) {
if (WIFEXITED(status)) return "exited with status " + std::to_string(WEXITSTATUS(status));
if (WIFSIGNALED(status)) return "died on signal " + std::to_string(WTERMSIG(status));
return "ended in an unrecognised way";
}
#endif
};
// The worker. It is a normal registered case so that the re-executed child can be selected
// with nothing but --gtest_filter, and it skips in every process that is not that child.
TEST_F(PoisonOmissionScenario, TheSequenceThePoisonControlsRun) {
if (std::getenv(kChildMarker) == nullptr) {
GTEST_SKIP() << "this case is the body the two poison controls run in a child process; "
"it does nothing unless " << kChildMarker << " is set, which only the "
"re-exec below does";
}
if (!Ready()) return;
RunSequence();
#if MGITEST_POISON_HAVE_FORK
// _exit, and not a return into gtest's teardown: this process exists to reach the verb
// above and its exit status is the datum the parent reads. A normal teardown of a live
// context could add signals of its own to that answer.
//
// HasFailure(), not 0: RunSequence() is full of ASSERT_/EXPECT_ macros, and a fatal one
// (the shader failing to compile, say) RETURNS from RunSequence before the draw and the
// glGenerateMipmap ever happen. Exiting 0 there would have WithoutOmissionCompletes pass
// on a child that ran none of the sequence it is the control for - green because nothing
// happened. The child's assertion text is on its stderr, which ctest captures.
std::fflush(nullptr);
_exit(::testing::Test::HasFailure() ? 1 : 0);
#endif
}
#if MGITEST_POISON_HAVE_FORK
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
if (!Ready()) return;
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
GTEST_SKIP() << "this case is the poison's negative control and needs "
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
"is what the PoisonOmitted. ctest entries set";
}
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
const std::string expectedPair = std::string(kOmittedField) + "@" + kOmittedVerb;
if (knob != std::string(kOmittedVerb) + ":" + kOmittedField) {
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
<< "how to provoke " << kOmittedVerb << ":" << kOmittedField;
}
int status = 0;
std::string reason;
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
const std::string childLog = ReadWholeFile(ChildLogPath());
ASSERT_TRUE(WIFSIGNALED(status))
<< "with the stamp of " << expectedPair << " omitted, the glGenerateMipmap in the child "
<< "had to read a field its verb never filled and abort. It " << DescribeStatus(status)
<< " instead - the poison is not armed (a build without MOBILEGL_PIPE_POISON, a filler "
"that stamps what it was told to skip, or a backend that no longer reads the field "
"through the accessor). Child log:\n"
<< childLog;
EXPECT_EQ(WTERMSIG(status), SIGABRT)
<< "the child died on signal " << WTERMSIG(status) << " rather than SIGABRT; the poison "
"reports through MGLOG_F + std::abort(), so any other signal is a different crash. "
"Child log:\n"
<< childLog;
if (ChildLogPath().empty()) {
GTEST_SKIP() << "the abort happened, but the lane set no MOBILEGL_LOG_FILE_PATH, so the "
"Fatal's text cannot be read back; the PoisonOmitted. ctest entries set it";
}
EXPECT_NE(childLog.find(std::string(kFatalPrefix) + ", \"" + expectedPair + "\""),
std::string::npos)
<< "the child aborted, but not with Fatal{UnmigratedPipeInput, \"" << expectedPair
<< "\"} - that message is the whole diagnostic value of the poison. Child log:\n"
<< childLog;
EXPECT_EQ(childLog.find("@DrawArrays"), std::string::npos)
<< "the draw that ran BEFORE the omitted verb also tripped the poison, so the omission "
"is not scoped to its verb: the fill classes are wrong, or the stamps are global. "
"Child log:\n"
<< childLog;
}
// The sibling control, in the ambient Verify. lanes: the same sequence with the knob UNSET
// must run to completion and log no Fatal at all.
//
// It deliberately does NOT skip when MOBILEGL_PIPE_POISON_OMIT is set. This is the entry
// CI's always-on negative control B exports the knob at: a green entry that the omission
// turns red is the whole proof that the poison is armed, and an entry that politely skipped
// itself would report that green either way. Nothing else in the integration suite calls
// glGenerateMipmap, so this case is also the only possible target for that control.
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
"build there is nothing for this control to be a control OF";
}
const bool omissionArmed = StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT");
int status = 0;
std::string reason;
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
const std::string childLog = ReadWholeFile(ChildLogPath());
const std::string note =
omissionArmed
? std::string(
" NOTE: MOBILEGL_PIPE_POISON_OMIT is set in this process, so this failure is "
"what CI's negative control B is asking for - the poison IS armed, and this "
"entry going red is the proof.")
: std::string();
ASSERT_TRUE(WIFEXITED(status))
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
<< DescribeStatus(status)
<< ". If it aborted, the poison is firing on a field the verb's fill table SHOULD list - "
"add the row to MG_Pipe/FillPoints.def, never mark the field sticky."
<< note << " Child log:\n"
<< childLog;
EXPECT_EQ(WEXITSTATUS(status), 0)
<< "the child " << DescribeStatus(status)
<< ". Status 1 is the child's OWN assertion failing inside the sequence (it exits "
"HasFailure() ? 1 : 0), so its gtest output on this job's stderr names the line; "
"anything else came from the harness. Child log:\n"
<< childLog;
EXPECT_EQ(childLog.find("Fatal{"), std::string::npos)
<< "an unpoisoned run logged a Fatal:\n"
<< childLog;
}
#else
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
"a datum; this platform has none of them";
}
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
"a datum; this platform has none of them";
}
#endif
} // namespace
} // namespace MGITest
@@ -0,0 +1,554 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitivesGeneratedNoXfbScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GL_PRIMITIVES_GENERATED COUNTS DRAWS MADE WITH TRANSFORM FEEDBACK
// INACTIVE.
//
// GL 4.6 core 13.4: the query counts what the last vertex processing stage emits,
// capture or no capture. The CTS leans its whole tessellation suite on that - the
// tessellator's output is MEASURED by an XFB-inactive PATCHES draw under
// rasterizer discard inside a GENERATED query, and the capture buffers of ~29
// tessellation tests are sized from the answer - so a backend that answers 0
// hands them a zero-byte buffer and an INVALID_OPERATION off its zero-length map.
//
// DirectVulkan serves the query from the transform-feedback stream query's
// primitivesNeeded, which VK_EXT_transform_feedback defines to count whether or
// not a capture span is open. Both the Mali-G1-Ultra driver AND Mesa lavapipe
// disagree with that definition: with no vkCmdBeginTransformFeedbackEXT recorded,
// the pair reads back 0. Where the bring-up probe measures that defect with a
// working control - or MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1 pins it on - the
// renderer accumulates XFB-inactive draws through the best proven substitute
// pool: VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT (which lavapipe hosts and passes,
// rasterizer discard included), else pipeline statistics over clipping-stage
// invocations (GL's CLIPPING_INPUT_PRIMITIVES). These cases assert the GL-visible
// answer, so on this machine they hold the reroute to the same numbers the
// healthy stream path must produce - the "two pools must agree" assertion - and
// on a healthy driver they pin the stream path itself.
//
// DirectVulkan only: DirectGLES has no GPU counter for an XFB-inactive draw at
// all (ES has no PRIMITIVES_GENERATED without a capture), and its CPU accounting
// is a different mechanism with its own tests.
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
GLuint CompileShaderStage(GLenum type, const char* source, std::string* log) {
const GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// A capture-capable vertex-only program: the varying gives glBeginTransformFeedback
// something to capture for the mixed-span case; the XFB-inactive cases draw with the
// same program and simply never begin a span.
const char* const kVertexSource = R"(#version 430 core
out vec4 vs_out_value;
void main() {
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
vs_out_value = vec4(1.0);
gl_Position = vec4(corners[gl_VertexID % 3], 0.0, 1.0);
}
)";
// A passthrough tessellation pipeline whose all-1 levels emit exactly one
// triangle per patch - the count the tessellation cases assert.
const char* const kTessVertexSource = R"(#version 430 core
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kTessControlSource = R"(#version 430 core
layout(vertices = 1) out;
void main() {
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
}
)";
const char* const kTessEvalSource = R"(#version 430 core
layout(triangles, equal_spacing, cw) in;
void main() {
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
}
)";
// The same tessellation pipeline with something to capture, so that
// glBeginTransformFeedback accepts it: the paused-span PATCHES case needs an
// open (but paused) capture span AND a tessellator in one program.
const char* const kTessEvalCaptureSource = R"(#version 430 core
layout(triangles, equal_spacing, cw) in;
out vec4 te_out_value;
void main() {
te_out_value = vec4(1.0);
gl_Position = vec4(gl_TessCoord.xy * 2.0 - 1.0, 0.0, 1.0);
}
)";
class PrimitivesGeneratedNoXfbScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() != std::string("DirectVulkan")) {
GTEST_SKIP() << "the stream-query defect and its reroute are DirectVulkan's; "
<< Gl().BackendName()
<< " answers this query from a different mechanism";
}
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenQueries(2, m_queries);
ASSERT_NE(m_queries[0], 0u);
ASSERT_NE(m_queries[1], 0u);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
if (m_queries[0] != 0 || m_queries[1] != 0) glDeleteQueries(2, m_queries);
m_queries[0] = m_queries[1] = 0;
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
ScenarioTest::TearDown();
}
// captureVarying: the name to record with glTransformFeedbackVaryings, or
// nullptr for a program that can never open a capture span.
GLuint BuildProgram(std::initializer_list<std::pair<GLenum, const char*>> stages,
const char* captureVarying) {
std::vector<GLuint> shaders;
for (const auto& [type, source] : stages) {
const GLuint shader = CompileShaderStage(type, source, &m_buildLog);
if (shader == 0) {
for (const GLuint built : shaders) glDeleteShader(built);
return 0;
}
shaders.push_back(shader);
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) glAttachShader(program, shader);
if (captureVarying != nullptr) {
glTransformFeedbackVaryings(program, 1, &captureVarying, GL_INTERLEAVED_ATTRIBS);
}
glLinkProgram(program);
for (const GLuint shader : shaders) glDeleteShader(shader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
m_buildLog = buffer.data();
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
GLuint BuildCaptureProgram() {
return BuildProgram({{GL_VERTEX_SHADER, kVertexSource}}, "vs_out_value");
}
GLuint BuildTessellationProgram(bool withCaptureVarying = false) {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTessGenLevel < 1) return 0;
return BuildProgram(
{{GL_VERTEX_SHADER, kTessVertexSource},
{GL_TESS_CONTROL_SHADER, kTessControlSource},
{GL_TESS_EVALUATION_SHADER,
withCaptureVarying ? kTessEvalCaptureSource : kTessEvalSource}},
withCaptureVarying ? "te_out_value" : nullptr);
}
// A capture span that is open but PAUSED. The pause closes the capture, so
// every draw inside it is XFB-inactive at the backend - the stream query's
// silent case - while the GL span stays active. `program` must be the one
// that is bound: GL requires the same program at resume.
void BeginPausedSpan() {
glGenBuffers(1, &m_captureBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_captureBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBeginTransformFeedback(GL_TRIANGLES);
glPauseTransformFeedback();
}
void EndPausedSpan() {
glResumeTransformFeedback();
glEndTransformFeedback();
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
if (m_captureBuffer != 0) glDeleteBuffers(1, &m_captureBuffer);
m_captureBuffer = 0;
}
// GENERATED query around `record()`, answered with GL_QUERY_RESULT.
GLuint QueryGenerated(const std::function<void()>& record) {
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
record();
glEndQuery(GL_PRIMITIVES_GENERATED);
GLuint generated = 0xFFFFFFFFu;
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, &generated);
return generated;
}
static GLenum DrainGLErrors() {
const GLenum first = glGetError();
while (glGetError() != GL_NO_ERROR) {
}
return first;
}
const std::string& BuildLog() const { return m_buildLog; }
static std::filesystem::path LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
: std::filesystem::path();
}
static std::uintmax_t LibraryLogSize() {
std::error_code ec;
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return 0;
const std::uintmax_t size = std::filesystem::file_size(path, ec);
return ec ? 0 : size;
}
static std::string LibraryLogSince(std::uintmax_t offset) {
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
file.seekg(static_cast<std::streamoff>(offset));
return std::string((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
GLuint m_vao = 0;
GLuint m_queries[2] = {0, 0}; // [0]=written, [1]=generated
GLuint m_captureBuffer = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
// The plain shape: no capture object was ever bound, no span begun, no
// rasterizer discard - just a GENERATED query around two triangles. On a
// healthy driver the stream query answers it; on an affected one the armed
// reroute must produce the same 2.
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsADrawMadeWithNoCaptureSpan) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
EXPECT_EQ(DrainGLErrors(), 0u);
EXPECT_EQ(generated, 2u)
<< "GL_PRIMITIVES_GENERATED must count a draw made while transform feedback is "
"inactive (GL 4.6 core 13.4)";
}
// THE CTS SHAPE (esextcTessellationShaderUtils.cpp, captureTessellationData):
// rasterizer discard ON, transform feedback INACTIVE, the draw inside a
// GENERATED query. This is the exact query whose 0 sizes ~29 tessellation
// tests' capture buffers on the affected device.
//
// On lavapipe this case holds through the dedicated
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT reroute (its discard feature is
// what makes a discarded draw countable there - llvmpipe's clipping
// statistics AND stream query both read 0 under discard).
//
// The value-conditioned skip below is deliberate and narrow, for a stack
// with NO counter that survives discard: there this case is unfalsifiable,
// and a red would indict MobileGL for a hole the bring-up probe already
// measures and reports (StatisticsSubstitutePlainOnly / Unfixable). The
// exact-zero answer IS the capability signal - any wrong nonzero count
// still fails - and on every driver that counts discarded draws at all the
// full assertion runs. The device probe list holds this shape on the Mali.
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsUnderRasterizerDiscardWithNoCaptureSpan) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 6); });
glDisable(GL_RASTERIZER_DISCARD);
EXPECT_EQ(DrainGLErrors(), 0u);
if (generated == 0u) {
GTEST_SKIP() << "no counter this backend can reach (stream query, dedicated "
"primitives-generated query, clipping statistics) survives "
"rasterizer discard for an XFB-inactive draw on this stack - the "
"shape is unfalsifiable here; the bring-up probe measures the same "
"hole and the POST row reports it";
}
EXPECT_EQ(generated, 2u)
<< "rasterizer discard drops primitives after clipping and must not hide them from "
"GL_PRIMITIVES_GENERATED - this is the exact shape the CTS measures the "
"tessellator with";
}
// The tessellation flavour: a PATCHES draw whose all-1 levels emit exactly
// one triangle - the count the CTS's getAmountOfVerticesGeneratedByTessellator
// protocol derives everything from. Undiscarded, so that the answer is
// holdable on this machine through whichever accounting path is armed (the
// discard interaction is the case above's business, measured separately).
TEST_F(PrimitivesGeneratedNoXfbScenario, CountsATessellatedPatchWithNoCaptureSpan) {
if (!Ready()) return;
const GLuint program = BuildTessellationProgram();
if (program == 0) {
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
}
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
EXPECT_EQ(DrainGLErrors(), 0u);
EXPECT_EQ(generated, 1u)
<< "a triangles-domain patch with every level 1 tessellates to exactly one "
"triangle, and GL_PRIMITIVES_GENERATED must say so with no capture active";
}
// One query span holding BOTH kinds of draw: an XFB-inactive draw, then a
// captured one, then another XFB-inactive one. The GENERATED answer must
// accumulate across the two accounting paths the armed reroute splits them
// into (stream slots for the captured draw, statistics slots for the
// others), and WRITTEN must stay exactly the captured draw's count - the
// pairing the stream path exists to keep exact. Undiscarded, so the
// accumulation invariant is holdable on this machine (see the discard
// case's comment); the triangles rasterize into the harness framebuffer,
// which nothing here reads.
TEST_F(PrimitivesGeneratedNoXfbScenario, ASpanMixingActiveAndInactiveDrawsAccumulatesBoth) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
GLuint captureBuffer = 0;
glGenBuffers(1, &captureBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 3 * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
const GLuint generated = QueryGenerated([]() {
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive
glBeginTransformFeedback(GL_TRIANGLES);
glDrawArrays(GL_TRIANGLES, 0, 3); // captured
glEndTransformFeedback();
glDrawArrays(GL_TRIANGLES, 0, 3); // XFB inactive again
});
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
GLuint written = 0xFFFFFFFFu;
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, &written);
glDeleteBuffers(1, &captureBuffer);
EXPECT_EQ(DrainGLErrors(), 0u);
EXPECT_EQ(generated, 3u) << "one triangle before the span, one inside it, one after";
EXPECT_EQ(written, 1u) << "only the draw inside the span writes anything";
}
// ===================== DRAWS INSIDE A PAUSED SPAN =====================
//
// glPauseTransformFeedback closes the capture without closing the span, so a
// draw made while paused is XFB-INACTIVE at the backend - the stream query is
// exactly as silent for it as for a draw with no span at all - while
// GL_PRIMITIVES_GENERATED must still count what the last vertex processing
// stage emitted (GL 4.6 core 13.4; the WRITTEN query is the one the pause
// silences). The frontend does keep a CPU counter for paused draws, but it can
// price only 3 of the ~15 draw entry points and answers 0 for GL_PATCHES, so
// these draws are the reroute's business like any other - and the trap on the
// other side is counting them TWICE, once in each accounting.
//
// Each case measures the SAME draw twice: once with no span open at all (the
// capability control - what this stack can count) and once inside the paused
// span, and requires the two to agree. That differential is what makes these
// cases falsifying rather than vacuous: a stack where no counter reaches a
// capture-less draw fails the control and skips, while a stack that counts the
// unpaused draw and answers 0 for the paused one - which is what excluding
// paused draws from the reroute produced - fails, instead of skipping into
// green.
// The draw the CPU counter CAN price: if the span both reroutes it and adds the
// CPU delta, this reads 2.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsACpuPricedDrawExactlyOnce) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") == AmbientQuirk::Off) {
GTEST_SKIP() << "the negative control replays the pre-probe accounting, whose paused "
"draws are CPU-counted on top of whatever the stream query says";
}
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
"stack, so the paused half of the comparison proves nothing; the "
"bring-up probe measures the same hole and the POST row reports it";
}
EXPECT_EQ(unpaused, 1u) << "the control itself: one triangle is one primitive";
EXPECT_EQ(paused, unpaused)
<< "one triangle drawn while the capture span is paused is still one primitive "
"generated - counted once, by whichever accounting owns it, never by two of them "
"(a reroute slot AND the frontend's CPU paused counter reads 2)";
}
// The draw the CPU counter CANNOT price: GL_PATCHES, whose amplification is not
// knowable on the CPU (CountPrimitivesForDraw answers 0 for it by design) - and
// the CTS's tessellator-measuring shape. Excluding paused draws from the
// reroute left this counted by nothing at all on the affected device.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsATessellatedPatchExactlyOnce) {
if (!Ready()) return;
const GLuint program = BuildTessellationProgram(/*withCaptureVarying=*/true);
if (program == 0) {
GTEST_SKIP() << "no tessellation stages on this stack: " << BuildLog();
}
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
const GLuint unpaused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArrays(GL_PATCHES, 0, 1); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less patch draw "
"on this stack, so the paused half proves nothing; the bring-up probe "
"measures the same hole and the POST row reports it";
}
EXPECT_EQ(unpaused, 1u)
<< "the control itself: a triangles-domain patch with every level 1 tessellates to "
"exactly one triangle";
EXPECT_EQ(paused, unpaused)
<< "pausing the capture does not stop the tessellator from generating that triangle, "
"and the frontend's CPU paused counter answers 0 for GL_PATCHES - so a paused "
"patch draw left out of the reroute is counted by nothing at all";
}
// The other half of the same hole: the instanced entry points never reach the
// frontend's paused accounting either, so a paused instanced draw excluded from
// the reroute is likewise counted by nothing.
TEST_F(PrimitivesGeneratedNoXfbScenario, APausedSpanCountsAnInstancedDrawExactlyOnce) {
if (!Ready()) return;
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
const GLuint unpaused =
QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
BeginPausedSpan();
const GLuint paused = QueryGenerated([]() { glDrawArraysInstanced(GL_TRIANGLES, 0, 3, 4); });
EndPausedSpan();
EXPECT_EQ(DrainGLErrors(), 0u);
if (unpaused == 0u) {
GTEST_SKIP() << "no counter this backend can reach answers a capture-less draw on this "
"stack, so the paused half proves nothing";
}
EXPECT_EQ(unpaused, 4u) << "the control itself: four instances of one triangle";
EXPECT_EQ(paused, unpaused)
<< "four instances generate four primitives whether or not the capture span is "
"paused, and no instanced entry point reaches the frontend's paused accounting";
}
// THE ONE CASE THAT CAN FAIL WHEN THE REROUTE SILENTLY STOPS BEING ARMED -
// the UnlocatedIoBlockScenario shape, for the same reason: every case above
// is green here whether the reroute ran or not (that is the "two pools
// agree" point), so none of them can say the pinned lane actually exercised
// a reroute pool. This one asserts a LIBRARY OBSERVABLE against the
// environment: with MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE pinned on, an
// XFB-inactive draw inside a GENERATED span must make the renderer say -
// through its latched MGLOG_I - that it engaged the reroute. It reads
// MG_Config not at all (on Android this module links the shipping library)
// and trusts only the log bytes appended after it started.
TEST_F(PrimitivesGeneratedNoXfbScenario, TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE") != AmbientQuirk::On) {
GTEST_SKIP() << "this case needs the reroute pinned ON for the whole process, which "
"is what the PrimGenReroute. ctest entry does with "
"MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1; unset, the bring-up probe "
"decides and this machine's verdict is its own business";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned on but "
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
"record that it rerouted anything; the PrimGenReroute. ctest "
"entry sets both";
}
const GLuint program = BuildCaptureProgram();
ASSERT_NE(program, 0u) << BuildLog();
glUseProgram(program);
// Taken BEFORE the draw, so the line this looks for can only be one this
// process wrote for this span. The latch fires on the FIRST rerouted
// draw, which is inside the query below.
const std::uintmax_t before = LibraryLogSize();
const GLuint generated = QueryGenerated([]() { glDrawArrays(GL_TRIANGLES, 0, 3); });
EXPECT_EQ(DrainGLErrors(), 0u);
EXPECT_EQ(generated, 1u) << "the pinned-on lane did not even count correctly";
const std::string appended = LibraryLogSince(before);
EXPECT_NE(appended.find("PRIMITIVES_GENERATED reroute engaged"), std::string::npos)
<< "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE is pinned ON, an XFB-inactive draw ran inside "
"a GENERATED query, and the renderer never reported engaging the reroute. The "
"quirk is not armed - check the override mapping "
"(ChoosePrimitivesGeneratedReroute) and the arming gate in "
"VulkanRenderer::BeginXfbQueryForDraw. Log appended by this test:\n"
<< appended;
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,178 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleMaskScopeScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - GL_SAMPLE_MASK IS A MULTISAMPLE FRAGMENT OPERATION, SO IT DOES NOTHING AT ONE SAMPLE.
//
// GL 4.6 core 17.3.3 groups alpha-to-coverage, sample coverage and the sample mask together and
// says they make no change "if MULTISAMPLE is disabled, or if the value of SAMPLE_BUFFERS is not
// one". SAMPLE_BUFFERS is 0 for a single-sample framebuffer, so on one the mask is inert whatever
// glSampleMaski last wrote.
//
// Vulkan has no such rule. VkPipelineMultisampleStateCreateInfo::pSampleMask is ANDed with
// rasterization coverage at every rasterizationSamples, and at one sample that coverage is bit 0
// alone - so a mask with bit 0 clear discards every fragment of every primitive. Plumbing
// glSampleMaski straight into pSampleMask therefore turned an ordinary and legal GL sequence into
// a fully black draw:
//
// glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2); // while an MSAA target is bound
// ... render ...
// glBindFramebuffer(GL_FRAMEBUFFER, 0); draw a fullscreen quad to present
//
// Neither piece of state is per-framebuffer, so nothing resets it when the target changes, and
// dEQP/GL-CTS multisample cases leave exactly these masks behind. That is the MSAA-then-present
// shape every application uses.
//
// The cases below are single-sample by construction (the scenario harness's colour FBO), so each
// one asserts that the mask changed nothing.
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kFboSize = 32;
constexpr const char* kQuadVertexSource = R"(#version 430 core
void main() {
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
gl_Position = vec4(corner, 0.0, 1.0);
}
)";
constexpr const char* kGreenFragmentSource = R"(#version 430 core
out vec4 o_color;
void main() {
o_color = vec4(0.0, 1.0, 0.0, 1.0);
}
)";
class SampleMaskScopeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
glGenVertexArrays(1, &m_vao);
std::string error;
m_program = CompileProgram(kQuadVertexSource, kGreenFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
}
void TearDown() override {
if (!Ready()) return;
// Process-wide GL state: leaving it set would hand the next scenario in this
// process the very bug under test.
glDisable(GL_SAMPLE_MASK);
glSampleMaski(0, 0xFFFFFFFFu);
glBindVertexArray(0);
glUseProgram(0);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DestroyColorFbo(m_target);
ScenarioTest::TearDown();
}
void ExpectQuadStillPaints(const char* what) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindVertexArray(m_vao);
glUseProgram(m_program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the draw raised a GL error";
const Image image = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(image.Empty()) << what << ": the readback came back empty";
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0, what))
<< what << ": an all-black target means the sample mask discarded every fragment, "
<< "which GL says it cannot do on a single-sample framebuffer";
}
ColorFbo m_target{};
GLuint m_vao = 0;
unsigned int m_program = 0;
};
} // namespace
// The exact reported shape: bit 0 clear, so the single sample of a single-sample target is
// masked off if the mask is applied at all.
TEST_F(SampleMaskScopeScenario, AMaskWithBitZeroClearDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x2);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0x2");
}
// Zero is the strongest form of the same thing, and the mask value the CTS's mask_zero cases
// set.
TEST_F(SampleMaskScopeScenario, AZeroMaskDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x0);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK enabled with mask 0");
}
// Control: the same mask word with the capability disabled has never had any effect, so this
// one passed before the fix too. It is here so a regression that ignores the enable bit
// instead of the sample count is still caught.
TEST_F(SampleMaskScopeScenario, ADisabledSampleMaskDoesNotDiscardASingleSampleDraw) {
if (!Ready() || IsSkipped()) return;
glDisable(GL_SAMPLE_MASK);
glSampleMaski(0, 0x0);
ASSERT_EQ(FirstGLError(), 0u) << "setting the sample mask raised a GL error";
ExpectQuadStillPaints("GL_SAMPLE_MASK disabled with mask 0");
}
// The mask is state, not a draw parameter, so a second draw after the first must not inherit
// a pipeline built while the memo word and the payload disagreed. Two draws either side of a
// mask change, both to the same single-sample target, both required to paint.
TEST_F(SampleMaskScopeScenario, ChangingTheMaskBetweenSingleSampleDrawsKeepsBothPainting) {
if (!Ready() || IsSkipped()) return;
glEnable(GL_SAMPLE_MASK);
glSampleMaski(0, 0xFFFFFFFFu);
ExpectQuadStillPaints("first draw, full mask");
glSampleMaski(0, 0x2);
ASSERT_EQ(FirstGLError(), 0u) << "changing the sample mask raised a GL error";
ExpectQuadStillPaints("second draw, mask 0x2");
}
// GL_MAX_SAMPLE_MASK_WORDS must be 1 on both backends: MobileGL stores one word and
// SampleMaski_State raises GL_INVALID_VALUE for any maskNumber above 0, so advertising more
// makes dEQP's per-case gluStateReset - which issues glSampleMaski up to the advertised count
// - fail every case. DirectGLES clamped; DirectVulkan forwarded the raw device limit.
TEST_F(SampleMaskScopeScenario, TheAdvertisedSampleMaskWordCountMatchesWhatSampleMaskiAccepts) {
if (!Ready() || IsSkipped()) return;
GLint words = 0;
glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &words);
ASSERT_EQ(FirstGLError(), 0u) << "querying GL_MAX_SAMPLE_MASK_WORDS raised a GL error";
EXPECT_EQ(words, 1) << "every word below the advertised count must be writable, and only word 0 is";
for (GLint word = 0; word < words; ++word) {
glSampleMaski(static_cast<GLuint>(word), 0xFFFFFFFFu);
EXPECT_EQ(FirstGLError(), 0u) << "glSampleMaski(" << word << ", ...) was refused although "
<< "GL_MAX_SAMPLE_MASK_WORDS advertises " << words << " words";
}
}
} // namespace MGITest
@@ -0,0 +1,231 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampledSetStalenessScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A TEXTURE THAT BECOMES COMPLETE WITHOUT A REBIND MUST RE-ENTER THE SAMPLED SET.
//
// DirectVulkan does not bind a texture GL calls incomplete: it substitutes a fallback so the
// sampler reads (0,0,0,1) instead of losing the draw. That decision is made twice per draw - once
// by CollectSampledTextures, which builds the list SetupDraw syncs, materialises pending clears
// for and transitions to a sampled layout BEFORE the render pass opens, and once by the descriptor
// resolve inside the pass. Both ask SamplesAsIncompleteTexture.
//
// The per-draw memo that lets the first of those be skipped was keyed only on the program, the
// transform flags and the texture BIND generation. Completeness is not a function of any of them:
// it moves on a filter change (glTexParameteri / glSamplerParameteri), on a level-range change,
// and on an upload that fills the mip chain - none of which bind anything. So a texture that went
// incomplete -> complete under a fixed binding kept being answered out of the memo as "not in the
// set", and the work SetupDraw does for the set never happened for it:
//
// * its queued clear was never materialised, so the draw sampled pre-clear content - wrong
// pixels, no validation layer needed, which is what the case below detects; and
// * its layout transition moved into the descriptor resolve, which records
// vkCmdPipelineBarrier inside an already-open render pass whose subpass declares no
// self-dependency - the exact hazard CollectSampledTextures exists to prevent.
//
// The fix adds the sampling-resolution generation to that memo key, which is the counter the
// codebase already maintains for "what a unit resolves to changed without a bind" and which both
// TextureObjectBase::BumpShapeVersion and SamplerObject::BumpVersion move.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kFboSize = 32;
constexpr int kTexSize = 8;
constexpr const char* kQuadVertexSource = R"(#version 430 core
void main() {
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
gl_Position = vec4(corner, 0.0, 1.0);
}
)";
// texelFetch, not texture(): the point is WHICH image is sampled, and a fetch cannot be
// explained away by filtering.
constexpr const char* kSampleFragmentSource = R"(#version 430 core
uniform sampler2D u_tex;
out vec4 o_color;
void main() {
o_color = texelFetch(u_tex, ivec2(0, 0), 0);
}
)";
class SampledSetStalenessScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
glGenVertexArrays(1, &m_vao);
std::string error;
m_program = CompileProgram(kQuadVertexSource, kSampleFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
// The sampled texture: ONE level, and no glTexParameteri at all, so MIN_FILTER
// keeps its initial GL_NEAREST_MIPMAP_LINEAR and GL calls it mipmap-incomplete.
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
std::vector<unsigned char> green(static_cast<std::size_t>(kTexSize * kTexSize * 4), 0);
for (std::size_t i = 0; i < green.size(); i += 4) {
green[i + 1] = 255;
green[i + 3] = 255;
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
green.data());
ASSERT_EQ(FirstGLError(), 0u) << "defining the sampled texture raised a GL error";
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
glUseProgram(0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
if (m_program != 0) glDeleteProgram(m_program);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DestroyColorFbo(m_target);
ScenarioTest::TearDown();
}
// One draw of the fullscreen quad sampling texel (0,0) of whatever unit 0 holds, and
// NO readback. That matters: a readback submits and waits, which ends the command
// buffer and resets the per-draw memos with it - so a case that read back between its
// two draws would never leave a stale entry to catch. The two draws here have to land
// in one recording.
void DrawOnly() {
BindFbo(m_target);
glBindVertexArray(m_vao);
glUseProgram(m_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
const GLint location = glGetUniformLocation(m_program, "u_tex");
if (location != -1) glUniform1i(location, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw raised a GL error";
}
ColorFbo m_target{};
GLuint m_vao = 0;
GLuint m_texture = 0;
unsigned int m_program = 0;
};
} // namespace
// The full sequence, ordered so the ONLY state change between the two draws is the filter.
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenAFilterChangeCompletesTheTexture) {
if (!Ready() || IsSkipped()) return;
// 1. Queue a clear on the texture through an FBO and take it straight back out, with no
// draw in between - the "attach -> clear -> detach" shape that leaves the clear
// pending for whoever samples the texture next.
GLuint clearFbo = 0;
glGenFramebuffers(1, &clearFbo);
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, red);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &clearFbo);
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
// 2. Draw while the texture is still incomplete. The backend substitutes its fallback,
// and the per-draw memo records the resulting sampled set.
DrawOnly();
// 3. Make it complete. No bind, no upload, no program change - one filter write, which is
// exactly the state the old memo key could not see.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "changing the filter raised a GL error";
// 4. Draw again, into the same recording, and only now read back. The texture is in the
// sampled set now, so its queued clear has to be materialised before the pass opens and
// the fetch has to see RED. Reading the green the texture was uploaded with means the
// clear was never materialised, i.e. the texture never entered the set - the stale-memo
// bug. Black means the fallback was still being handed out.
DrawOnly();
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
"the draw after the completeness flip"))
<< "green means the queued clear was never materialised, so the texture never re-entered "
"the sampled set after the filter change; blue means the draw did not happen at all";
}
// The same flip driven from a SAMPLER OBJECT rather than the texture's own parameters. It is
// the other half of what feeds the completeness predicate, it moves the same generation, and
// it likewise binds nothing.
TEST_F(SampledSetStalenessScenario, AQueuedClearIsMaterialisedWhenASamplerObjectCompletesTheTexture) {
if (!Ready() || IsSkipped()) return;
GLuint sampler = 0;
glGenSamplers(1, &sampler);
// Bound BEFORE the first draw, still carrying the mipmapping default, so binding it is
// not what changes between the two draws.
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glBindSampler(0, sampler);
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object raised a GL error";
GLuint clearFbo = 0;
glGenFramebuffers(1, &clearFbo);
glBindFramebuffer(GL_FRAMEBUFFER, clearFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, red);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &clearFbo);
ASSERT_EQ(FirstGLError(), 0u) << "queueing the clear raised a GL error";
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
DrawOnly();
// One parameter write on an ALREADY-BOUND sampler object.
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
ASSERT_EQ(FirstGLError(), 0u) << "changing the sampler filter raised a GL error";
DrawOnly();
const Image afterFlip = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(afterFlip.Empty()) << "the readback came back empty";
EXPECT_TRUE(RegionIsMostly(afterFlip, 0, kFboSize - 1, 0, kFboSize - 1, "red", 0.0,
"the draw after the sampler-object flip"))
<< "green means the queued clear was never materialised after the sampler parameter change";
glBindSampler(0, 0);
glDeleteSamplers(1, &sampler);
}
} // namespace MGITest
@@ -0,0 +1,896 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
//
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
// case here is the reduced form of a conformance body that fails on a device:
//
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
// two backends reach it by completely different routes - DirectGLES has to name a
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
//
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
// user-declared per-vertex interface block, both read back out of gl_in[] by the
// evaluation stage and only then captured. This is the shape of
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
// the family still fails: gl_Position arrives, and everything travelling beside it in
// the same patch does not.
//
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
// absence of a GL error: every failure this guards against is silent.
#include <cmath>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Nothing a capture can legitimately produce, so a component that still reads it
// names the failure instead of looking like an ordinary numeric mismatch.
constexpr float kPoison = -987654.0f;
const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
class TessellationXfbCaptureScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
ScenarioTest::TearDown();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
static GLint MaxPatchVertices() {
GLint value = 0;
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
DrainErrors();
return value;
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
const std::vector<const char*>& varyings) {
m_buildLog.clear();
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
ok = false;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
program = 0;
}
}
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (program != 0) m_programs.push_back(program);
return program;
}
// One capture span over a single patch. Returns the capture buffer read back as
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
const std::vector<float> poison(capturedFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(captureMode);
glDrawArrays(GL_PATCHES, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> readback(capturedFloats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
return readback;
}
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
float expected, float epsilon = 1e-4f) {
if (index >= data.size()) {
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
}
const float actual = data[index];
if (actual == kPoison) {
return ::testing::AssertionFailure()
<< "component " << index << " still holds the poison value - the capture never reached "
<< "these bytes (expected " << expected << ")";
}
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
return ::testing::AssertionFailure()
<< "component " << index << " is " << actual << ", expected " << expected;
}
return ::testing::AssertionSuccess();
}
// Defined below the shader builders it uses. `withPointSize` is the conformance
// body's own should_pass_pointsize_data axis.
void RunPerVertexPayloadCase(bool withPointSize);
// Why the gl_PointSize cases cannot be run here, or empty when they can.
//
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
// has no query that reports either, so this probes for it by running a program.
//
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
// compares that against the identical program without the write. A backend that
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
// comes back untouched too - which is a capability answer, not a capture answer. If
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
// the cases run and FAIL rather than skipping on an unrelated breakage.
//
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
// a `return`, so a void helper would leave only the helper and let the case run its
// assertions anyway and report Failed instead of Skipped.
std::string WhyPointSizeCasesCannotRun();
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
// extensions are independent (Loader models them as two PointSizeTier fields fed by
// four distinct strings, and neither implies the other), so a driver with
// tessellation point size and no geometry point size passes the probe above and
// still cannot run the case below. Same two-program shape, one stage over.
//
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
// a hardcoded frontend constant (256) with no capability behind it, so "does this
// stack have a geometry stage at all" can only be answered by trying to build one -
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
// reason.
std::string WhyGeometryPointSizeCaseCannotRun();
std::vector<GLuint> m_programs;
std::string m_buildLog;
GLuint m_vao = 0;
};
// ---------------------------------------------------------------------------------
// Built-ins captured BY NAME from the evaluation stage.
// ---------------------------------------------------------------------------------
const char* const kMinimalVertexSource = R"(#version 420 core
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kMinimalTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
void main()
{
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
}
)";
// Values no stale buffer would hold by accident. The two sources differ ONLY by
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
// built-in is not even declared in a tessellation stage without
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
const char* const kPositionTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
void main()
{
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
}
)";
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
void main()
{
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
gl_PointSize = 5.0;
}
)";
// The two probe programs. They differ by one statement; both capture `probe_value`,
// which has nothing to do with point size.
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
out float probe_value;
void main()
{
probe_value = 42.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 3.0;
}
)";
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw, point_mode) in;
out float probe_value;
void main()
{
probe_value = 42.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const auto probeCaptures = [&](const char* tessEvalSource) {
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"probe_value"});
if (program == 0) return false;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
DrainErrors();
return captured[0] == 42.0f;
};
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
if (withPointSize) return {};
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
// The control failed too, so nothing here is about point size.
return {};
}
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
"program captures an ordinary varying with the gl_PointSize write removed and captures "
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
"Vulkan device without shaderTessellationAndGeometryPointSize)";
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
// point_mode with every level at 1 emits three points, all carrying the same
// constant; only the first record has to be right for the mechanism to be proven.
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position", "gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------
// The per-vertex payload the control stage hands to the evaluation stage.
// ---------------------------------------------------------------------------------
// The conformance body's own shapes, reduced to one patch and parameterised by the
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
// which it varies together with point_mode - and which decides whether the whole
// program even involves the per-vertex built-in that ESSL gates behind an extension.
std::string PayloadVertexSource(bool withPointSize) {
return R"(#version 420 core
out gl_PerVertex {
vec4 gl_Position;
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
R"(};
void main()
{
}
)";
}
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
return R"(#version 420 core
layout(vertices = )" + std::to_string(outputVertices) +
R"() out;
in gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_in[gl_MaxPatchVertices];
out gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_out[];
out OUT_TC
{
vec2 value1;
ivec4 value2;
} result[];
void main()
{
)" + std::string(withPointSize
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
: "") +
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
gl_InvocationID + 3, gl_InvocationID + 4);
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
}
)";
}
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
return R"(#version 420 core
layout(isolines, equal_spacing, ccw, point_mode) in;
in gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(} gl_in[gl_MaxPatchVertices];
out gl_PerVertex {
vec4 gl_Position;
)" + perVertexTail +
R"(};
in OUT_TC
{
vec2 value1;
ivec4 value2;
} tc_data[];
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
R"(out vec4 te_position;
out vec2 te_value1;
out flat ivec4 te_value2;
void main()
{
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
R"( te_position = vec4 (0.0);
te_value1 = vec2 (0.0);
te_value2 = ivec4(0);
for (int n = 0; n < )" + std::to_string(inputVertices) +
R"(; ++n)
{
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
R"( te_position += gl_in [n].gl_Position;
te_value1 += tc_data[n].value1;
te_value2 += tc_data[n].value2;
}
}
)";
}
// The reduced conformance body. `withPointSize` selects between its two halves;
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
// evaluation stage - is the same on both.
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
if (withPointSize) {
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
}
const GLint patchVertices = MaxPatchVertices();
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
// the control stage runs that many invocations and every one of them contributes.
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
if (withPointSize) varyings.push_back("te_pointsize");
const GLuint program =
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
{GL_FRAGMENT_SHADER, kFragmentSource}},
varyings);
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
float referencePointSize = 0.0f;
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
float referenceValue1[2] = {0.0f, 0.0f};
int referenceValue2[4] = {0, 0, 0, 0};
for (int n = 0; n < patchVertices; ++n) {
referencePointSize += 1.0f / static_cast<float>(n + 1);
for (int c = 0; c < 4; ++c) {
referencePosition[c] += static_cast<float>(n * 4 + c);
referenceValue2[c] += n + 1 + c;
}
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
}
// isolines with every level at 1 emits two points; the record stride is
// vec4 + vec2 + ivec4 [+ float] components.
const std::size_t stride = withPointSize ? 11 : 10;
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
for (int c = 0; c < 4; ++c) {
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
<< "te_position." << c << " (gl_in[].gl_Position)";
}
for (int c = 0; c < 2; ++c) {
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
}
for (int c = 0; c < 4; ++c) {
const std::size_t index = static_cast<std::size_t>(6 + c);
ASSERT_LT(index, captured.size());
int actual = 0;
std::memcpy(&actual, &captured[index], sizeof(actual));
EXPECT_EQ(actual, referenceValue2[c])
<< "te_value2." << c << " (the user per-vertex block's integer member)";
}
if (withPointSize) {
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
<< "te_pointsize (gl_in[].gl_PointSize)";
}
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
RunPerVertexPayloadCase(false);
}
// ---------------------------------------------------------------------------------
// The same built-in, one stage over.
// ---------------------------------------------------------------------------------
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
// extensions that do not imply each other, so the geometry arm is a second code path
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
// shader, so without this case the arm ships untested.
const char* const kPointSizeGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float gs_value;
void main()
{
gs_value = 7.0;
gl_Position = gl_in[0].gl_Position;
gl_PointSize = 4.0;
EmitVertex();
}
)";
// The control: identical but for the gl_PointSize write, so the pair answers "can this
// stack host a geometry stage that names the built-in" without asking anything about
// capture.
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 1) out;
out float gs_value;
void main()
{
gs_value = 7.0;
gl_Position = gl_in[0].gl_Position;
EmitVertex();
}
)";
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
const auto probeCaptures = [&](const char* geometrySource) {
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_GEOMETRY_SHADER, geometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gs_value"});
if (program == 0) return false;
const std::vector<float> poison(1, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
float captured = kPoison;
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
&captured);
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
DrainErrors();
return captured == 7.0f;
};
if (probeCaptures(kPointSizeGeometrySource)) return {};
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
// The control failed too, so this stack cannot run a capturing geometry stage at
// all - which is not what this case is about, and is the question the dead
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
// and goes red on its own if that is what actually broke.
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
}
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
}
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
if (!Ready()) GTEST_SKIP();
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gs_value", "gl_PointSize"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
const std::vector<float> poison(2, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> captured(2, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
"the stage carrying it fails to compile";
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glUseProgram(0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
}
// ---------------------------------------------------------------------------------
// The conformance body's own READBACK, which is not glGetBufferSubData.
// ---------------------------------------------------------------------------------
// Every case above reads the capture back with glGetBufferSubData because that is the
// shortest path to the bytes. The conformance bodies do something else: they respecify
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
// while it is simultaneously bound to indexed capture point 0, and then read it with
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
// call, so the sequence itself is worth pinning: none of the map path's error conditions
// may fire, and the mapped bytes must be the captured ones.
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
{"gl_Position"});
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
constexpr std::size_t kFloats = 4 * 3;
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
for (int iteration = 0; iteration < 2; ++iteration) {
// Respecified through the generic binding, exactly as the conformance body does,
// while the same buffer is still bound to capture point 0.
const std::vector<float> poison(kFloats, kPoison);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
glDrawArrays(GL_PATCHES, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
const auto* mapped =
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
GL_MAP_READ_BIT));
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
const std::vector<float> captured(mapped, mapped + kFloats);
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
glUseProgram(0);
}
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
// without asking for it does not merely lose the value - the stage fails to compile
// and the whole program is replaced by program 0.
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
RunPerVertexPayloadCase(true);
}
// ---------------------------------------------------------------------------------
// The same capture through a PROGRAM PIPELINE OBJECT.
// ---------------------------------------------------------------------------------
// The conformance body runs each of its configurations twice: once with a monolithic
// program object and once with a pipeline of four separable programs, the capture
// declared on the separable EVALUATION program. That second shape goes through the
// hidden composite the pipeline object builds for the draw, and it is the only place a
// tessellation capture and the composite meet - so the capture list has to survive being
// taken from a program that is not the one bound.
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
// One separable program per stage. Only the evaluation program carries the capture
// list, because it is the one whose outputs are captured.
const auto buildSeparable = [&](GLenum stage, const char* source,
const std::vector<const char*>& varyings) -> GLuint {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true);
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
glAttachShader(program, shader);
if (!varyings.empty()) {
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
}
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
glDeleteShader(shader);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
};
m_buildLog.clear();
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
const GLuint evalProgram =
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
GLuint pipeline = 0;
glGenProgramPipelines(1, &pipeline);
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
constexpr std::size_t kFloats = 4 * 3;
const std::vector<float> poison(kFloats, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
poison.data(), GL_STATIC_DRAW);
glBindVertexArray(m_vao);
glUseProgram(0);
glBindProgramPipeline(pipeline);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
glDrawArrays(GL_PATCHES, 0, 1);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<float> captured(kFloats, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glBindProgramPipeline(0);
glDeleteProgramPipelines(1, &pipeline);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDeleteBuffers(1, &xfbBuffer);
}
} // namespace
} // namespace MGITest
@@ -119,6 +119,73 @@ void main() {
imageStore(u_unbound, int(index), uvec4(7u));
g_data[index] = index + 1u;
}
)";
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
// level and no mip chain - GL calls the second one incomplete for the initial
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
// texture the backend then fails to back.
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
uniform sampler2D u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = texture(u_unbound, vec2(0.0));
}
o_color = color;
}
)";
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
// unused one at an empty unit, so whichever of the two is unused has to have a
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
// cannot stand in for it.
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
uniform sampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = texelFetch(u_unbound, ivec2(0), 0);
}
o_color = color;
}
)";
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
// the descriptor resolve used to fail, losing the draw after the placeholder had already
// been created.
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
uniform usampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
}
o_color = color;
}
)";
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
uniform isampler2DMS u_unbound;
uniform int u_readUnbound;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_readUnbound != 0) {
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
}
o_color = color;
}
)";
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
@@ -369,6 +436,65 @@ void main() {
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
}
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
}
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
// unit whose DEFAULT texture object has an image but no mip chain.
//
// DirectVulkan resolved such a binding twice, through two different predicates that
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
// pre-syncs and transitions every texture the draw will sample, asked only whether the
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
// a SIGSEGV inside the draw, not a degraded picture.
//
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
// texture a base level, so the FIRST case in a process survived and every later one with an
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
// object bound there a FORMAT and a zero-sized level - which is what a bare
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
// state reset issues for every texture target. That combination is the whole point:
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
// and the texture stays in the sampled set;
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
// true and the descriptor holds the fallback instead;
// * and it has no valid mip level, so the sync declines and hands back nullptr.
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
if (!Ready() || IsSkipped()) return;
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
}
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
if (!Ready() || IsSkipped()) return;
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
@@ -0,0 +1,526 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
// QUALIFIER REMOVED.
//
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
// stages compile, the program links with an empty info log, the draw runs, and the consuming
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
// from the blocks (and changing nothing else) makes the same program carry its payload. The
// locations are not the application's in the first place: these shaders declare none, and
// glslang's cross-stage IO resolver invents them.
//
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
// location must still deliver its payload end to end. It is the assertion the affected device
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
// cannot reproduce the defect at all.
//
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
// entry, because llvmpipe carries a located block correctly and the driver POST would
// therefore never turn the emulation on here. The SAME cases also run under the ambient
// registrations with the emulation off, so both spellings of the interface are covered and a
// regression in either shows up.
//
// Colour code, so a failure names its own cause:
// green - the payload crossed all four stage boundaries, which is the pass.
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
// backend program was rejected and every draw became a no-op).
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
// failure is not about interface blocks.
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
// zeroed. That is what an interface whose two ends stopped matching looks like.
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// NOTHING in these five stages declares a location. Every location the emitted ESSL
// carries is invented by the cross-stage resolver, which is exactly the shape the
// affected driver mishandles and exactly what the strip removes.
//
// Two members per block, of different types, because an interface that is matched by
// name and member sequence rather than by location has to agree on the sequence too -
// a repair that silently reordered or dropped a member would still light up green with
// one member in the block.
const char* const kVertexSource = R"(#version 420 core
out VsData {
vec4 payload;
vec2 tint;
} vs_out;
out float vs_tcs_alive;
void main()
{
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
vs_out.tint = vec2(0.25, 0.5);
vs_tcs_alive = 1.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
in VsData {
vec4 payload;
vec2 tint;
} tcs_in[];
in float vs_tcs_alive[];
out TcsData {
vec4 payload;
vec2 tint;
} tcs_out[];
out float tcs_tes_alive[];
void main()
{
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
}
)";
// Distinct block names, so this case is about the LOCATION and nothing else; the
// one-name-in-both-directions shape is the case below.
const char* const kDistinctTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
vec2 tint;
} tes_in[];
in float tcs_tes_alive[];
out TesData {
vec4 payload;
vec2 tint;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_out.tint = tes_in[0].tint;
tes_gs_alive = tcs_tes_alive[0];
}
)";
// The 420pack shape: ONE name for the block this stage consumes and the block it
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
// the rename gives the two blocks one spelling per producing stage, the strip takes
// their locations off, and the interfaces still have to meet.
const char* const kCollidingTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
vec2 tint;
} tes_in[];
in float tcs_tes_alive[];
out TcsData {
vec4 payload;
vec2 tint;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_out.tint = tes_in[0].tint;
tes_gs_alive = tcs_tes_alive[0];
}
)";
// One geometry source per evaluation stage, because the block it consumes is named
// after the block the evaluation stage produced.
const char* const kDistinctGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TesData {
vec4 payload;
vec2 tint;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
vec2 tint;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_out.tint = gs_in[0].tint;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
const char* const kCollidingGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TcsData {
vec4 payload;
vec2 tint;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
vec2 tint;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_out.tint = gs_in[0].tint;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
// Green ONLY when both block members arrived: a repair that kept the first member and
// lost the second would otherwise pass. Red when the plain varying is missing too, so
// "the pipeline is broken" and "the block is broken" cannot be confused.
const char* const kFragmentSource = R"(#version 420 core
in GsData {
vec4 payload;
vec2 tint;
} fs_in;
in float gs_fs_alive;
out vec4 fragColor;
void main()
{
if (gs_fs_alive <= 0.5) {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
} else {
fragColor = fs_in.payload;
}
}
)";
class UnlocatedIoBlockScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (!BackendHostsTessellationAndGeometry()) {
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "); there is no five-stage pipeline to "
<< "carry a block through";
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
// frontend constants, so a stack with no five-stage pipeline is recognised by
// trying to build one, not by asking.
static bool BackendHostsTessellationAndGeometry() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
while (glGetError() != GL_NO_ERROR) {
}
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
}
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
GL_FRAGMENT_SHADER};
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
geometrySource, kFragmentSource};
GLuint shaders[5] = {0, 0, 0, 0, 0};
bool ok = true;
for (int i = 0; i < 5; ++i) {
shaders[i] = glCreateShader(stages[i]);
glShaderSource(shaders[i], 1, &sources[i], nullptr);
glCompileShader(shaders[i]);
GLint compiled = 0;
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
if (!compiled) {
m_buildLog = InfoLog(shaders[i], true);
ok = false;
break;
}
}
if (!ok) {
for (const GLuint shader : shaders) {
if (shader != 0) glDeleteShader(shader);
}
return 0;
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (!linked) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
// than something that could be mistaken for a zeroed payload.
Rgba8 DrawAndReadCentre(GLuint program) const {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
glDrawArrays(GL_PATCHES, 0, 1);
Rgba8 pixel{};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
return pixel;
}
static bool IsGreen(const Rgba8& pixel) {
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
}
const std::string& BuildLog() const { return m_buildLog; }
// The library log this process is writing, or an empty path when none was
// configured. MOBILEGL_LOG_FILE_PATH is read at log-init, before anything this
// fixture can reach, so the ctest entry sets it and this only reads it back.
static std::filesystem::path LibraryLogPath() {
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
: std::filesystem::path();
}
// How many bytes the library log already holds. Everything this fixture asserts on
// is searched from here forward, because the file is APPENDED to by every process
// in the lane and a line left behind by an earlier one would otherwise satisfy the
// assertion without this process having done anything at all.
static std::uintmax_t LibraryLogSize() {
std::error_code ec;
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return 0;
const std::uintmax_t size = std::filesystem::file_size(path, ec);
return ec ? 0 : size;
}
static std::string LibraryLogSince(std::uintmax_t offset) {
const std::filesystem::path path = LibraryLogPath();
if (path.empty()) return {};
std::ifstream file(path, std::ios::binary);
if (!file.good()) return {};
file.seekg(static_cast<std::streamoff>(offset));
return std::string((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
static GLenum FirstGLError() {
const GLenum first = glGetError();
while (glGetError() != GL_NO_ERROR) {
}
return first;
}
private:
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
if (isShader) {
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
} else {
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
}
return std::string(log.data());
}
GLuint m_vao = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
if (!Ready()) return;
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
if (program == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre))
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
"member arrived wrong, i.e. the interface stopped matching)";
}
// The two repairs together. The rename is what makes the evaluation stage's two
// TcsData blocks one spelling per producing stage; the strip then takes the locations
// off the names the rename just settled. Either one alone leaves a working program on
// these machines, so this case is here to catch the two of them disagreeing.
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
if (!Ready()) return;
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
ASSERT_NE(program, 0u)
<< "an interface block name reused across the two directions of one stage is legal "
"desktop GLSL, but the program did not build: "
<< BuildLog();
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre))
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
}
// THE ONE CASE THAT CAN FAIL WHEN THE REPAIR SILENTLY STOPS BEING ARMED.
//
// Everything above renders green on llvmpipe whether the blocks were stripped or not -
// this machine carries a located block correctly - so those cases pin that the strip
// does no HARM and can say nothing about whether it happened. That leaves the arming
// itself untested, and the arming is where the cheap mistake lives: Loader.cpp maps
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS onto the capability INVERTED (forcing the
// emulation on means declaring located blocks UNSUPPORTED), and a one-line swap of
// those two arms would disable the device repair with every test here still green.
//
// So this case asserts a LIBRARY OBSERVABLE against the environment, the shape
// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration uses: the environment
// says the emulation is pinned on, therefore the library must SAY it stripped
// something. The observable is the latched MGLOG_I DirectGLES emits the first time the
// pass fires (Managers.cpp); it is INFO rather than DEBUG precisely so that this
// assertion is possible in the builds CI runs.
//
// Two things it deliberately does NOT do: it does not read MG_Config (on Android this
// module links the shipping library, which exports nothing internal - the reason
// ViewportArrayScenario's control moved to the environment), and it does not trust the
// whole log file, only the bytes appended after this test started.
TEST_F(UnlocatedIoBlockScenario, TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn) {
if (!Ready()) return;
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS") != AmbientQuirk::On) {
GTEST_SKIP() << "this case needs the emulation pinned ON for the whole process, which "
"is what the UnlocatedIoBlocks. ctest entry does with "
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1; with the variable unset the "
"driver POST decides, and on this machine it decides the blocks are "
"fine - so there would be nothing to observe";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned on but "
"MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to "
"record that it stripped anything; the UnlocatedIoBlocks. ctest entry "
"sets both";
}
if (Gl().BackendName() != std::string("DirectGLES")) {
GTEST_SKIP() << "the strip is DirectGLES's; " << Gl().BackendName()
<< " hands the module to the driver as SPIR-V, where Location is how "
"interfaces are matched";
}
// Taken BEFORE the program is built, so the line this looks for can only be one
// this process wrote. The latch means it is emitted at the FIRST stage of the
// FIRST affected program, which is inside the build below.
const std::uintmax_t before = LibraryLogSize();
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
if (program == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so nothing would arm the strip: " << BuildLog();
}
// Drawn as well as built, so a stack that defers its backend program to first use
// still reaches the transpile this is asserting about.
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre)) << "the pinned-on lane did not even render correctly: " << centre;
const std::string appended = LibraryLogSince(before);
EXPECT_NE(appended.find("WITHOUT their layout(location) qualifier"), std::string::npos)
<< "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS is pinned ON, a five-stage program with four "
"interface-block boundaries was built and drawn, and DirectGLES never reported "
"stripping a single location. The emulation is not armed - check the override "
"mapping in Loader.cpp (it is inverted on purpose) and the arming gate in "
"Managers.cpp. Log appended by this test:\n"
<< appended;
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,276 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnwrittenPositionOutputScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A SHADER REDECLARES gl_PerVertex AND NEVER WRITES gl_Position.
//
// Legal, ordinary GLSL, and until now a process kill on DirectVulkan. The chain, all of it
// inside MobileGL's own SPIR-V plumbing:
//
// 1. glslang emits every DECLARED interface variable, used or not, and lists it on
// OpEntryPoint. So `out gl_PerVertex { vec4 gl_Position; };` with no write still produces
// the OpVariable, the OpMemberDecorate BuiltIn Position, and an interface slot.
// 2. At link, ShaderCompiler::SanitizeAndOptimizeBinary runs AggressiveDCE(remove_outputs =
// false) - which may never delete an Output - and then RemoveUnusedInterfaceVariables,
// which rebuilds the interface list from the variables instructions actually reference.
// The OpVariable and its BuiltIn decoration SURVIVE; the interface slot is DELISTED.
// 3. At pipeline build, ProgramFactory picks the last pre-rasterisation stage and runs two
// passes over it. GlToVulkanPositionFixPass finds the position target through the
// surviving ANNOTATION and injects a load-modify-STORE through it. When gl_Position is in
// the transform-feedback capture list, XfbCaptureDecoratePass::MirrorPositionForCapture
// also injects an access chain and a LOAD through it.
// 4. Either injection is a static use of a variable that is no longer on the entry point's
// interface, which is invalid SPIR-V ("Interface variable id <N> is used by entry point
// 'main' id <M>, but is not listed as an interface"). Mali r54 does not reject such a
// module - it faults inside pipeline creation and takes the process down.
//
// Measured on a Mali-G1-Ultra as 216 KHR-GL44/45/46.tessellation_shader.tessellation_control_
// to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize_* crashes; the CTS's TES
// there is exactly the shape below. It is not tessellation-specific and not XFB-specific: a
// vertex shader is enough, which is what these cases use.
//
// Every test captures a USER varying through transform feedback under GL_RASTERIZER_DISCARD.
// Position is undefined in the first two by construction, so it is never asserted on - what is
// asserted is that the capture came back at all, which it can only do if the driver accepted
// the module and built a pipeline.
#include <cstddef>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::size_t kCaptureFloats = 4;
constexpr GLsizeiptr kCaptureBytes = static_cast<GLsizeiptr>(kCaptureFloats * sizeof(float));
// The defect's shape: gl_PerVertex redeclared, gl_Position never assigned.
constexpr const char* kUnwrittenPositionVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out gl_PerVertex {
vec4 gl_Position;
};
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
// The control that isolates the redeclaration: identical but for the one assignment.
// This one keeps its interface slot through the sanitize chain, so both injections were
// always legal on it - it must stay working.
constexpr const char* kWrittenPositionVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out gl_PerVertex {
vec4 gl_Position;
};
out vec4 vs_out_value;
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
vs_out_value = vs_in_value;
}
)";
// The second control, and the one the CTS calls data_pass_through: no gl_PerVertex
// redeclaration at all, so there is no Position annotation for the passes to find and
// nothing to delist. It was never affected and proves the crash needs the redeclaration.
constexpr const char* kNoPositionBlockVertexSource = R"(#version 430 core
layout(location = 0) in vec4 vs_in_value;
out vec4 vs_out_value;
void main() {
vs_out_value = vs_in_value;
}
)";
GLuint CompileVertexShader(const std::string& source, std::string* log) {
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
const char* text = source.c_str();
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint status = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteShader(shader);
return 0;
}
return shader;
}
// `captureNames` is what goes to glTransformFeedbackVaryings. Passing gl_Position in it
// is what puts MirrorPositionForCapture on the path.
GLuint BuildCaptureProgram(const char* vertexSource, const std::vector<const char*>& captureNames,
std::string* log) {
const GLuint vertexShader = CompileVertexShader(vertexSource, log);
if (vertexShader == 0) return 0;
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glTransformFeedbackVaryings(program, static_cast<GLsizei>(captureNames.size()), captureNames.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vertexShader);
GLint status = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
if (log != nullptr) *log = buffer.data();
glDeleteProgram(program);
return 0;
}
return program;
}
class UnwrittenPositionOutputScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
const float vertex[kCaptureFloats] = {1.0f, 2.0f, 3.0f, 4.0f};
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, vertex, GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
glUseProgram(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
ScenarioTest::TearDown();
}
// Links `vertexSource` with `captureNames`, runs one captured point, and checks that
// the USER varying came back. `captureStride` is how many floats one captured vertex
// occupies, so the user varying can be read out from behind a captured gl_Position.
void ExpectUserVaryingIsCaptured(const char* vertexSource, const std::vector<const char*>& captureNames,
std::size_t captureStride, std::size_t userVaryingOffset,
const char* what) {
std::string log;
const GLuint program = BuildCaptureProgram(vertexSource, captureNames, &log);
ASSERT_NE(program, 0u) << what << ": the capture program failed to build: " << log;
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(captureStride * sizeof(float));
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
// Pre-fill with a value the shader cannot produce, so "captured nothing" is
// distinguishable from "captured the wrong thing".
const std::vector<float> poison(captureStride, -1.0f);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, captureBytes, poison.data(), GL_DYNAMIC_READ);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
ASSERT_EQ(FirstGLError(), 0u) << what << ": setting up the capture buffer raised a GL error";
glEnable(GL_RASTERIZER_DISCARD);
glUseProgram(program);
glBindVertexArray(m_vao);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, 1);
glEndTransformFeedback();
glBindVertexArray(0);
glUseProgram(0);
glDisable(GL_RASTERIZER_DISCARD);
EXPECT_EQ(FirstGLError(), 0u) << what << ": the captured draw raised a GL error";
std::vector<float> readback(captureStride, -2.0f);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, readback.data());
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
EXPECT_FLOAT_EQ(readback[userVaryingOffset + i], static_cast<float>(i + 1))
<< what << ": captured float " << i << " came back as "
<< readback[userVaryingOffset + i]
<< "; the pre-fill value means the draw never produced a vertex, which is what an "
"invalid shader module looks like from out here";
}
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteBuffers(1, &xfbBuffer);
glDeleteProgram(program);
}
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
} // namespace
// The clip fixup's half: PositionZRemap is on for every draw, so the fixup runs on this
// program and used to inject a store through the delisted block.
TEST_F(UnwrittenPositionOutputScenario, ARedeclaredButUnwrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"redeclared, never written");
}
// The XFB half: capturing gl_Position adds an access chain and a LOAD through the same
// delisted block, which the interface rule covers exactly as it covers the store. Position
// itself is undefined here - only the user varying behind it is asserted.
TEST_F(UnwrittenPositionOutputScenario, CapturingAnUnwrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
// DirectVulkan only, and not because the defect was backend-specific in principle - the
// injection this pins lives in DirectVulkan's ProgramFactory, and DirectGLES cannot
// reach the case at all: capturing gl_Position BY NAME off a shader that never writes it
// comes back empty there, because the ESSL the transpiler emits has no such output for
// the capture list to name. That is a known, separate DirectGLES gap (the same one that
// blocks gl_Position/gl_PointSize capture in the tessellation capture segment), tracked
// outside this scenario; asserting it here would only re-report it.
if (Gl().BackendName() != "DirectVulkan") {
GTEST_SKIP() << "capturing an unwritten gl_Position by name is a separate, known "
<< "DirectGLES gap; this case pins the DirectVulkan injection";
}
ExpectUserVaryingIsCaptured(kUnwrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
kCaptureFloats * 2, kCaptureFloats, "capturing an unwritten gl_Position");
}
// Control: the same shader with the one assignment restored. Its block is never delisted,
// so it exercises the path the fixup is actually for and must keep working.
TEST_F(UnwrittenPositionOutputScenario, AWrittenRedeclaredPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"redeclared and written");
}
TEST_F(UnwrittenPositionOutputScenario, CapturingAWrittenPositionStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kWrittenPositionVertexSource, {"gl_Position", "vs_out_value"},
kCaptureFloats * 2, kCaptureFloats, "capturing a written gl_Position");
}
// Control: no gl_PerVertex redeclaration, so no Position annotation and nothing to delist.
TEST_F(UnwrittenPositionOutputScenario, AShaderWithNoPositionBlockStillDraws) {
if (!Ready() || IsSkipped()) return;
ExpectUserVaryingIsCaptured(kNoPositionBlockVertexSource, {"vs_out_value"}, kCaptureFloats, 0,
"no gl_PerVertex block");
}
} // namespace MGITest
@@ -525,7 +525,7 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
//
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
// emulation that works from a backend that was going to be right anyway. This case builds
// the SAME program in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0
// the SAME program in a process started with MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0
// (the NoViewportArrayEmulation. ctest entry) and requires case 1's
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
@@ -551,10 +551,10 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
// entry for it, so the control still runs in every ctest run; anywhere else - the
// ambient ctest entries, or the binary run straight from a device shell - the
// emulation is on and this case skips.
if (AmbientQuirkFromEnvironment("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
if (AmbientQuirkFromEnvironment("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) {
GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the "
"whole process; the NoViewportArrayEmulation. ctest entry runs it with "
"MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0";
"MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0";
}
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
@@ -0,0 +1,657 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - A CAPTURE MUST STILL RECORD WHEN IT IS NOT THE FIRST ONE IN THE PROCESS,
// AND THE CAPTURE STAGE MAY BE ANY OF THE FOUR THAT CAN BE THE LAST ONE.
//
// The conformance suite exposed a whole family of transform feedback failures that no
// existing scenario could reproduce, because every one of them ran ONE capture, from a
// VERTEX stage, in a freshly initialised process. What the suite actually does is
// different in three ways at once, and each of them turned out to matter:
//
// * it runs case after case in ONE GL context, resetting state between them - and the
// reset is not a fresh context. Its transform feedback part
// (framework/opengl/gluStateReset.cpp resetStateGLCore) unbinds the generic
// GL_TRANSFORM_FEEDBACK_BUFFER and then clears every indexed capture point from 0 to
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, which permanently raises MobileGL's
// touched-binding-point high-water mark. Every later capture that uses fewer points
// than that - i.e. every INTERLEAVED_ATTRIBS capture - then had the unused tail
// re-cleared on the driver immediately before glBeginTransformFeedback.
// ReplayDeqpStateReset below is that reset, reduced to the calls that touch capture
// state, so a defect that only appears from the second capture onwards is reachable
// here instead of only on a device.
//
// * the capture stage is frequently a GEOMETRY or a TESSELLATION EVALUATION shader,
// never a plain vertex shader. The tree had zero coverage for either: none of the
// Xfb* scenarios mentioned tessellation and neither TessellationDrawModeScenario nor
// GeometryDrawModeScenario mentioned transform feedback.
//
// * the capture program frequently has NO FRAGMENT STAGE at all, because it draws
// under GL_RASTERIZER_DISCARD and never rasterises anything. That is legal in
// desktop GL and the shape most "use transform feedback as a readback channel"
// tests are built on.
//
// Every case here asserts the captured BYTES, never just the absence of a GL error: the
// failure this guards against writes nothing and raises nothing, so a buffer that kept
// its poison is the only thing that distinguishes it from success.
#include <cmath>
#include <string>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Nothing a capture can legitimately produce, so a component that still reads it
// names the failure ("the capture never reached these bytes") instead of looking
// like an ordinary numeric mismatch.
constexpr int kPoison = -987654;
const char* const kPassthroughVertexSource = R"(#version 420 core
layout(location = 0) in int vs_in_value;
flat out int vs_out_value;
void main()
{
vs_out_value = vs_in_value;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
// The primitive_counter shape: one flat int per emitted vertex, several vertices
// per input primitive, so the capture is geometry-AMPLIFIED and the CPU-side
// primitive model cannot predict its length.
const char* const kPointAmplifyingGeometrySource = R"(#version 420 core
layout(points) in;
layout(points, max_vertices = 2) out;
flat in int vs_out_value[];
flat out int gs_out_value;
void main()
{
for (int i = 0; i < 2; ++i)
{
gs_out_value = vs_out_value[0];
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
}
)";
// Adjacency input. Only a geometry stage can consume it, and CountPrimitivesForDraw
// used to answer 0 for every adjacency mode, which silently excluded the whole draw
// from the capture accounting.
const char* const kAdjacencyGeometrySource = R"(#version 420 core
layout(lines_adjacency) in;
layout(points, max_vertices = 1) out;
flat in int vs_out_value[];
flat out int gs_out_value;
void main()
{
gs_out_value = vs_out_value[1];
gl_Position = gl_in[1].gl_Position;
EmitVertex();
EndPrimitive();
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
flat in int vs_out_value[];
patch out int tcs_out_value;
void main()
{
tcs_out_value = vs_out_value[0];
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kTessEvalSource = R"(#version 420 core
layout(triangles, equal_spacing, cw) in;
patch in int tcs_out_value;
flat out int tes_out_value;
void main()
{
tes_out_value = tcs_out_value;
gl_Position = gl_in[0].gl_Position;
}
)";
const char* const kFragmentSource = R"(#version 420 core
flat in int gs_out_value;
out vec4 fragColor;
void main()
{
fragColor = vec4(float(gs_out_value), 0.0, 0.0, 1.0);
}
)";
class XfbRepeatedCaptureScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
const int values[kInputVertices] = {10, 11, 12, 13};
glBufferData(GL_ARRAY_BUFFER, sizeof(values), values, GL_STATIC_DRAW);
glVertexAttribIPointer(0, 1, GL_INT, 0, nullptr);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vbo = 0;
m_vao = 0;
ScenarioTest::TearDown();
}
static constexpr int kInputVertices = 4;
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
static bool BackendHostsGeometry() {
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
DrainErrors();
return maxGeometryOutputVertices >= 2;
}
static bool BackendHostsTessellation() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
DrainErrors();
return maxTessGenLevel >= 1;
}
// The transform-feedback-relevant half of deqp's resetStateGLCore, in its order.
// It runs between EVERY pair of conformance cases, and running one capture
// through it is the difference between "the first capture in the process" and
// every other one.
static void ReplayDeqpStateReset() {
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
GLint maxSeparateAttribs = 0;
glGetIntegerv(GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSeparateAttribs);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
for (GLint index = 0; index < maxSeparateAttribs; ++index) {
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(index), 0);
}
DrainErrors();
}
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
if (isShader) {
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
} else {
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
}
return buffer.data();
}
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
const char* varying) {
return BuildCaptureProgram(stages, std::vector<const char*>{varying});
}
// Builds a capture program out of `stages` capturing `varyings` interleaved.
// Returns 0 and fills m_buildLog on failure.
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, const char*>>& stages,
const std::vector<const char*>& varyings) {
m_buildLog.clear();
std::vector<GLuint> shaders;
bool ok = true;
for (const auto& [stage, source] : stages) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
shaders.push_back(shader);
if (compiled == GL_FALSE) {
m_buildLog = InfoLog(shader, true);
ok = false;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
program = 0;
}
}
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (program != 0) m_programs.push_back(program);
return program;
}
// One capture span. `captureMode` is the transform feedback primitive mode,
// `drawMode`/`count` the draw. Returns the capture buffer's contents.
std::vector<int> RunCaptureSpan(GLuint program, GLenum captureMode, GLenum drawMode, GLsizei count,
std::size_t capturedInts) {
std::vector<int> poison(capturedInts, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// The capture point is the ONLY thing bound; the generic
// GL_TRANSFORM_FEEDBACK_BUFFER binding comes along for the ride, exactly as
// the conformance tests rely on (GL 4.6 core 6.1.1).
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(captureMode);
glDrawArrays(drawMode, 0, count);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
std::vector<int> readback(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
glUseProgram(0);
glDeleteBuffers(1, &xfbBuffer);
return readback;
}
static ::testing::AssertionResult CapturedNothing(const std::vector<int>& data) {
for (std::size_t i = 0; i < data.size(); ++i) {
if (data[i] != kPoison) {
return ::testing::AssertionFailure() << "component " << i << " is " << data[i];
}
}
return ::testing::AssertionSuccess();
}
static ::testing::AssertionResult CapturedIs(const std::vector<int>& data,
const std::vector<int>& expected) {
if (data.size() != expected.size()) {
return ::testing::AssertionFailure()
<< "captured " << data.size() << " value(s), expected " << expected.size();
}
for (std::size_t i = 0; i < data.size(); ++i) {
if (data[i] != expected[i]) {
::testing::AssertionResult failure = ::testing::AssertionFailure();
failure << "component " << i << " is " << data[i] << ", expected " << expected[i];
if (data[i] == kPoison) {
failure << " (the capture never reached these bytes)";
}
return failure;
}
}
return ::testing::AssertionSuccess();
}
std::vector<GLuint> m_programs;
std::string m_buildLog;
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
// THE REGRESSION GUARD FOR THE WHOLE FAMILY. Two geometry-stage captures in one
// process with the conformance suite's own state reset between them; the assertion
// that matters is on the SECOND one, which is the one every device run failed while
// whichever body happened to land first in its process passed.
TEST_F(XfbRepeatedCaptureScenario, ASecondGeometryCaptureAfterADeqpStateResetStillRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
// Two vertices emitted per input point, so the capture is amplified beyond what
// the CPU primitive model can predict from the draw alone.
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
for (int capture = 0; capture < 3; ++capture) {
// A fresh program per capture, because that is what a fresh conformance case
// builds - and it is what makes the driver recycle program and buffer names.
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "capture " << capture << " program failed to build: " << m_buildLog;
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected))
<< "capture " << capture << " of 3 in this process"
<< (capture == 0 ? "" : " (every earlier one was followed by a deqp-shaped state reset)");
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "capture " << capture;
glDeleteProgram(program);
m_programs.pop_back();
ReplayDeqpStateReset();
glBindVertexArray(m_vao);
}
}
// The tessellation half, which had no coverage anywhere in the tree: a capture taken
// from a GL_PATCHES draw, whose last vertex-processing stage is the evaluation shader
// and whose record count only the tessellator knows.
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAPatchesDrawRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsTessellation()) {
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
<< ")";
}
// One input patch of one vertex, all levels at 1: the tessellator emits exactly
// one triangle, so three captured vertices all carrying the first input value.
glPatchParameteri(GL_PATCH_VERTICES, 1);
DrainErrors();
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_TESS_CONTROL_SHADER, kTessControlSource},
{GL_TESS_EVALUATION_SHADER, kTessEvalSource}},
"tes_out_value");
ASSERT_NE(program, 0u) << "patch capture program failed to build: " << m_buildLog;
const std::vector<int> expected = {10, 10, 10};
const std::vector<int> captured = RunCaptureSpan(program, GL_TRIANGLES, GL_PATCHES, 1, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// A capture program with NO FRAGMENT STAGE, drawn under GL_RASTERIZER_DISCARD. Legal
// in desktop GL, and the shape most transform-feedback-as-readback tests use; the
// program above only differs from it by the fragment shader, so a failure here is
// specifically about the missing stage.
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAFragmentlessProgramRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "fragmentless capture program failed to build: " << m_buildLog;
const std::vector<int> expected = {10, 10, 11, 11, 12, 12, 13, 13};
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_POINTS, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// An ADJACENCY draw feeding the capture. CountPrimitivesForDraw answered 0 for all
// four adjacency modes, which made the transform feedback accounting skip the draw
// entirely - so neither the captured-vertex counter nor the geometry-capture-draw
// flag moved, and anything downstream of either was working from "nothing happened".
TEST_F(XfbRepeatedCaptureScenario, ACaptureFromAnAdjacencyDrawRecords) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kAdjacencyGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "adjacency capture program failed to build: " << m_buildLog;
// Four vertices of GL_LINES_ADJACENCY are one line primitive; the shader emits
// the second vertex of the four, which is the line's first real endpoint.
const std::vector<int> expected = {11};
const std::vector<int> captured =
RunCaptureSpan(program, GL_POINTS, GL_LINES_ADJACENCY, kInputVertices, expected.size());
EXPECT_TRUE(CapturedIs(captured, expected));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
}
// An adjacency draw with NO geometry stage. GL 4.6 core table 13.1 admits
// GL_LINES_ADJACENCY and GL_LINE_STRIP_ADJACENCY under capture mode GL_LINES (and the
// triangle pair under GL_TRIANGLES): without a geometry shader the adjacent vertices
// are ignored and the primitive assembled is a plain line, so the combination is legal
// and must capture. MobileGL's active-capture primitive-mode table listed only the
// non-adjacency modes, so this raised GL_INVALID_OPERATION and dropped the draw
// entirely - the buffer kept its pre-draw bytes and the application saw an error the
// spec does not allow. Distinct from ACaptureFromAnAdjacencyDrawRecords above, which
// HAS a geometry stage and therefore bypasses that table completely.
TEST_F(XfbRepeatedCaptureScenario, AVertexOnlyAdjacencyCaptureRecords) {
if (!Ready()) GTEST_SKIP();
const GLuint program =
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
ASSERT_NE(program, 0u) << "vertex-only capture program failed to build: " << m_buildLog;
// Four vertices of GL_LINES_ADJACENCY are one line whose real endpoints are the
// middle pair, so the capture is those two vertices in order.
const std::vector<int> expected = {11, 12};
const std::vector<int> captured =
RunCaptureSpan(program, GL_LINES, GL_LINES_ADJACENCY, kInputVertices, expected.size());
// THE GUARD FOR THE DEFECT ITSELF, and it is backend-independent: the frontend
// validator must not reject the combination. It used to record
// GL_INVALID_OPERATION and return before the draw was ever issued.
EXPECT_EQ(glGetError(), GL_NO_ERROR)
<< "a capture-mode/draw-mode pair GL 4.6 core table 13.1 admits must raise no error";
// Whether the capture then RECORDS is a backend question, and the two answer it
// differently. ES 3.2 (10.1) supports the adjacency primitive types only for a
// pipeline with a geometry shader, so DirectGLES has nothing to forward this draw
// to; desktop GL and Vulkan both assemble the plain line and capture it. Asserting
// the data unconditionally would be asserting that DirectGLES emulates a whole ES
// restriction away, which is a separate piece of work and not what this guards.
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "DirectGLES cannot forward a geometry-shader-less adjacency draw: ES 3.2 10.1 "
"supports the adjacency primitive types only with a geometry stage. The frontend "
"no longer rejects the draw (checked above), which is the defect this covers.";
}
EXPECT_TRUE(CapturedIs(captured, expected));
}
// A CAPTURE MUST NEVER LAND IN A BUFFER THE APPLICATION DID NOT BIND FOR IT.
//
// A capture list may legally begin with gl_NextBuffer, which leaves capture buffer 0
// with stride 0 and nothing to capture - so glBeginTransformFeedback does not require a
// buffer at point 0 and the application binds only point 1. The driver-side program is
// a single-buffer interleaved capture (the pseudo-varyings are consumed at link time),
// so it writes capture point 0, and MobileGL redirects that into scratch storage and
// scatters the records afterwards.
//
// Two ways that went wrong, both fixed here: the scratch was sized by reading each
// target's stride at its POSITION in a list that skips unbound buffers, which for this
// layout read stride 0 for everything and produced a zero capacity; and when the
// scratch then failed to bind, the span opened anyway onto whatever capture point 0
// still held from an earlier capture in the process - silently overwriting an unrelated
// application buffer. The first span below exists purely to leave such a binding behind.
TEST_F(XfbRepeatedCaptureScenario, ACaptureListBeginningWithGlNextBufferSparesTheEarlierBuffer) {
if (!Ready()) GTEST_SKIP();
const std::size_t capturedInts = 4;
const GLsizeiptr captureBytes = static_cast<GLsizeiptr>(capturedInts * sizeof(int));
// Span A: an ordinary capture, so capture point 0 is left holding bufferA.
const GLuint programA =
BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}}, "vs_out_value");
ASSERT_NE(programA, 0u) << "plain capture program failed to build: " << m_buildLog;
std::vector<int> poison(capturedInts, kPoison);
GLuint bufferA = 0;
glGenBuffers(1, &bufferA);
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, bufferA);
glBindVertexArray(m_vao);
glUseProgram(programA);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kInputVertices);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
std::vector<int> afterA(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, afterA.data());
const std::vector<int> spanAExpected = {10, 11, 12, 13};
ASSERT_TRUE(CapturedIs(afterA, spanAExpected)) << "the setup span itself did not capture";
// Span B: gl_NextBuffer first, so buffer 0 captures nothing and only point 1 is bound.
const GLuint programB = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource}},
{"gl_NextBuffer", "vs_out_value"});
if (programB == 0) {
GTEST_SKIP() << "gl_NextBuffer capture lists are not linkable on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "): " << m_buildLog;
}
GLuint bufferB = 0;
glGenBuffers(1, &bufferB);
glBindBuffer(GL_ARRAY_BUFFER, bufferB);
glBufferData(GL_ARRAY_BUFFER, captureBytes, poison.data(), GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Point 0 released, point 1 is the only destination this capture asks for.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, bufferB);
glUseProgram(programB);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kInputVertices);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
// THE ASSERTION THAT MATTERS: bufferA was not a destination of this capture, so it
// must still read exactly what span A left in it. A failure here is the corruption.
std::vector<int> bufferAAfterB(capturedInts, 0);
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
glGetBufferSubData(GL_ARRAY_BUFFER, 0, captureBytes, bufferAAfterB.data());
glBindBuffer(GL_ARRAY_BUFFER, 0);
EXPECT_TRUE(CapturedIs(bufferAAfterB, spanAExpected))
<< "the gl_NextBuffer capture wrote into the buffer the PREVIOUS span had bound";
EXPECT_EQ(glGetError(), GL_NO_ERROR);
// ...and, where the backend places this layout at all, the buffer it WAS asked to
// write gets the records. That placement is the DirectGLES scatter path, whose
// scratch sizing used to read each target's stride at its POSITION in a list that
// skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0
// for every target and sized the scratch at zero. DirectVulkan does not implement a
// leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a
// pre-existing gap of its own, and the assertion above - that it corrupts nothing
// while declining - is what matters for it.
const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan";
if (backendPlacesLeadingNextBuffer) {
std::vector<int> bufferBAfter(capturedInts, kPoison);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data());
EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected));
}
// Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer
// this test deleted would follow the process into the next scenario.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0);
glDeleteBuffers(1, &bufferA);
glDeleteBuffers(1, &bufferB);
if (!backendPlacesLeadingNextBuffer) {
GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it "
"captures nothing, which the no-corruption assertion above has already covered.";
}
}
// The control for all of the above: a span that never draws must leave the capture
// buffer alone. Without it "the buffer kept its poison" could be read as the correct
// outcome of some path rather than as the bug, and the tightened early returns in
// StartPendingTransformFeedback have to keep this legal case legal.
TEST_F(XfbRepeatedCaptureScenario, ASpanThatNeverDrawsLeavesTheCaptureBufferAlone) {
if (!Ready()) GTEST_SKIP();
if (!BackendHostsGeometry()) {
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString() << ")";
}
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kPassthroughVertexSource},
{GL_GEOMETRY_SHADER, kPointAmplifyingGeometrySource},
{GL_FRAGMENT_SHADER, kFragmentSource}},
"gs_out_value");
ASSERT_NE(program, 0u) << "capture program failed to build: " << m_buildLog;
const std::size_t capturedInts = 8;
std::vector<int> poison(capturedInts, kPoison);
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedInts * sizeof(int)), poison.data(),
GL_STATIC_COPY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
glUseProgram(program);
glBeginTransformFeedback(GL_POINTS);
glEndTransformFeedback();
glUseProgram(0);
std::vector<int> readback(capturedInts, 0);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
static_cast<GLsizeiptr>(capturedInts * sizeof(int)), readback.data());
EXPECT_TRUE(CapturedNothing(readback));
EXPECT_EQ(glGetError(), GL_NO_ERROR);
glDeleteBuffers(1, &xfbBuffer);
}
} // namespace
} // namespace MGITest
+131
View File
@@ -0,0 +1,131 @@
// MobileGL - MobileGL/MG_Pipe/Coverage.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call
// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57
// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py).
//
// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and
// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc
// with the per-accessor table and prints the coverage summary. Rows matching neither are
// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes
// "regenerate and git diff --exit-code with 0 UNMAPPED".
//
// Three pseudo-calls stand for read points that do NOT become a forward call:
// kClientResolved - the frontend answers it itself; the server is never asked
// (section 4.4.6: "the server answers nothing the client can answer").
// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1).
// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr<MG_State...>, which
// becomes an MGPipeHandle parameter; there is no single call to name.
//
// clang-format off
// X(Accessor, PipeCall)
#define MGP_COVERAGE_ACCESSOR_LIST(X) \
X(GetActiveTextureUnit, SetSamplerViews) \
X(GetBlendColor, SetDynamicState) \
X(GetBlendEquationIndexed, CreateRenderState) \
X(GetBlendFuncIndexed, CreateRenderState) \
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \
/* inventory is re-vendored carrying the target argument (deferred out of P1: */ \
/* the extractor lives in MobileGL-CS). 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) \
/* The D21 XFB counter-slot rekey's reads (VulkanRenderer.cpp); the calls they */ \
/* map to are GetTransformFeedbackGeneration's. */ \
X(GetBoundTransformFeedbackLifetimeId, SetStreamOutputTargets) \
X(HasOpenTransformFeedbackSpan, SetStreamOutputTargets)
// X(Accessor, Reason) - the STICKY fields (P1 brief D6): the only PipeInputs fields whose
// value is valid across verbs, so the poison's per-verb generation does not apply to them.
// Exactly the seven F-class (forwarded) accessors, and the argument for each is the same:
// it takes an argument that is not verb state - a GL name, a lifetime id, a target - i.e.
// it is a lookup or a reverse-channel write, not a state read; there is no value the
// filler could copy and no verb whose fill could make it stale; phase C replaces them
// with handle tables and callbacks. None of the version/generation accessors is sticky:
// those change under verbs and are precisely what the poison must protect. The verify
// lane's Fatal{UnmigratedPipeInput} is fixed by a FillPoints.def row, never by a row here.
// gen_pipe.py refuses a name that is not an accessor above.
#define MGP_COVERAGE_STICKY_LIST(X) \
X(GetBufferBindingPointCount, "keyed by target: a constexpr capacity table, not verb state") \
X(GetProgramObject, "keyed by GL name: an object lookup, not verb state") \
X(GetTextureObject, "keyed by GL name: an object lookup, not verb state") \
X(HasOpenTransformFeedbackSpan, "keyed by lifetime id: an object lookup, not verb state") \
X(ValidateProgramName, "keyed by GL name: a name-table lookup, not verb state") \
X(InvalidateCompileEnv, "reverse channel: a write into the frontend, not a state read") \
X(RecordError, "reverse channel: a write into the frontend, not a state read")
// X(DeltaKind, PipeCall) - for inventory rows with no accessor in the member column.
// Read by gen_pipe.py ONLY, never by the C++ preprocessor: the delta kinds are the
// inventory's own free-text labels, not C tokens.
#define MGP_COVERAGE_DELTA_LIST(X) \
X(handle-ify (wire handle), kStructuralHandle) \
X(Buffer ops delta, ResourceRespecify)
// clang-format on
+296
View File
@@ -0,0 +1,296 @@
// MobileGL - MobileGL/MG_Pipe/FillPoints.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the
// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into
// generated/PipeFillPoints.inc:
//
// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class
// MGP_FILL_CLASS_LIST the verb classes
// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read
//
// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable
// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and
// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum
// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every
// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go
// through BackendObject virtuals and read no frontend state, so they are not verbs here.
//
// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every
// class and are not listed. The verify lane is the oracle for this table: a
// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field)
// row, never by marking the field sticky.
//
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
//
// clang-format off
// X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h),
// in declaration order. gen_pipe.py parses that struct and refuses a row set that is not exactly its member set.
#define MGP_FILL_VERB_LIST(X) \
X(DrawArrays, kDraw) \
X(DrawElements, kDraw) \
X(DrawElementsBaseVertex, kDraw) \
X(MultiDrawArrays, kDraw) \
X(MultiDrawElements, kDraw) \
X(MultiDrawElementsBaseVertex, kDraw) \
X(MultiDrawElementsIndirect, kDraw) \
X(MultiDrawArraysIndirect, kDraw) \
X(MultiDrawElementsIndirectCount, kDraw) \
X(MultiDrawArraysIndirectCount, kDraw) \
X(DrawRangeElementsBaseVertex, kDraw) \
X(DrawRangeElements, kDraw) \
X(DrawElementsInstancedBaseVertexBaseInstance, kDraw) \
X(DrawElementsInstancedBaseVertex, kDraw) \
X(DrawElementsInstancedBaseInstance, kDraw) \
X(DrawElementsInstanced, kDraw) \
X(DrawArraysInstancedBaseInstance, kDraw) \
X(DrawArraysInstanced, kDraw) \
X(DrawElementsIndirect, kDraw) \
X(DrawArraysIndirect, kDraw) \
X(Clear, kClear) \
X(ClearBufferfi, kClear) \
X(ClearBufferfv, kClear) \
X(ClearBufferuiv, kClear) \
X(ClearBufferiv, kClear) \
X(ClearNamedFramebufferfv, kClear) \
X(ClearNamedFramebufferfi, kClear) \
X(ClearNamedFramebufferiv, kClear) \
X(ClearNamedFramebufferuiv, kClear) \
X(BlitFramebuffer, kBlitOrCopy) \
X(BlitNamedFramebuffer, kBlitOrCopy) \
X(CopyTexImage2D, kBlitOrCopy) \
X(CopyTexSubImage2D, kBlitOrCopy) \
X(CopyImageSubData, kBlitOrCopy) \
X(GenerateMipmap, kTextureOp) \
X(ReadPixels, kReadback) \
X(GetTexImage, kReadback) \
X(GetTextureImage, kReadback) \
X(DispatchCompute, kDispatch) \
X(DispatchComputeIndirect, kDispatch) \
X(MemoryBarrier, kQuery) \
X(MemoryBarrierByRegion, kQuery) \
X(BindImageTexture, kTextureOp) \
X(GetIntegeri_v, kQuery) \
X(ShaderStorageBlockBinding, kProgramOp) \
X(FenceSync, kQuery) \
X(ClientWaitSync, kQuery) \
X(WaitSync, kQuery) \
X(DeleteSync, kQuery) \
X(GetSyncStatus, kQuery) \
X(IsTimerQuerySupported, kQuery) \
X(BeginTimeElapsedQuery, kQuery) \
X(EndTimeElapsedQuery, kQuery) \
X(QueryCounterTimestamp, kQuery) \
X(IsQueryResultAvailable, kQuery) \
X(GetQueryResult64, kQuery) \
X(DeleteBackendQuery, kQuery) \
X(BeginOcclusionQuery, kQuery) \
X(EndOcclusionQuery, kQuery) \
X(BeginXfbPrimitivesQuery, kQuery) \
X(EndXfbPrimitivesQuery, kQuery) \
X(PatchParameteri, kQuery) \
X(BeginTransformFeedback, kXfbSpan) \
X(EndTransformFeedback, kXfbSpan) \
X(PauseTransformFeedback, kXfbSpan) \
X(ResumeTransformFeedback, kXfbSpan) \
X(BindTransformFeedback, kXfbSpan) \
X(DeleteTransformFeedback, kXfbSpan) \
X(GetGpuTimestampNs, kQuery)
// X(Class) - the nine verb classes (ARCHITECTURE.md:153 names eight; kProgramOp is split out because
// ShaderStorageBlockBinding is the one non-draw verb that syncs Espryt's render state and textures).
#define MGP_FILL_CLASS_LIST(X) \
X(kDraw) X(kDispatch) X(kClear) X(kBlitOrCopy) X(kTextureOp) X(kReadback) X(kXfbSpan) X(kProgramOp) X(kQuery)
// X(Class, Field) - the may-read table. A field named here is filled and stamped at every verb of the class;
// a read of a field NOT named here is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
// Derived from the verified reachability of every backend read (both backends, union), P1 brief D7.
#define MGP_FILL_FIELD_LIST(X) \
/* kDraw: every draw entry of both backends */ \
X(kDraw, GetBoundVertexArray) \
X(kDraw, GetProgramForDraw) \
X(kDraw, GetBufferBindingSlot) \
X(kDraw, GetBufferBindingPoint) \
X(kDraw, GetTouchedBufferBindingPointCount) \
X(kDraw, GetTextureUnitObject) \
X(kDraw, GetTextureContextId) \
X(kDraw, GetTextureBindGeneration) \
X(kDraw, GetMaxTouchedTextureUnit) \
X(kDraw, GetSamplingResolutionGeneration) \
X(kDraw, GetImageTextureBinding) \
X(kDraw, GetCurrentVertexAttribute) \
X(kDraw, GetRenderStateParameters) \
X(kDraw, GetRenderStateParametersVersion) \
X(kDraw, GetPipelineStateVersion) \
X(kDraw, GetViewport) \
X(kDraw, GetViewportIndexed) \
X(kDraw, GetDepthRangeIndexed) \
X(kDraw, GetScissorBox) \
X(kDraw, IsCapabilityEnabled) \
X(kDraw, IsCapabilityEnabledIndexed) \
X(kDraw, GetBlendColor) \
X(kDraw, GetBlendFuncIndexed) \
X(kDraw, GetBlendEquationIndexed) \
X(kDraw, GetColorMaskIndexed) \
X(kDraw, GetLogicOp) \
X(kDraw, GetDepthFunc) \
X(kDraw, GetDepthMask) \
X(kDraw, GetStencilState) \
X(kDraw, GetCullFaceMode) \
X(kDraw, GetPolygonModeFront) \
X(kDraw, GetPolygonOffsetFactor) \
X(kDraw, GetPolygonOffsetUnits) \
X(kDraw, GetLineWidth) \
X(kDraw, GetMinSampleShadingValue) \
X(kDraw, GetProvokingVertexMode) \
X(kDraw, GetPatchVertices) \
X(kDraw, GetPatchDefaultOuterLevel) \
X(kDraw, GetPatchDefaultInnerLevel) \
X(kDraw, GetPrimitiveRestartIndex) \
X(kDraw, GetFramebufferBindingSlot) \
X(kDraw, IsTransformFeedbackActive) \
X(kDraw, IsTransformFeedbackPaused) \
X(kDraw, GetTransformFeedbackProgram) \
X(kDraw, GetTransformFeedbackGeneration) \
X(kDraw, GetBoundTransformFeedbackLifetimeId) \
X(kDraw, GetTransformFeedbackCapturedVertices) \
/* kDispatch: the patch fields are Espryt's SyncCurrentProgram -> */ \
/* AttachPassthroughTessControlStage (Managers.cpp) */ \
X(kDispatch, GetProgramForDispatch) \
X(kDispatch, GetBufferBindingSlot) \
X(kDispatch, GetBufferBindingPoint) \
X(kDispatch, GetTouchedBufferBindingPointCount) \
X(kDispatch, GetTextureUnitObject) \
X(kDispatch, GetTextureContextId) \
X(kDispatch, GetTextureBindGeneration) \
X(kDispatch, GetMaxTouchedTextureUnit) \
X(kDispatch, GetSamplingResolutionGeneration) \
X(kDispatch, GetImageTextureBinding) \
X(kDispatch, GetFramebufferBindingSlot) \
/* Magma's PrepareStorageImageTextures materialises a queued clear for every */ \
/* storage image the dispatch writes, and the clear pre-compensates its colour */ \
/* against GL_FRAMEBUFFER_SRGB (VkClearManager::PreCompensateSrgbClearColor). */ \
X(kDispatch, IsCapabilityEnabled) \
X(kDispatch, GetPatchVertices) \
X(kDispatch, GetPatchDefaultOuterLevel) \
X(kDispatch, GetPatchDefaultInnerLevel) \
/* kClear */ \
X(kClear, GetRenderStateParameters) \
X(kClear, GetRenderStateParametersVersion) \
X(kClear, GetViewport) \
X(kClear, IsCapabilityEnabled) \
X(kClear, GetFramebufferBindingSlot) \
X(kClear, GetClearColor) \
X(kClear, GetClearDepth) \
X(kClear, GetClearStencil) \
X(kClear, GetScissorBox) \
X(kClear, GetColorMaskIndexed) \
X(kClear, GetDepthMask) \
X(kClear, GetStencilState) \
X(kClear, GetTextureUnitObject) \
X(kClear, GetTextureContextId) \
X(kClear, GetSamplingResolutionGeneration) \
X(kClear, GetTextureBindGeneration) \
X(kClear, GetMaxTouchedTextureUnit) \
X(kClear, GetImageTextureBinding) \
/* kBlitOrCopy */ \
X(kBlitOrCopy, GetFramebufferBindingSlot) \
X(kBlitOrCopy, IsCapabilityEnabled) \
X(kBlitOrCopy, GetScissorBox) \
X(kBlitOrCopy, IsTransformFeedbackActive) \
X(kBlitOrCopy, IsTransformFeedbackPaused) \
X(kBlitOrCopy, GetRenderStateParameters) \
X(kBlitOrCopy, GetRenderStateParametersVersion) \
X(kBlitOrCopy, GetViewport) \
X(kBlitOrCopy, GetActiveTextureUnit) \
X(kBlitOrCopy, GetTextureUnitObject) \
X(kBlitOrCopy, GetTextureContextId) \
X(kBlitOrCopy, GetSamplingResolutionGeneration) \
X(kBlitOrCopy, GetTextureBindGeneration) \
X(kBlitOrCopy, GetMaxTouchedTextureUnit) \
X(kBlitOrCopy, GetImageTextureBinding) \
X(kBlitOrCopy, GetColorMaskIndexed) \
X(kBlitOrCopy, GetDepthMask) \
X(kBlitOrCopy, GetStencilState) \
/* Magma's shader blit to the default framebuffer */ \
/* (TryBlitToDefaultFramebufferWithShader) is a real draw of a backend-owned */ \
/* helper program: it sets the dynamic viewport through ApplyGLViewportState */ \
/* -> ComputeGLViewport (viewport 0 and its depth range), picks the pipeline's */ \
/* provoking vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode, */ \
/* and binds the helper's descriptors through BindProgramUniformBuffers, whose */ \
/* buffer-block resolvers read the frontend binding points. */ \
X(kBlitOrCopy, GetViewportIndexed) \
X(kBlitOrCopy, GetDepthRangeIndexed) \
X(kBlitOrCopy, GetProvokingVertexMode) \
X(kBlitOrCopy, GetBufferBindingPoint) \
/* kTextureOp */ \
X(kTextureOp, GetActiveTextureUnit) \
X(kTextureOp, GetTextureUnitObject) \
X(kTextureOp, GetImageTextureBinding) \
X(kTextureOp, GetTextureContextId) \
X(kTextureOp, GetSamplingResolutionGeneration) \
X(kTextureOp, GetTextureBindGeneration) \
X(kTextureOp, GetMaxTouchedTextureUnit) \
/* Magma's GenerateMipmap materialises the texture's queued clear before it */ \
/* blits (MaterializePendingClearForTexture -> PreCompensateSrgbClearColor, */ \
/* which reads GL_FRAMEBUFFER_SRGB), and a depth texture takes the shader path */ \
/* (GenerateDepthMipmapWithShader -> BindProgramUniformBuffers), whose sampler */ \
/* resolver reads the draw framebuffer for the feedback-loop check and whose */ \
/* buffer-block resolvers read the frontend binding points. */ \
X(kTextureOp, IsCapabilityEnabled) \
X(kTextureOp, GetFramebufferBindingSlot) \
X(kTextureOp, GetBufferBindingPoint) \
/* kReadback */ \
X(kReadback, GetPixelStoreParameters) \
X(kReadback, GetBufferBindingSlot) \
X(kReadback, GetFramebufferBindingSlot) \
X(kReadback, GetActiveTextureUnit) \
X(kReadback, GetTextureUnitObject) \
X(kReadback, GetClampReadColor) \
X(kReadback, IsCapabilityEnabled) \
X(kReadback, GetRenderStateParameters) \
X(kReadback, GetRenderStateParametersVersion) \
X(kReadback, GetViewport) \
X(kReadback, GetTextureContextId) \
X(kReadback, GetSamplingResolutionGeneration) \
X(kReadback, GetTextureBindGeneration) \
X(kReadback, GetMaxTouchedTextureUnit) \
X(kReadback, GetImageTextureBinding) \
/* The depth/stencil read emulation draws (ScopedEmulationDrawState, */ \
/* DirectGLES.cpp) and pauses an active capture around its own draw, so a */ \
/* readback reads the transform-feedback state exactly as a draw does. */ \
X(kReadback, IsTransformFeedbackActive) \
X(kReadback, IsTransformFeedbackPaused) \
/* kXfbSpan */ \
X(kXfbSpan, GetTransformFeedbackProgram) \
X(kXfbSpan, GetBufferBindingPoint) \
X(kXfbSpan, GetTouchedBufferBindingPointCount) \
X(kXfbSpan, GetTransformFeedbackCapturedVertices) \
X(kXfbSpan, IsTransformFeedbackActive) \
X(kXfbSpan, IsTransformFeedbackPaused) \
X(kXfbSpan, GetTransformFeedbackGeneration) \
X(kXfbSpan, GetBoundTransformFeedbackLifetimeId) \
/* kProgramOp: ShaderStorageBlockBinding syncs Espryt's render state and textures */ \
X(kProgramOp, GetRenderStateParameters) \
X(kProgramOp, GetRenderStateParametersVersion) \
X(kProgramOp, GetViewport) \
X(kProgramOp, IsCapabilityEnabled) \
X(kProgramOp, GetFramebufferBindingSlot) \
X(kProgramOp, GetTextureUnitObject) \
X(kProgramOp, GetTextureContextId) \
X(kProgramOp, GetSamplingResolutionGeneration) \
X(kProgramOp, GetTextureBindGeneration) \
X(kProgramOp, GetMaxTouchedTextureUnit) \
X(kProgramOp, GetImageTextureBinding) \
/* kQuery: Magma's transform feedback query end reads the paused counter */ \
/* (DirectVulkan.cpp); every other verb in the class reads nothing and */ \
/* its fill is a serial bump */ \
X(kQuery, GetTransformFeedbackPausedPrimitiveCounter)
// clang-format on
+98
View File
@@ -0,0 +1,98 @@
// 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"
// G5b: the verb enum (one per GLFunctionsTable entry), the verb classes and their
// may-read field masks - what MGPipeFillForVerb fills and what a poison build lets a
// verb read (FillPoints.def).
#include "generated/PipeFillPoints.inc"
// G6: the backend read inventory's coverage table.
#include "generated/PipeCoverage.inc"
// G7: the render-state pipeline subset, by member name.
#include "generated/PipeSpanTable.inc"
} // namespace MobileGL::MG_Pipe
+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
+807
View File
@@ -0,0 +1,807 @@
// 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, half repaid. The MG_State half is gone: ResidualValueBlock's
// RenderStateParameters and PixelStoreParameters now come from MGPipeValueTypes.h, so
// this header no longer reaches RenderState.h (its closure still touches TextureEnum.h,
// through BackendObject.h, for the reason in the next sentence). What remains is MGPCaps embedding
// MG_Backend's DynamicBackendParameters - deliberate, the caps block IS that struct
// (section 4.4.1) - and that one include is what still keeps purity gate A (section
// 10.3) off this header; the gate asserts MGPipeValueTypes.h instead. The caps block
// needs fixed-width members before it can move (a type change, not a move): P1/P7.
#include <MG_Backend/BackendObject.h>
#include "MGPipeValueTypes.h"
namespace MobileGL::MG_Pipe {
using MG_Backend::DynamicBackendParameters;
// Both live directly in namespace MobileGL and, since P0.5, are declared in
// 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
+549
View File
@@ -0,0 +1,549 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeValueTypes.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#ifndef MOBILEGL_MG_PIPE_VALUE_TYPES_H // belt and braces: this file is reachable both as
#define MOBILEGL_MG_PIPE_VALUE_TYPES_H // <MG_Pipe/...> and <...> (CMakeLists.txt:531,535)
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h> // includes only <Includes.h> + <cstring>
#include <cstddef> // offsetof
#include <type_traits>
// The value types MG_Pipe payloads embed (plan B section 6.3; ARCHITECTURE.md section on
// the value header): the render-state, pixel-store, sampler and vertex-attribute value
// structs and the enums they are made of. They lived in MG_State::GLState until P0.5;
// the MG_State headers that used to define them now include this file, so every existing
// spelling (namespace and name) compiles unchanged.
//
// PURITY: nothing from MG_State, MG_Impl, MG_Backend or MG_Remote -
// scripts/check_include_closure.py probe "value-header" (ROADMAP P0.5; ARCHITECTURE.md
// section 10.3 gate A). Adding one turns CI red. MG_Pipe never includes MG_State back.
namespace MobileGL {
// GL_MAX_DRAW_BUFFERS as MobileGL advertises it. FramebufferObject::MAX_DRAW_BUFFERS is
// defined from this constant, so the two cannot drift.
inline constexpr Uint kMGMaxDrawBuffers = 8;
enum class BlendFactor {
Zero,
One,
SrcColor,
OneMinusSrcColor,
DstColor,
OneMinusDstColor,
SrcAlpha,
OneMinusSrcAlpha,
DstAlpha,
OneMinusDstAlpha,
ConstantColor,
OneMinusConstantColor,
ConstantAlpha,
OneMinusConstantAlpha,
// Dual-source blend factors (GL_SRC1_*, glBindFragDataLocationIndexed); require the
// dualSrcBlend device feature.
Src1Color,
OneMinusSrc1Color,
Src1Alpha,
OneMinusSrc1Alpha,
BlendFactorCount,
Unknown = -1
};
enum class BlendEquation {
Add,
Subtract,
ReverseSubtract,
Min,
Max,
BlendEquationCount,
Unknown = -1
};
enum class LogicOperation {
Clear,
And,
AndReverse,
Copy,
AndInverted,
Noop,
Xor,
Or,
Nor,
Equiv,
Invert,
OrReverse,
CopyInverted,
OrInverted,
Nand,
Set,
LogicOperationCount,
Unknown = -1
};
enum class DepthTestFunc {
Never,
Less,
Equal,
LessEqual,
Greater,
NotEqual,
GreaterEqual,
Always,
DepthTestFuncCount,
Unknown = -1
};
enum class StencilOperation {
Keep,
Zero,
Replace,
IncrementClamp,
DecrementClamp,
Invert,
IncrementWrap,
DecrementWrap,
StencilOperationCount,
Unknown = -1
};
enum class StencilFace {
Front,
Back,
StencilFaceCount,
Unknown = -1
};
enum class PixelStoreParam {
// Pack Parameters
PackAlignment,
PackRowLength,
PackImageHeight,
PackSkipRows,
PackSkipPixels,
PackSkipImages,
PackSwapBytes,
PackLSBFirst,
// Unpack Parameters
UnpackAlignment,
UnpackRowLength,
UnpackImageHeight,
UnpackSkipRows,
UnpackSkipPixels,
UnpackSkipImages,
UnpackSwapBytes,
UnpackLSBFirst,
PixelStoreParamCount,
Unknown = -1
};
enum class CullFaceMode {
Front,
Back,
FrontAndBack,
CullFaceModeCount,
Unknown = -1
};
enum class FrontFaceMode {
CounterClockwise,
Clockwise,
FrontFaceModeCount,
Unknown = -1
};
enum class ProvokingVertexMode {
FirstVertex,
LastVertex,
ProvokingVertexModeCount,
Unknown = -1
};
enum class CapabilityInput {
Blend,
ClipDistance0,
ClipDistance1,
ClipDistance2,
ClipDistance3,
ClipDistance4,
ClipDistance5,
ClipDistance6,
ClipDistance7,
ColorLogicOp,
CullFace,
DebugOutput,
DebugOutputSynchronous,
DepthClamp,
DepthTest,
Dither,
FramebufferSrgb,
LineSmooth,
Multisample,
PolygonOffsetFill,
PolygonOffsetLine,
PolygonOffsetPoint,
PolygonSmooth,
PrimitiveRestart,
PrimitiveRestartFixedIndex,
RasterizerDiscard,
SampleAlphaToCoverage,
SampleAlphaToOne,
SampleCoverage,
SampleShading,
SampleMask,
ScissorTest,
StencilTest,
TextureCubeMapSeamless,
ProgramPointSize,
CapabilityInputCount,
Unknown = -1
};
struct PixelStoreParameters {
Bool SwapBytes = false;
Bool LSBFirst = false;
Int RowLength = 0;
Int ImageHeight = 0;
Int SkipPixels = 0;
Int SkipRows = 0;
Int SkipImages = 0;
Int Alignment = 4;
};
struct PerBufferBlendState {
Bool Enabled = false;
BlendFactor SrcFactorRGB = BlendFactor::One;
BlendFactor DstFactorRGB = BlendFactor::Zero;
BlendFactor SrcFactorAlpha = BlendFactor::One;
BlendFactor DstFactorAlpha = BlendFactor::Zero;
BlendEquation ColorEquation = BlendEquation::Add;
BlendEquation AlphaEquation = BlendEquation::Add;
};
struct StencilFaceState {
DepthTestFunc Func = DepthTestFunc::Always;
Int Ref = 0;
Uint32 ValueMask = 0xffffffffu;
Uint32 WriteMask = 0xffffffffu;
StencilOperation FailOp = StencilOperation::Keep;
StencilOperation PassDepthFailOp = StencilOperation::Keep;
StencilOperation PassDepthPassOp = StencilOperation::Keep;
};
struct RenderStateParameters {
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
static constexpr Uint MAX_VIEWPORTS = 16;
// Rasterization
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
// integers are simply one way to write it. Index 0 is what a program that never assigns
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
// rounded back to integers; the STATE stays exact, which is the half the conformance
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
Float LineWidth = 1.0f;
Float PointSize = 1.0f;
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
Uint PatchVertices = 3;
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL (glPatchParameterfv). The
// tessellation levels used when a program has an evaluation stage and NO control stage -
// GL's fixed-function pass-through (4.6 core 11.2.2). Both backends have to synthesize
// that stage, and they bake these numbers into it, so a change here makes an already-built
// one stale exactly as PATCH_VERTICES does. Default 1.0, per table 23.44.
FloatVec4 PatchDefaultOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
FloatVec2 PatchDefaultInnerLevel = FloatVec2(1.0f, 1.0f);
Float PolygonOffsetFactor = 0.0f;
Float PolygonOffsetUnits = 0.0f;
// GL_POLYGON_OFFSET_CLAMP (GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp): the maximum
// magnitude of the offset glPolygonOffsetClamp's third argument allows. Zero - the default
// - means "no clamp", which is exactly the behaviour glPolygonOffset leaves behind.
Float PolygonOffsetClamp = 0.0f;
// glClipControl (GL 4.5 core 13.5). Defaults per table 23.7 are the pre-4.5 fixed
// behaviour: origin at the lower left, depth mapped from -1..1.
GLenum ClipOrigin = GL_LOWER_LEFT;
GLenum ClipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
// Blending
Array<PerBufferBlendState, kMGMaxDrawBuffers> BlendStates;
LogicOperation LogicOp = LogicOperation::Copy;
// Depth
Bool DepthTestEnabled = false;
DepthTestFunc DepthFunc = DepthTestFunc::Less;
Bool DepthMask = true;
// Color Mask. Per-draw-buffer state (glColorMaski); glColorMask broadcasts to all buffers.
// Every entry is initialized to all-true in RenderState's constructor.
Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
// Clear State
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
Float ClearDepth = 1.0f;
Uint32 ClearStencil = 0;
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
// then lose it again at the same place.
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
Float SampleCoverageValue = 1.0f;
Bool SampleCoverageInvert = false;
Uint32 SampleMaskValue = 0xffffffffu;
// glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples
// that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial
// value is 0, and the value is clamped to [0, 1] on the way in.
Float MinSampleShadingValue = 0.0f;
Array<StencilFaceState, 2> StencilStates{};
// Cull Face
Bool CullFaceEnabled = false;
CullFaceMode CullFaceModeSetting = CullFaceMode::Back;
FrontFaceMode FrontFaceModeSetting = FrontFaceMode::CounterClockwise;
ProvokingVertexMode ProvokingVertexModeSetting = ProvokingVertexMode::LastVertex;
// Hints (glHint). All GL 3.3 core hint targets default to GL_DONT_CARE.
GLenum LineSmoothHint = GL_DONT_CARE;
GLenum PolygonSmoothHint = GL_DONT_CARE;
GLenum TextureCompressionHint = GL_DONT_CARE;
GLenum FragmentShaderDerivativeHint = GL_DONT_CARE;
// Point parameters (glPointParameter). Only the two GL 3.3 core pnames.
Float PointFadeThresholdSize = 1.0f;
GLenum PointSpriteCoordOrigin = GL_UPPER_LEFT;
// Color clamping (glClampColor). Core profile exposes only GL_CLAMP_READ_COLOR.
GLenum ClampReadColor = GL_FIXED_ONLY;
// Polygon rasterization mode (glPolygonMode). Core profile sets front and back together,
// but GL_POLYGON_MODE still reports both slots, so keep them separate for a faithful query.
GLenum PolygonModeFront = GL_FILL;
GLenum PolygonModeBack = GL_FILL;
// Primitive restart index (glPrimitiveRestartIndex); consumed when GL_PRIMITIVE_RESTART is
// enabled during an indexed draw. Default 0.
Uint32 PrimitiveRestartIndex = 0;
// Scissor
Bool ColorLogicOpEnabled = false;
Bool DebugOutputEnabled = false;
Bool DebugOutputSynchronousEnabled = false;
Bool DitherEnabled = true;
Bool LineSmoothEnabled = false;
Bool MultisampleEnabled = true;
Bool PolygonOffsetFillEnabled = false;
Bool PolygonOffsetLineEnabled = false;
Bool PolygonOffsetPointEnabled = false;
Bool PolygonSmoothEnabled = false;
Bool PrimitiveRestartEnabled = false;
Bool PrimitiveRestartFixedIndexEnabled = false;
Bool RasterizerDiscardEnabled = false;
Bool SampleAlphaToCoverageEnabled = false;
Bool SampleAlphaToOneEnabled = false;
Bool SampleCoverageEnabled = false;
Bool SampleMaskEnabled = false;
Bool SampleShadingEnabled = false;
Bool StencilTestEnabled = false;
Bool ProgramPointSizeEnabled = false;
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
// turns it into a real glEnable/glDisable.
Uint32 ScissorTestEnabledMask = 0;
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
// One bit per viewport, set the first time the application writes that index's scissor
// rectangle - glScissor broadcasts and sets all 16, glScissorIndexed/glScissorArrayv set
// the indices they name. It exists because the RECTANGLE cannot answer "has the
// application spoken?": ScissorBoxes starts all-zero (its spec initial value is the size
// of a window the frontend does not know yet, see the RenderState constructor), and
// glScissor(0, 0, 0, 0) is a legal GL state meaning "the scissor test rejects every
// fragment". A backend that reads an empty rectangle as the never-written sentinel
// therefore INVERTS that request into "accept every fragment"; DirectGLES did exactly
// that and KHR-GL43.viewport_array.scissor_zero_dimension caught it. Deliberately beside
// ScissorBoxes so it shares their tail span (after LogicOp) and DirectGLES' span memcmp
// picks a transition up like any other state.
Uint32 ScissorBoxWrittenMask = 0;
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
// eight bools because every consumer wants the set, not an individual flag, and because
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
// eight numbered capabilities cannot share. Lives in the tail span (after LogicOp), so
// DirectGLES' span memcmp picks a change up like any other capability.
Uint32 ClipDistanceEnabledMask = 0;
};
enum class SamplerFilterMode {
Nearest,
Linear,
SamplerFilterCount,
Unknown = -1
};
enum class SamplerMipmapMode {
None,
Nearest,
Linear,
SamplerMipmapModeCount,
Unknown = -1
};
enum class SamplerWrapMode {
ClampToEdge,
MirroredRepeat,
Repeat,
ClampToBorder,
MirrorClampToEdge,
SamplerWrapModeCount,
Unknown = -1
};
enum class SamplerCompareMode {
None,
CompareToTexture,
SamplerCompareModeCount,
Unknown = -1
};
enum class SamplerCompareFunc {
Never,
Less,
Equal,
LessEqual,
Greater,
NotEqual,
GreaterEqual,
Always,
SamplerCompareFuncCount,
Unknown = -1
};
// Which of the three GL_TEXTURE_BORDER_COLOR entry-point families last wrote the border colour,
// and therefore which of the three stored representations is AUTHORITATIVE. GL 4.6 core 8.10:
// TexParameterIiv/Iuiv store an integer border colour "unmodified, with an internal data type of
// integer", TexParameterfv stores a floating-point one, and the derived forms are only a
// convenience for a getter of the other spelling. A backend cannot pick the right driver entry
// point (glSamplerParameterIiv vs fv) or the right VkBorderColor family without this: numerically
// the three representations are always populated, so the value alone says nothing about the form.
enum class BorderColorForm : Uint8 {
Float,
Int,
Uint
};
struct SamplerParameters {
SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
SamplerWrapMode wrapR = SamplerWrapMode::Repeat;
SamplerFilterMode minFilter = SamplerFilterMode::Nearest;
SamplerFilterMode magFilter = SamplerFilterMode::Linear;
SamplerMipmapMode mipmapMode = SamplerMipmapMode::Linear;
Float minLod = -1000.0f;
Float maxLod = 1000.0f;
Float lodBias = 0.0f;
Float maxAnisotropy = 1.0f;
// GL 4.6 core table 23.18 / GLES 3.2 table 21.16: TEXTURE_COMPARE_FUNC starts at LEQUAL,
// for both sampler objects and the sampler state a texture object carries.
SamplerCompareFunc compareFunc = SamplerCompareFunc::LessEqual;
SamplerCompareMode compareMode = SamplerCompareMode::None;
// TEXTURE_BORDER_COLOR is sampler state (GL 4.6 core table 23.18), so it belongs here and
// not on the texture - a texture object reaches it through the sampler object it owns. The
// three representations are the float, integer and unsigned-integer forms glSamplerParameterfv,
// glSamplerParameterIiv and glSamplerParameterIuiv set; whichever is written last defines
// the colour and the other two follow it, so a getter always has an answer.
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
IntVec4 borderColorI = {0, 0, 0, 0};
UintVec4 borderColorUI = {0, 0, 0, 0};
BorderColorForm borderColorForm = BorderColorForm::Float;
};
namespace MG_State::GLState {
class BufferObject;
struct VertexAttribute {
Bool Enabled = false;
int Size = 4;
DataType Type = DataType::Float32;
Bool Normalized = false;
// The RESOLVED byte distance between consecutive elements, never the raw
// glVertexAttrib*Pointer argument: a pointer call's stride 0 means "tightly
// packed" and is resolved to the element size here, so a zero that survives
// into this field can only have come from the binding model, where a zero
// VERTEX_BINDING_STRIDE means the opposite - every vertex reads the SAME
// element and the fetch address never advances (GL 4.6 core 10.3.1). Backends
// consume this verbatim; collapsing 0 back into the element size is what made
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
int Stride = 0;
SizeT Offset = 0;
Bool IsInteger = false;
// GL_BGRA vertex size: four components in reversed (B,G,R,A) memory order. Size stays 4.
// Set only by the long (L) format entry points. It is NOT implied by
// Type == Float64: VertexAttribFormat(GL_DOUBLE) also reads doubles from memory but
// asks for them *converted to float*, while VertexAttribLFormat keeps all 64 bits
// (GL 4.6 core 10.3.2). Backends have to tell the two apart, and it is what
// GL_VERTEX_ATTRIB_ARRAY_LONG reports.
Bool IsLong = false;
Bool IsBgra = false;
Uint Divisor = 0;
SharedPtr<BufferObject> Buffer;
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
// arguments of the last glVertexAttrib*Pointer call on this attribute,
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
// and the binding model does overwrite those, so the two views have to be
// stored apart or the binding-model sequence reports a legacy state it never
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
int LegacyStride = 0;
SizeT LegacyPointer = 0;
};
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
// binding-point API are resolved eagerly into the flat VertexAttribute view above, so
// backends keep consuming resolved attributes and never see binding points.
struct VertexBufferBindingPoint {
SharedPtr<BufferObject> Buffer;
SizeT Offset = 0;
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
int Stride = 16;
Uint Divisor = 0;
};
struct VertexAttributeVersion {
Uint16 FormatVersion = 0;
Uint16 BufferVersion = 0;
Uint16 SwitchVersion = 0;
};
} // namespace MG_State::GLState
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
// therefore not trivially copyable (MGPipeTypes.h carries them as a blob).
static_assert(std::is_trivially_copyable_v<PixelStoreParameters> && sizeof(PixelStoreParameters) == 28);
static_assert(std::is_trivially_copyable_v<PerBufferBlendState> && sizeof(PerBufferBlendState) == 28);
static_assert(std::is_trivially_copyable_v<StencilFaceState> && sizeof(StencilFaceState) == 28);
static_assert(std::is_trivially_copyable_v<RenderStateParameters>);
static_assert(std::is_standard_layout_v<RenderStateParameters>); // offsetof legality
static_assert(sizeof(RenderStateParameters) == 1168,
"RenderStateParameters changed size; MGL_RESIDUAL_BLOCK_SIZE and the Espryt spans depend on it");
static_assert(offsetof(RenderStateParameters, BlendStates) < offsetof(RenderStateParameters, LogicOp));
static_assert(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)> == kMGMaxDrawBuffers);
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
} // namespace MobileGL
#endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H
+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).
+313
View File
@@ -0,0 +1,313 @@
// MobileGL - MobileGL/MG_Pipe/PipeFields.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Field lists for the G4 shadow comparator (plan B section 10.3-2). One macro per payload
// in MGPipeTypes.h, listing the fields that carry MEANING - padding is deliberately absent,
// because MOBILEGL_PIPE_VERIFY has to have ZERO false positives and a padding byte is
// exactly what makes a memcmp of RenderStateParameters false-DIFFER
// (DirectGLES.cpp documents that behaviour where it does the same comparison itself).
//
// Hand maintained alongside MGPipeTypes.h, MGPipeValueTypes.h, MGPipeHostSpan.h and
// MG_Backend/BackendObject.h. Adding a member to one of these structs without adding it here
// would make the comparator blind to it, so gen_pipe.py asserts - in both modes, hence in
// pipe-gates - that every list below names exactly the direct data members of its struct
// (P1 brief D8; a member named Pad<n> is padding and is not listed).
//
// clang-format off
#define MGP_FIELDS_MGPBlobRef(F) \
F(Offset) F(Size) F(Seg)
#define MGP_FIELDS_MGPRange(F) \
F(Offset) F(Size)
#define MGP_FIELDS_MGPBox(F) \
F(X) F(Y) F(Z) F(W) F(H) F(D)
#define MGP_FIELDS_MGPReplySlot(F) \
F(Id)
#define MGP_FIELDS_MGPStateChunk(F) \
F(Offset) F(Length)
#define MGP_FIELDS_MGPHandleOnly(F) \
F(Handle) F(Kind)
#define MGP_FIELDS_MGPCaps(F) \
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
#define MGP_FIELDS_MGPResourceDesc(F) \
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
F(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
F(BufOffset) F(BufSize)
#define MGP_FIELDS_MGPFenceWait(F) \
F(Fence) F(TimeoutNs)
#define MGP_FIELDS_MGPQueryDesc(F) \
F(Query) F(Kind) F(Stream)
#define MGP_FIELDS_MGPQueryResultRequest(F) \
F(Query) F(Wait)
#define MGP_FIELDS_MGPTimestampRequest(F) \
F(Reserved)
#define MGP_FIELDS_MGPRenderStateDesc(F) \
F(Cso) F(BaseCso) F(ChunkMask) F(Blob)
#define MGP_FIELDS_MGPBindRenderState(F) \
F(Cso) F(Version) F(PipelineVersion)
#define MGP_FIELDS_MGPDynamicState(F) \
F(ChunkMask) F(Version) F(Blob)
#define MGP_FIELDS_MGPVertexElements(F) \
F(Cso) F(AttributeCount) F(BindingPointCount) F(Blob)
#define MGP_FIELDS_MGPSamplerDesc(F) \
F(Cso) F(Parameters)
#define MGP_FIELDS_MGPSamplerView(F) \
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
F(Samples) F(FixedSampleLocations)
#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)
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
// gone (a struct without a list is a compile error), and gen_pipe.py asserts every list names
// every direct data member of its struct - Pad-named members are padding and excluded - so a
// member added to RenderStateParameters without a row here fails pipe-gates.
#define MGP_FIELDS_RenderStateParameters(F) \
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
F(DepthMask) F(ColorMasks) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
F(DepthRanges) F(SampleCoverageValue) F(SampleCoverageInvert) F(SampleMaskValue) \
F(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
F(TextureCompressionHint) F(FragmentShaderDerivativeHint) F(PointFadeThresholdSize) \
F(PointSpriteCoordOrigin) F(ClampReadColor) F(PolygonModeFront) F(PolygonModeBack) \
F(PrimitiveRestartIndex) F(ColorLogicOpEnabled) F(DebugOutputEnabled) \
F(DebugOutputSynchronousEnabled) F(DitherEnabled) F(LineSmoothEnabled) F(MultisampleEnabled) \
F(PolygonOffsetFillEnabled) F(PolygonOffsetLineEnabled) F(PolygonOffsetPointEnabled) \
F(PolygonSmoothEnabled) F(PrimitiveRestartEnabled) F(PrimitiveRestartFixedIndexEnabled) \
F(RasterizerDiscardEnabled) F(SampleAlphaToCoverageEnabled) F(SampleAlphaToOneEnabled) \
F(SampleCoverageEnabled) F(SampleMaskEnabled) F(SampleShadingEnabled) F(StencilTestEnabled) \
F(ProgramPointSizeEnabled) F(ScissorTestEnabledMask) F(ScissorBoxes) F(ScissorBoxWrittenMask) \
F(ClipDistanceEnabledMask)
#define MGP_FIELDS_PixelStoreParameters(F) \
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
F(Alignment)
#define MGP_FIELDS_PerBufferBlendState(F) \
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
F(AlphaEquation)
#define MGP_FIELDS_StencilFaceState(F) \
F(Func) F(Ref) F(ValueMask) F(WriteMask) F(FailOp) F(PassDepthFailOp) F(PassDepthPassOp)
#define MGP_FIELDS_DynamicBackendParameters(F) \
F(UniformBufferOffsetAlignment) F(ShaderStorageBufferOffsetAlignment) F(MaxTextureMaxAnisotropy) \
F(AliasedLineWidthRangeMin) F(AliasedLineWidthRangeMax) F(SmoothLineWidthRangeMin) \
F(SmoothLineWidthRangeMax) F(SmoothLineWidthGranularity) F(PointSizeRangeMin) \
F(PointSizeRangeMax) F(PointSizeGranularity) F(Max3DTextureSize) F(MaxArrayTextureLayers) \
F(MaxCubeMapTextureSize) F(MaxFramebufferWidth) F(MaxFramebufferHeight) F(MaxFramebufferLayers) \
F(MaxRenderbufferSize) F(MaxTextureSize) F(MaxColorTextureSamples) F(MaxDepthTextureSamples) \
F(MaxFramebufferSamples) F(MaxIntegerSamples) F(MaxSamples) F(MaxSampleMaskWords) \
F(MaxPatchVertices) F(MaxTessGenLevel) F(MinProgramTextureGatherOffset) \
F(MaxProgramTextureGatherOffset) F(MaxTextureImageUnits) F(MaxVertexTextureImageUnits) \
F(MaxComputeTextureImageUnits) F(MaxCombinedTextureImageUnits) F(MaxVertexAttribs) \
F(MaxComputeShaderStorageBlocks) F(MaxCombinedShaderStorageBlocks) \
F(MaxVertexShaderStorageBlocks) F(MaxTessControlShaderStorageBlocks) \
F(MaxTessEvaluationShaderStorageBlocks) F(MaxGeometryShaderStorageBlocks) \
F(MaxFragmentShaderStorageBlocks) F(MaxComputeUniformBlocks) F(MaxComputeWorkGroupInvocations) \
F(MaxComputeWorkGroupCount) F(MaxComputeWorkGroupSize) F(MaxShaderStorageBufferBindings) \
F(MaxTextureBufferSize) F(TextureBufferOffsetAlignment) F(MaxUniformBufferBindings) \
F(MaxUniformBlockSize) F(MaxImageUnits) F(MaxCombinedImageUniforms) F(MaxVertexImageUniforms) \
F(MaxGeometryImageUniforms) F(MaxFragmentImageUniforms) F(MaxComputeImageUniforms) \
F(MaxDrawBuffers) F(MaxColorAttachments) F(MaxClipDistances) F(MaxCullDistances) \
F(MaxCombinedClipAndCullDistances) F(MaxViewports) F(LayerProvokingVertex) \
F(ViewportIndexProvokingVertex) F(MaxViewportWidth) F(MaxViewportHeight) \
F(ViewportBoundsRangeMin) F(ViewportBoundsRangeMax) F(ViewportSubpixelBits) \
F(MinFragmentInterpolationOffset) F(MaxFragmentInterpolationOffset) \
F(FragmentInterpolationOffsetBits) F(SupportsWideLines) \
F(SupportsDistinctDepthStencilAttachments) F(PerLayerFramebufferAttachmentTargets) \
F(SupportsShaderFloat64) F(SupportsFloat64VertexAttributes) F(SupportsTessellationPointSize) \
F(SupportsGeometryPointSize) F(MaxShaderStorageBlockSize) F(SubgroupSize) \
F(SubgroupSupportedStages) F(SubgroupSupportedFeatures) F(SubgroupQuadOperationsInAllStages) \
F(GpuVendor)
#define MGP_FIELDS_MGHostSpan(F) \
F(Ptr) F(Seg) F(Size) F(Offset)
// Every payload above, in the order the comparator is generated. Keep in sync with the
// macros; gen_pipe.py reads THIS list to know what to emit.
#define MGP_VERIFY_PAYLOAD_LIST(P) \
P(MGPBlobRef) P(MGPRange) P(MGPBox) P(MGPReplySlot) P(MGPStateChunk) P(MGPHandleOnly) P(MGPCaps) \
P(MGPResourceDesc) P(MGPFenceWait) P(MGPQueryDesc) P(MGPQueryResultRequest) P(MGPTimestampRequest) P(MGPRenderStateDesc) \
P(MGPBindRenderState) P(MGPDynamicState) P(MGPVertexElements) P(MGPSamplerDesc) P(MGPSamplerView) \
P(MGPTextureParams) P(MGPProgramDesc) P(MGPSurface) P(MGPFramebufferState) P(MGPVertexBuffer) \
P(MGPVertexBuffers) P(MGPIndexBuffer) P(MGPIndirectBuffers) P(MGPBoundView) P(MGPSamplerViews) \
P(MGPSamplerStates) P(MGPImageView) P(MGPShaderImages) P(MGPBufferRange) P(MGPShaderBuffers) \
P(MGPStreamOutputTargets) P(MGPGlobalConstants) P(MGPAttribValue) P(MGPVertexAttribDefaults) \
P(MGPPixelPackState) P(MGPPatchState) P(ResidualValueBlock) P(MGPResidualValueState) \
P(MGPSubRegion) P(MGPSubData) P(MGPSubDataComplete) P(MGPFlushRange) P(MGPReadback) \
P(MGPCopyRegion) P(MGPBlit) P(MGPClear) P(MGPMipPlan) P(MGPReadbackInfo) P(MGPDrawInfo) \
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
P(MGPSurfaceInfo) \
P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \
P(DynamicBackendParameters) P(MGHostSpan)
// clang-format on
+28
View File
@@ -0,0 +1,28 @@
// MobileGL - MobileGL/MG_Pipe/PipeInputsSwitch.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#ifndef MOBILEGL_MG_PIPE_INPUTS_SWITCH_H // belt and braces: reachable as <MG_Pipe/..> and <..> (CMakeLists.txt:531,535)
#define MOBILEGL_MG_PIPE_INPUTS_SWITCH_H
// The strangler switch (ARCHITECTURE.md 9.2). Every backend read of frontend state is spelled
// MGB_CTX->Accessor(...). Pull arm: the live GLContext, so the pull build is the tree before P1
// token for token. Push arm: the PipeInputs block the frontend fills at every verb boundary.
// The pull arm is the ONLY place under MobileGL/ outside MG_State and MG_Impl that may spell
// pGLContext; purity gate C greps MG_Backend/ for that token.
#if MOBILEGL_PIPE_PUSH
#include <MG_Backend/MGPipe/PipeInputs.h>
#define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#define MGB_CTX_LIVE (::MobileGL::MG_Pipe::gPipeInputs.IsLive())
#define MGB_CTX_IDENTITY (::MobileGL::MG_Pipe::gPipeInputs.ContextIdentity())
#else
#include <MG_State/GLState/Core.h>
#define MGB_CTX (::MobileGL::MG_State::pGLContext)
#define MGB_CTX_LIVE (::MobileGL::MG_State::pGLContext != nullptr)
#define MGB_CTX_IDENTITY (static_cast<const void*>(::MobileGL::MG_State::pGLContext.get()))
#endif
#endif
+42
View File
@@ -0,0 +1,42 @@
// MobileGL - MobileGL/MG_Pipe/PipeMutation.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#ifndef MOBILEGL_MG_PIPE_MUTATION_H // belt and braces: reachable as <MG_Pipe/..> and <..>
#define MOBILEGL_MG_PIPE_MUTATION_H
// Push-on-mutation (P1 lane finding F2). MGP_FILL copies a verb's may-read set out of the
// live GLContext at the verb boundary; the backend then reads that copy for the whole verb.
// A backend that WRITES a frontend object inside its own verb - Magma synthesising a
// fallback texture for an unbound sampler, materialising a queued clear, or overriding a
// sampler's filter - moves a value the boundary already copied, and every read after that
// point sees a block that no longer equals the live context. That is a real divergence, not
// a harness artefact: the pull build reads the moved value and the push build does not.
//
// The frontend mutator that moves such a value spells MGP_NOTE_MUTATION(Field) right where
// it moves it. The notice refreshes that ONE field in the pushed block when the field
// belongs to the verb currently in flight, so "the pushed block equals the live context at
// every read" stays literally true and the push build keeps pull semantics. It refreshes
// the value only and never the poison stamp, so a withheld stamp (MOBILEGL_PIPE_POISON_OMIT,
// negative control B) stays withheld.
//
// In the pull build the macro is ((void)0) and this header includes nothing, so the pull
// build is byte-identical to a tree without it.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext).
// A no-op unless a context is live, a verb has been filled, and `field` is in that verb
// class's may-read mask; a forwarded (sticky) field has no storage and is never copied.
void MGPipeNoteFrontendMutation(MGPipeInputField field);
} // namespace MobileGL::MG_Pipe
#define MGP_NOTE_MUTATION(Field) \
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
#else
#define MGP_NOTE_MUTATION(Field) ((void)0)
#endif
#endif
+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");
@@ -0,0 +1,300 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeFillPoints.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
// G5b: the verb enum, the verb classes and their may-read field masks.
//
// GENERATED by scripts/gen_pipe.py from FillPoints.def, Coverage.def and MG_Backend/BackendObject.h - 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 verb per function-pointer member of MG_Backend::GLFunctionsTable, in declaration
// order, so the enum IS the table's member list. MG_Impl spells MGP_FILL(Verb) before every
// call through the table; MGPipeFillForVerb fills exactly the fields of the verb's class
// (plus the sticky fields, OR'ed into every mask) and stamps them with the new serial. A
// read of any other field is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
enum class MGPipeVerb : Uint8 {
DrawArrays,
DrawElements,
DrawElementsBaseVertex,
MultiDrawArrays,
MultiDrawElements,
MultiDrawElementsBaseVertex,
MultiDrawElementsIndirect,
MultiDrawArraysIndirect,
MultiDrawElementsIndirectCount,
MultiDrawArraysIndirectCount,
DrawRangeElementsBaseVertex,
DrawRangeElements,
DrawElementsInstancedBaseVertexBaseInstance,
DrawElementsInstancedBaseVertex,
DrawElementsInstancedBaseInstance,
DrawElementsInstanced,
DrawArraysInstancedBaseInstance,
DrawArraysInstanced,
DrawElementsIndirect,
DrawArraysIndirect,
Clear,
ClearBufferfi,
ClearBufferfv,
ClearBufferuiv,
ClearBufferiv,
ClearNamedFramebufferfv,
ClearNamedFramebufferfi,
ClearNamedFramebufferiv,
ClearNamedFramebufferuiv,
BlitFramebuffer,
BlitNamedFramebuffer,
CopyTexImage2D,
CopyTexSubImage2D,
CopyImageSubData,
GenerateMipmap,
ReadPixels,
GetTexImage,
GetTextureImage,
DispatchCompute,
DispatchComputeIndirect,
MemoryBarrier,
MemoryBarrierByRegion,
BindImageTexture,
GetIntegeri_v,
ShaderStorageBlockBinding,
FenceSync,
ClientWaitSync,
WaitSync,
DeleteSync,
GetSyncStatus,
IsTimerQuerySupported,
BeginTimeElapsedQuery,
EndTimeElapsedQuery,
QueryCounterTimestamp,
IsQueryResultAvailable,
GetQueryResult64,
DeleteBackendQuery,
BeginOcclusionQuery,
EndOcclusionQuery,
BeginXfbPrimitivesQuery,
EndXfbPrimitivesQuery,
PatchParameteri,
BeginTransformFeedback,
EndTransformFeedback,
PauseTransformFeedback,
ResumeTransformFeedback,
BindTransformFeedback,
DeleteTransformFeedback,
GetGpuTimestampNs,
kVerbCount,
};
inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);
static_assert(kMGPipeVerbCount == 69, "the GLFunctionsTable verb set moved");
inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {
"DrawArrays",
"DrawElements",
"DrawElementsBaseVertex",
"MultiDrawArrays",
"MultiDrawElements",
"MultiDrawElementsBaseVertex",
"MultiDrawElementsIndirect",
"MultiDrawArraysIndirect",
"MultiDrawElementsIndirectCount",
"MultiDrawArraysIndirectCount",
"DrawRangeElementsBaseVertex",
"DrawRangeElements",
"DrawElementsInstancedBaseVertexBaseInstance",
"DrawElementsInstancedBaseVertex",
"DrawElementsInstancedBaseInstance",
"DrawElementsInstanced",
"DrawArraysInstancedBaseInstance",
"DrawArraysInstanced",
"DrawElementsIndirect",
"DrawArraysIndirect",
"Clear",
"ClearBufferfi",
"ClearBufferfv",
"ClearBufferuiv",
"ClearBufferiv",
"ClearNamedFramebufferfv",
"ClearNamedFramebufferfi",
"ClearNamedFramebufferiv",
"ClearNamedFramebufferuiv",
"BlitFramebuffer",
"BlitNamedFramebuffer",
"CopyTexImage2D",
"CopyTexSubImage2D",
"CopyImageSubData",
"GenerateMipmap",
"ReadPixels",
"GetTexImage",
"GetTextureImage",
"DispatchCompute",
"DispatchComputeIndirect",
"MemoryBarrier",
"MemoryBarrierByRegion",
"BindImageTexture",
"GetIntegeri_v",
"ShaderStorageBlockBinding",
"FenceSync",
"ClientWaitSync",
"WaitSync",
"DeleteSync",
"GetSyncStatus",
"IsTimerQuerySupported",
"BeginTimeElapsedQuery",
"EndTimeElapsedQuery",
"QueryCounterTimestamp",
"IsQueryResultAvailable",
"GetQueryResult64",
"DeleteBackendQuery",
"BeginOcclusionQuery",
"EndOcclusionQuery",
"BeginXfbPrimitivesQuery",
"EndXfbPrimitivesQuery",
"PatchParameteri",
"BeginTransformFeedback",
"EndTransformFeedback",
"PauseTransformFeedback",
"ResumeTransformFeedback",
"BindTransformFeedback",
"DeleteTransformFeedback",
"GetGpuTimestampNs",
};
enum class MGPipeVerbClass : Uint8 {
kDraw,
kDispatch,
kClear,
kBlitOrCopy,
kTextureOp,
kReadback,
kXfbSpan,
kProgramOp,
kQuery,
kClassCount,
};
inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);
static_assert(kMGPipeVerbClassCount == 9, "the verb class set moved");
inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {
"kDraw",
"kDispatch",
"kClear",
"kBlitOrCopy",
"kTextureOp",
"kReadback",
"kXfbSpan",
"kProgramOp",
"kQuery",
};
inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {
MGPipeVerbClass::kDraw, // DrawArrays
MGPipeVerbClass::kDraw, // DrawElements
MGPipeVerbClass::kDraw, // DrawElementsBaseVertex
MGPipeVerbClass::kDraw, // MultiDrawArrays
MGPipeVerbClass::kDraw, // MultiDrawElements
MGPipeVerbClass::kDraw, // MultiDrawElementsBaseVertex
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirect
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirect
MGPipeVerbClass::kDraw, // MultiDrawElementsIndirectCount
MGPipeVerbClass::kDraw, // MultiDrawArraysIndirectCount
MGPipeVerbClass::kDraw, // DrawRangeElementsBaseVertex
MGPipeVerbClass::kDraw, // DrawRangeElements
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertexBaseInstance
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseVertex
MGPipeVerbClass::kDraw, // DrawElementsInstancedBaseInstance
MGPipeVerbClass::kDraw, // DrawElementsInstanced
MGPipeVerbClass::kDraw, // DrawArraysInstancedBaseInstance
MGPipeVerbClass::kDraw, // DrawArraysInstanced
MGPipeVerbClass::kDraw, // DrawElementsIndirect
MGPipeVerbClass::kDraw, // DrawArraysIndirect
MGPipeVerbClass::kClear, // Clear
MGPipeVerbClass::kClear, // ClearBufferfi
MGPipeVerbClass::kClear, // ClearBufferfv
MGPipeVerbClass::kClear, // ClearBufferuiv
MGPipeVerbClass::kClear, // ClearBufferiv
MGPipeVerbClass::kClear, // ClearNamedFramebufferfv
MGPipeVerbClass::kClear, // ClearNamedFramebufferfi
MGPipeVerbClass::kClear, // ClearNamedFramebufferiv
MGPipeVerbClass::kClear, // ClearNamedFramebufferuiv
MGPipeVerbClass::kBlitOrCopy, // BlitFramebuffer
MGPipeVerbClass::kBlitOrCopy, // BlitNamedFramebuffer
MGPipeVerbClass::kBlitOrCopy, // CopyTexImage2D
MGPipeVerbClass::kBlitOrCopy, // CopyTexSubImage2D
MGPipeVerbClass::kBlitOrCopy, // CopyImageSubData
MGPipeVerbClass::kTextureOp, // GenerateMipmap
MGPipeVerbClass::kReadback, // ReadPixels
MGPipeVerbClass::kReadback, // GetTexImage
MGPipeVerbClass::kReadback, // GetTextureImage
MGPipeVerbClass::kDispatch, // DispatchCompute
MGPipeVerbClass::kDispatch, // DispatchComputeIndirect
MGPipeVerbClass::kQuery, // MemoryBarrier
MGPipeVerbClass::kQuery, // MemoryBarrierByRegion
MGPipeVerbClass::kTextureOp, // BindImageTexture
MGPipeVerbClass::kQuery, // GetIntegeri_v
MGPipeVerbClass::kProgramOp, // ShaderStorageBlockBinding
MGPipeVerbClass::kQuery, // FenceSync
MGPipeVerbClass::kQuery, // ClientWaitSync
MGPipeVerbClass::kQuery, // WaitSync
MGPipeVerbClass::kQuery, // DeleteSync
MGPipeVerbClass::kQuery, // GetSyncStatus
MGPipeVerbClass::kQuery, // IsTimerQuerySupported
MGPipeVerbClass::kQuery, // BeginTimeElapsedQuery
MGPipeVerbClass::kQuery, // EndTimeElapsedQuery
MGPipeVerbClass::kQuery, // QueryCounterTimestamp
MGPipeVerbClass::kQuery, // IsQueryResultAvailable
MGPipeVerbClass::kQuery, // GetQueryResult64
MGPipeVerbClass::kQuery, // DeleteBackendQuery
MGPipeVerbClass::kQuery, // BeginOcclusionQuery
MGPipeVerbClass::kQuery, // EndOcclusionQuery
MGPipeVerbClass::kQuery, // BeginXfbPrimitivesQuery
MGPipeVerbClass::kQuery, // EndXfbPrimitivesQuery
MGPipeVerbClass::kQuery, // PatchParameteri
MGPipeVerbClass::kXfbSpan, // BeginTransformFeedback
MGPipeVerbClass::kXfbSpan, // EndTransformFeedback
MGPipeVerbClass::kXfbSpan, // PauseTransformFeedback
MGPipeVerbClass::kXfbSpan, // ResumeTransformFeedback
MGPipeVerbClass::kXfbSpan, // BindTransformFeedback
MGPipeVerbClass::kXfbSpan, // DeleteTransformFeedback
MGPipeVerbClass::kQuery, // GetGpuTimestampNs
};
// One bit per MGPipeInputField. The 7 sticky fields are OR'ed into every class.
struct MGPipeFieldMask {
Uint64 Words[2];
};
inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
return (mask.Words[index / 64] >> (index % 64)) & 1u;
}
inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {
// kDraw: 54 fields (47 own + 7 sticky)
{{0x7ffbfff7bfffc3eeull, 0x0000000000000000ull}},
// kDispatch: 22 fields (15 own + 7 sticky)
{{0x5c40f2281d3003c0ull, 0x0000000000000000ull}},
// kClear: 25 fields (18 own + 7 sticky)
{{0x5c50ffa001347900ull, 0x0000000000000000ull}},
// kBlitOrCopy: 29 fields (22 own + 7 sticky)
{{0x5f70ffe0013c4181ull, 0x0000000000000000ull}},
// kTextureOp: 17 fields (10 own + 7 sticky)
{{0x5c40f22001300181ull, 0x0000000000000000ull}},
// kReadback: 24 fields (17 own + 7 sticky)
{{0x5f50f3a041300541ull, 0x0000000000000000ull}},
// kXfbSpan: 15 fields (8 own + 7 sticky)
{{0x7f0b402000000380ull, 0x0000000000000000ull}},
// kProgramOp: 18 fields (11 own + 7 sticky)
{{0x5c50f3a001300100ull, 0x0000000000000000ull}},
// kQuery: 8 fields (1 own + 7 sticky)
{{0x5c04402000000100ull, 0x0000000000000000ull}},
};
static_assert(kMGPipeInputFieldCount <= 2 * 64, "MGPipeFieldMask needs another word");
+321
View File
@@ -0,0 +1,321 @@
// 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).
//
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
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,
GetBoundTransformFeedbackLifetimeId,
HasOpenTransformFeedbackSpan,
kFieldCount,
};
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
static_assert(kMGPipeInputFieldCount == 63, "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",
"GetBoundTransformFeedbackLifetimeId",
"HasOpenTransformFeedbackSpan",
};
// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison
// cannot protect, so every true is argued for in Coverage.def's
// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).
inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetActiveTextureUnit
false, // GetBlendColor
false, // GetBlendEquationIndexed
false, // GetBlendFuncIndexed
false, // GetBoundTransformFeedbackName
false, // GetBoundVertexArray
false, // GetBufferBindingSlot
false, // GetBufferBindingPoint
true, // GetBufferBindingPointCount: keyed by target: a constexpr capacity table, not verb state
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
true, // GetProgramObject: keyed by GL name: an object lookup, not verb state
false, // GetProvokingVertexMode
false, // GetRenderStateParameters
false, // GetRenderStateParametersVersion
false, // GetSamplingResolutionGeneration
false, // GetScissorBox
false, // GetStencilState
false, // GetTextureBindGeneration
false, // GetTextureContextId
true, // GetTextureObject: keyed by GL name: an object lookup, not verb state
false, // GetTextureUnitObject
false, // GetTransformFeedbackCapturedVertices
false, // GetTransformFeedbackGeneration
false, // GetTransformFeedbackPausedPrimitiveCounter
false, // GetTransformFeedbackProgram
false, // GetViewport
false, // GetViewportIndexed
false, // IsCapabilityEnabled
false, // IsCapabilityEnabledIndexed
false, // IsTransformFeedbackActive
false, // IsTransformFeedbackPaused
true, // InvalidateCompileEnv: reverse channel: a write into the frontend, not a state read
true, // ValidateProgramName: keyed by GL name: a name-table lookup, not verb state
true, // RecordError: reverse channel: a write into the frontend, not a state read
false, // GetBoundTransformFeedbackLifetimeId
true, // HasOpenTransformFeedbackSpan: keyed by lifetime id: an object lookup, not verb state
};
inline constexpr SizeT kMGPipeInputStickyFieldCount = 7;
// 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
"SetStreamOutputTargets",
"SetStreamOutputTargets",
};
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();
}
// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the
// serial is 0 as well, and a read in that window is the poison's "<Field>@<none>" case
// (P1 brief D6), never a fresh read of default-constructed storage.
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == 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);
}
+648
View File
@@ -0,0 +1,648 @@
// 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 {};
// A vector type (FloatVec4, IntVec4, BoolVec4...) is detected through its VecBase and
// compared BITWISE over its data: VecBase::operator== is IEEE ==, under which a NaN patch
// level would differ from itself. The probe rather than an overload because a
// derived-to-base conversion loses overload resolution to the exact-match generic template.
template <class Derived, class T, SizeT N>
std::true_type MGPipeVecBaseProbe(const VecBase<Derived, T, N>*);
std::false_type MGPipeVecBaseProbe(const void*);
template <class T>
inline constexpr Bool kMGPipeIsVecBase = decltype(MGPipeVecBaseProbe(static_cast<const T*>(nullptr)))::value;
template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b);
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b);
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]);
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);
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& 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 <>
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<DynamicBackendParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGHostSpan> : 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 (kMGPipeIsVecBase<T>) {
return std::memcmp(a.data.data(), b.data.data(), sizeof(a.data)) == 0;
} 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 {
// NO MEMCMP FALLBACK. Every value struct has a field list in PipeFields.def since P1
// (and gen_pipe.py asserts each list covers its struct's members); a type reaching
// this branch is one nobody gave a field list, and a memcmp would false-differ on
// its padding. A compile error is the honest answer.
static_assert(sizeof(T) == 0, "no field list in PipeFields.def for this type");
return false;
}
}
template <class T, SizeT N>
inline Bool MGPipeFieldEqual(const Array<T, N>& a, const Array<T, N>& b) {
for (SizeT i = 0; i < N; ++i) {
if (!MGPipeFieldEqual(a[i], b[i])) return false;
}
return true;
}
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;
}
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField) {
MGP_FIELDS_PixelStoreParameters(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField) {
MGP_FIELDS_StencilFaceState(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField) {
MGP_FIELDS_DynamicBackendParameters(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField) {
MGP_FIELDS_MGHostSpan(MGP_VERIFY_FIELD)
return true;
}
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
+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

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