Compare commits

...
41 Commits
Author SHA1 Message Date
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
71 changed files with 8693 additions and 1299 deletions
+684 -4
View File
@@ -7,6 +7,14 @@ on:
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
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:
@@ -295,6 +303,343 @@ 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
@@ -322,6 +667,35 @@ jobs:
- 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
@@ -533,6 +907,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
@@ -542,6 +917,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 }})
@@ -755,10 +1133,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
@@ -767,14 +1430,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
@@ -826,6 +1494,17 @@ jobs:
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
@@ -843,7 +1522,8 @@ jobs:
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
# Informational: the frontend mutation surface an MGPipe aggregate generation has to
# cover. It becomes a gate in P1, when the mapping file exists to diff against.
# 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
+28
View File
@@ -22,6 +22,11 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
# 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")
@@ -451,6 +456,22 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
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
@@ -524,6 +545,13 @@ 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
+20
View File
@@ -330,6 +330,26 @@ namespace MobileGL::MG_Config {
// 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;
+7
View File
@@ -244,6 +244,13 @@ namespace MobileGL::MG_ConfigLoader {
// 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
+96 -94
View File
@@ -16,6 +16,7 @@
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
@@ -140,14 +141,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// addresses again - the cached pointers cannot go stale. Invalidation is
// exactly the pointer compare below.
using FbBindingSlot =
std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
static const MG_State::GLState::GLContext* g_fbSlotCacheContext = nullptr;
std::remove_reference_t<decltype(MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
static const void* g_fbSlotCacheContext = nullptr;
static Array<FbBindingSlot*, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fbSlotCache = {};
static inline FbBindingSlot& GetFramebufferBindingSlotFast(FramebufferTarget target) {
MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get();
const void* ctx = MGB_CTX_IDENTITY;
if (ctx != g_fbSlotCacheContext) {
auto& live = *MGB_CTX;
for (SizeT i = 0; i < g_fbSlotCache.size(); ++i) {
g_fbSlotCache[i] = &ctx->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
g_fbSlotCache[i] = &live.GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
}
g_fbSlotCacheContext = ctx;
}
@@ -258,7 +260,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
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);
@@ -354,7 +356,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
// Only sync up to the high-water mark of app-touched points; the fixed array is 84
// deep but apps bind a handful, so the never-touched tail is already at GL default 0.
auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);
auto bindingPointCnt = MGB_CTX->GetTouchedBufferBindingPointCount(target);
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
@@ -367,7 +369,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
}
for (SizeT i = 0; i < bindingPointCnt; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
auto& point = MGB_CTX->GetBufferBindingPoint(target, i);
auto& obj = point.GetBoundObject();
if (!obj) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), 0);
@@ -426,7 +428,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT pointCount = std::min<SizeT>(
bufferCount, MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS);
for (SizeT i = 0; i < pointCount; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback, i);
const auto& obj = point.GetBoundObject();
// A stride-0 slot (two consecutive gl_NextBuffer entries) captures nothing and
// needs no binding; anything else with no buffer never got past the frontend.
@@ -459,10 +461,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// pull the real contents back (BufferObject::SyncGpuWrites).
void MarkShaderStorageBuffersGpuWritten() {
const SizeT bindingPointCnt =
MG_State::pGLContext->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
MGB_CTX->GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
for (SizeT i = 0; i < bindingPointCnt; ++i) {
const auto& obj =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
MGB_CTX->GetBufferBindingPoint(BufferTarget::ShaderStorage, i).GetBoundObject();
if (obj) obj->MarkGpuWritten();
}
}
@@ -471,14 +473,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
const SizeT pointCount = MGB_CTX->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
for (const Int glBinding : glBindings) {
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to it
// there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::AtomicCounter,
static_cast<Uint>(glBinding));
auto& obj = point.GetBoundObject();
if (!obj) {
@@ -515,7 +517,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& bufferObject = MG_State::pGLContext->GetBufferBindingSlot(target).GetBoundObject();
auto& bufferObject = MGB_CTX->GetBufferBindingSlot(target).GetBoundObject();
if (!bufferObject) {
g_GLESFuncs.glBindBuffer(glTarget, 0);
return;
@@ -659,7 +661,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// context since indirect draws now execute natively on the GPU.
if (includeIndirectBuffer) {
auto& possibleIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (possibleIndirectBuffer) {
SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER);
}
@@ -889,7 +891,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT packedStride = program->GetTransformFeedbackPackedStride();
const SizeT modelledVertices =
static_cast<SizeT>(MG_State::pGLContext->GetTransformFeedbackCapturedVertices());
static_cast<SizeT>(MGB_CTX->GetTransformFeedbackCapturedVertices());
const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
if (packedStride == 0 || vertices == 0) {
// The scatter path redirected the DRIVER's capture into the scratch buffer,
@@ -984,7 +986,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// not captured, and opening the span would also subject it to the capture
// primitive-mode rule the paused draw is exempt from.
if (!xfb.pending || xfb.paused) return;
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
if (!program) {
// The pending flag is deliberately NOT consumed here. It used to be cleared
// before this check, so a single draw that could not see the capture program
@@ -1004,7 +1006,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// recording it here keeps End independent of the frontend capture state.
const SizeT bufferCount = program->GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < bufferCount; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
const auto& bufferObject = point.GetBoundObject();
if (!bufferObject) continue;
@@ -1241,7 +1243,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
if (!program) return;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = MGB_CTX->GetBoundVertexArray();
if (!vao || !vaoTwin) return;
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
@@ -1272,7 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (Uint32 remaining = memo.pendingMask; remaining != 0; remaining &= remaining - 1) {
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
const auto& currentValue = MGB_CTX->GetCurrentVertexAttribute(location);
const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location));
switch (typeInfo.baseType) {
case MG_State::GLState::VertexAttribBaseType::Float:
@@ -1370,7 +1372,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static void CaptureUnitBindings(Int maxTouchedUnit, Vector<UnitBindingsSnapshot>& out) {
out.resize(static_cast<SizeT>(maxTouchedUnit + 1));
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
auto& snapshot = out[static_cast<SizeT>(unit)];
const auto& slots = textureUnit.GetAllBindingSlots();
for (SizeT i = 0; i < slots.size(); ++i) {
@@ -1383,7 +1385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool UnitBindingsUnchanged(Int maxTouchedUnit, const Vector<UnitBindingsSnapshot>& snapshots) {
if (snapshots.size() != static_cast<SizeT>(maxTouchedUnit + 1)) return false;
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const auto& snapshot = snapshots[static_cast<SizeT>(unit)];
const auto& slots = textureUnit.GetAllBindingSlots();
for (SizeT i = 0; i < slots.size(); ++i) {
@@ -1411,8 +1413,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bump - so epoch equality alone proves the bindings a consumer resolved against
// are the bindings on the units now.
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
const Uint64 contextId = MG_State::pGLContext->GetTextureContextId();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
const Uint64 contextId = MGB_CTX->GetTextureContextId();
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
if (MG_Util::PipeStats::Enabled()) {
// Two accessor calls whichever way the shutter goes; only the unit WALK is
// gated, and that walk reads no GLContext accessor of its own.
@@ -1512,10 +1514,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawTextureSyncKeys CaptureDrawTextureSyncKeys() {
DrawTextureSyncKeys keys;
keys.contextId = MG_State::pGLContext->GetTextureContextId();
keys.contextId = MGB_CTX->GetTextureContextId();
// Units past the frontend's high-water mark have provably-empty slots.
keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
keys.maxTouchedUnit = MGB_CTX->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
if (MG_Util::PipeStats::Enabled()) {
// The three reads above; CurrentUnitBindingsEpoch counts its own two.
@@ -1577,7 +1579,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_unitTextureSyncListValid = false;
g_unitTextureSyncList.clear();
for (Int index = 0; index <= maxTouchedUnit; ++index) {
auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);
auto& unit = MGB_CTX->GetTextureUnitObject(index);
for (const auto& bindingSlot : unit.GetAllBindingSlots()) {
auto& textureObject = bindingSlot.GetBoundObject();
// An image-less default texture (name 0) is the slot's initial / "unbound"
@@ -1725,7 +1727,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
@@ -1817,7 +1819,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_writableImageBufferUnitCount == 0) return;
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
if (!g_writableImageBufferUnits[unit]) continue;
const auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
const auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
if (!IsWritableImageBufferTexture(imageBinding)) {
TrackWritableImageBufferUnit(unit, false);
continue;
@@ -2023,7 +2025,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Uint16 currentRenderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
Uint16 currentRenderStateVersion = MGB_CTX->GetRenderStateParametersVersion();
const Bool forceFullPush = g_forceFullRenderStateResync;
g_forceFullRenderStateResync = false;
// The alpha discipline for widened colour attachments (see the header comment on
@@ -2051,7 +2053,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
const auto& parameters = MGB_CTX->GetRenderStateParameters();
// The frontend has ONE version for the whole parameter block, so a per-draw blend
// toggle used to re-diff all ~40 pieces of state field by field on every draw
@@ -2080,7 +2082,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
IntVec4 backendViewport = MGB_CTX->GetViewport();
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
Int surfaceWidth = 0;
Int surfaceHeight = 0;
@@ -2163,7 +2165,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// never turns it on, so the driver has to be told to write raw. Without this a render
// into an sRGB colour buffer comes back encoded once too often (the shader's own
// decode on the next fetch then leaves the value one conversion short).
const Bool srgbWrites = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
const Bool srgbWrites = MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB)
@@ -2839,11 +2841,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// member set it has to be told.
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
(twin->GetPassthroughTessControlPatchVertices() !=
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
static_cast<Int>(MGB_CTX->GetPatchVertices()) ||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
MGB_CTX->GetPatchDefaultOuterLevel()) ||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
MGB_CTX->GetPatchDefaultInnerLevel())))) {
twin->SyncToBackend(currentProgram);
}
g_currentDrawFrontendProgram = currentProgram.get();
@@ -2954,7 +2956,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// resolved-buffers memo on the twin), the VAO sync and the draw-time bind
// below. Nothing in between can invalidate it — the bound VAO is pinned by
// the context, and no step here erases or replaces a live VAO's twin.
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
// Early config-version read: see the note on SyncNeccessaryBuffers - issuing
@@ -2965,7 +2967,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// either, and none can run inside this preparation. GetProgramForDraw is a
// cross-TU call with a guarded static inside - repeating it per stage showed
// up in draw-loop profiles.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
if (MG_Util::PipeStats::Enabled()) {
// THE per-draw denominator for Espryt, plus this function's own two accessor
// calls (the VAO and the draw program). Everything the callees below read is
@@ -3045,7 +3047,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
Array<TextureTarget, (SizeT)TextureTarget::TextureTargetCount> claimedByFrontendTarget{};
claimedByFrontendTarget.fill(TextureTarget::Unknown);
@@ -3209,7 +3211,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
g_unitSamplerWalkValid = false;
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
const auto& samplerObject = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
if (samplerObject) {
if (auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject)) {
backendSampler->Bind(unit);
@@ -3359,7 +3361,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
void BindCurrentTextures() {
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MG_State::pGLContext->GetProgramForDraw());
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), MGB_CTX->GetProgramForDraw());
}
// Binds the current program's backend object and re-establishes its per-program
@@ -3479,7 +3481,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Connect buffer to backend binding point
auto binding = currentProgram->GetUniformBlockBinding(i);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::Uniform, binding);
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, binding);
auto& bufferObj = point.GetBoundObject();
auto range = point.GetRange();
@@ -3577,7 +3579,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
samplerBinding.lastAssignedUnit = unit;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
auto& samplerObject = textureUnit.GetSamplerObject();
const auto& texture2D =
textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
@@ -3659,7 +3661,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PrepareForCompute, where the current program (and therefore its registry twin)
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
return nullptr;
}
@@ -3709,7 +3711,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
// so an unanswerable program counts as needing them.
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
const auto program = GetCurrentBackendProgram();
if (!currentProgram || program == nullptr ||
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
@@ -3818,12 +3820,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// times; rasterizer discard means there are no fragments to gate at all, so replaying
// would be pure cost with nothing to show for it. Both fall back to a single pass with an
// open gate, i.e. to the pre-emulation behaviour, rather than to wrong data.
if (MG_State::pGLContext->IsTransformFeedbackActive() ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
if (MGB_CTX->IsTransformFeedbackActive() ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return 1;
}
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
const auto& parameters = MGB_CTX->GetRenderStateParameters();
Int surfaceWidth = 0;
Int surfaceHeight = 0;
if (!QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
@@ -4059,7 +4061,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a
// pipeline bound this is its compute stage program, which is a whole program on its
// own - the graphics composite a draw builds carries no compute stage.
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
const auto& currentProgram = MGB_CTX->GetProgramForDispatch();
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
@@ -4085,12 +4087,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
GLuint GetBackendProgramId(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
if (!MGB_CTX->ValidateProgramName(program)) {
MGLOG_E_ONCE("Invalid frontend program object: %u", program);
return 0;
}
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
auto& programObject = MGB_CTX->GetProgramObject(program);
if (!programObject) {
MGLOG_E_ONCE("Program object %u is null.", program);
return 0;
@@ -4153,7 +4155,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// color must go through glClearBufferfv, which GLES does not clamp.
GLbitfield remainingMask = mask;
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
const FloatVec4& cc = MG_State::pGLContext->GetRenderStateParameters().ClearColor;
const FloatVec4& cc = MGB_CTX->GetRenderStateParameters().ClearColor;
const Bool outOfRange = cc.x() < 0.f || cc.x() > 1.f || cc.y() < 0.f || cc.y() > 1.f || cc.z() < 0.f ||
cc.z() > 1.f || cc.w() < 0.f || cc.w() > 1.f;
// A widened attachment's stored alpha has to end up 1.0, and glClear applies ONE
@@ -4212,7 +4214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb);
const GLenum rbErr = g_GLESFuncs.glGetError();
const auto& feFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
int feDb0 = -1, feDb1 = -1;
Uint feIdx = 0, feVer = 0;
if (feFbo) {
@@ -4377,7 +4379,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
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();
}
@@ -4393,13 +4395,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
return RestartSubstitutionKind::None;
}
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
if (fixedMax == 0) return RestartSubstitutionKind::None;
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
const Uint32 restartIndex = MGB_CTX->GetPrimitiveRestartIndex();
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
@@ -4439,7 +4441,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
const Uint32 applicationRestartIndex = MGB_CTX->GetPrimitiveRestartIndex();
const auto& indexBuffer = BoundElementArrayBuffer();
const Uint8* source = nullptr;
@@ -4567,7 +4569,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
DrawSyncFlags syncBit = DrawSyncBit::None;
PrepareForDraw(syncBit);
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
if (currentVAO) {
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
if (backendVAOSlot && *backendVAOSlot) {
@@ -4606,7 +4608,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ladder and the indirect executors do. Without it every sub-draw of a
// glMultiDrawArrays read draw index 0.
const Bool feedDrawID = CurrentProgramReadsDrawID();
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
for (GLsizei i = 0; i < drawcount; ++i) {
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
if (currentVAO) {
@@ -4677,7 +4679,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
}
@@ -4708,8 +4710,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
return;
@@ -4779,7 +4781,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
drawcount, stride, "MultiDrawArraysIndirect");
}
@@ -4810,8 +4812,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
return;
@@ -4970,7 +4972,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
"DrawElementsIndirect");
@@ -5011,7 +5013,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
}
@@ -5256,8 +5258,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// restore. Drop the flag so it is not misattributed to the emulation's own work.
DrainBlitErrors();
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
if (MGB_CTX->IsTransformFeedbackActive() &&
!MGB_CTX->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
g_GLESFuncs.glPauseTransformFeedback();
m_pausedTransformFeedback = true;
DrainBlitErrors();
@@ -6041,8 +6043,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A no-op on every driver that honours a non-zero destination array layer, which is all
// of them but the probed one. Whatever it performs itself is taken out of the mask.
mask &= ~BlitLayeredDestinationAspects(
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask);
if (mask != 0) {
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
@@ -6104,8 +6106,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedNC(__func__, TRACY_ZONECOLOR_BACKEND);
#endif
auto unit = MG_State::pGLContext->GetActiveTextureUnit();
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto unit = MGB_CTX->GetActiveTextureUnit();
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) {
@@ -6182,7 +6184,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The frontend's current PACK parameters, for readbacks the ES driver serves
// directly with the client's layout.
static PixelStoreImpl::PackState PackStateFromContext() {
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
return {static_cast<GLint>(packParams.Alignment), static_cast<GLint>(packParams.RowLength),
static_cast<GLint>(packParams.SkipRows), static_cast<GLint>(packParams.SkipPixels)};
}
@@ -6372,7 +6374,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertTextureInternalFormatToString(format).c_str());
MG_State::pGLContext->RecordError(
MGB_CTX->RecordError(
ConvertGLESErrorToErrorCode(err),
MakeUnique<GenericErrorInfo>("DirectGLES", operation,
MG_Util::ConvertGLEnumToString(err)));
@@ -6696,8 +6698,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Bind necessary FBO and texture
BindCurrentFBO(FramebufferTarget::Read);
Uint activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit)
Uint activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
@@ -6791,8 +6793,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Bind necessary FBO and texture
BindCurrentFBO(FramebufferTarget::Read);
auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
@@ -6929,8 +6931,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
DebugImpl::OpenGLScopeMarker marker(__func__);
#endif
auto unitIndex = MG_State::pGLContext->GetActiveTextureUnit();
auto& unit = MG_State::pGLContext->GetTextureUnitObject(unitIndex);
auto unitIndex = MGB_CTX->GetActiveTextureUnit();
auto& unit = MGB_CTX->GetTextureUnitObject(unitIndex);
auto& slot = unit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target));
auto& texture = slot.GetBoundObject();
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
@@ -7354,8 +7356,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// effect by the block's next use.
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
if (!storageBlockName) return;
if (!MG_State::pGLContext->ValidateProgramName(program)) return;
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
if (!MGB_CTX->ValidateProgramName(program)) return;
auto& programObject = MGB_CTX->GetProgramObject(program);
if (!programObject) return;
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
@@ -7551,7 +7553,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
template <typename FillRow>
static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels,
const char* what, FillRow&& fillRow) {
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 = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
@@ -7559,7 +7561,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT rowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT packedSize = dstOffset + static_cast<SizeT>(height - 1) * dstRowStride + rowBytes;
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what);
@@ -8541,7 +8543,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (!pixelPackBufferObject && pixels == nullptr) {
return true;
}
@@ -8771,7 +8773,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (!pixelPackBufferObject && pixels == nullptr) {
return true;
}
@@ -9038,7 +9040,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// and legacy GL_RED reads) goes through the wide-format conversion, which picks a wide type
// the driver accepts for the current attachment. GL_PACK_SWAP_BYTES has no ES equivalent, so
// it always takes the conversion path (which swaps on the CPU).
const Bool packSwapBytes = MG_State::pGLContext->GetPixelStoreParameters(false).SwapBytes;
const Bool packSwapBytes = MGB_CTX->GetPixelStoreParameters(false).SwapBytes;
// The read buffer is what glReadPixels reads, so the frontend's READ binding is exactly
// the right thing to ask here.
const Bool forceOpaqueAlpha = FramebufferImpl::IsAlphaWidenedFallbackReadAttachment();
@@ -9081,7 +9083,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (the driver-level binding used to stay on the user PBO after this call,
// capturing subsequent client-memory readbacks into it).
auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
Bool usePBO = false;
GLuint packBufferId = 0;
if (pixelPackBufferObject) {
@@ -9191,10 +9193,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("GetTexImage: SyncCurrentFBO()");
FramebufferImpl::SyncCurrentFBO();
auto activeTextureUnit = MG_State::pGLContext->GetActiveTextureUnit();
auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
MGLOG_D("GetTexImage: active texture unit = %u", activeTextureUnit);
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit)
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
@@ -9417,7 +9419,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Each slice is packed as its own 2D image, so the per-slice call must not apply
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
// over them, using the same layout StoreWideRowsToClient computes.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
const SizeT dstPixelBytes = GetReadbackDstPixelSize(conversionMapping, type);
const SizeT rowPixels =
static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : size.x());
@@ -9507,7 +9509,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Handle PBO. The pack binding is scoped: it returns to the resting 0 state
// on every exit path, so a later readback can never land in a stale PBO.
auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
Bool usePBO = false;
GLuint packBufferId = 0;
if (pixelPackBufferObject) {
+24 -23
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Managers.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "Utils.h"
#include "DirectGLES.h"
#include "BackendObject_DirectGLES.h"
@@ -3641,9 +3642,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// that needs no work costs the same nothing per draw that any other synced texture does.
void BackendTextureObject::StampViewSyncKeys(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (MG_State::pGLContext) {
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
if (MGB_CTX_LIVE) {
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
m_syncedContentVersion = stateTextureObject->GetContentVersion();
@@ -3770,9 +3771,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// version - and backend-side storage resets clear m_isInitialized. Restricted to
// Mipmap storage like the probe fast path: a buffer texture's backing store can move
// without any of these keys noticing.
if (m_isInitialized && m_syncedShapeContextId != 0 && MG_State::pGLContext &&
m_syncedShapeContextId == MG_State::pGLContext->GetTextureContextId() &&
m_syncedShapeGeneration == MG_State::pGLContext->GetSamplingResolutionGeneration() &&
if (m_isInitialized && m_syncedShapeContextId != 0 && MGB_CTX_LIVE &&
m_syncedShapeContextId == MGB_CTX->GetTextureContextId() &&
m_syncedShapeGeneration == MGB_CTX->GetSamplingResolutionGeneration() &&
m_syncedContentVersion == stateTextureObject->GetContentVersion() &&
m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() &&
stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
@@ -3841,9 +3842,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The probe just proved "fully synced" from the real state, so the cheap
// gate may be (re)stamped here: the coarse generation only ever goes stale
// from OTHER textures' churn, and this draw re-validated this one.
if (MG_State::pGLContext) {
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
if (MGB_CTX_LIVE) {
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
return;
@@ -4732,9 +4733,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Same instant, so the cheap gate's keys describe exactly this synced state.
// Only Mipmap storage may arm it - the gate refuses other storage types anyway,
// but a stale trio must not linger on an object that later switches type.
if (MG_State::pGLContext && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
if (MGB_CTX_LIVE && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
m_syncedShapeContextId = MGB_CTX->GetTextureContextId();
m_syncedShapeGeneration = MGB_CTX->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
} else {
m_syncedShapeContextId = 0;
@@ -5352,7 +5353,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// read buffer names no colour attachment at all.
static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() {
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return nullptr;
}
@@ -5457,7 +5458,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool IsFixedPointFallbackReadAttachment() {
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return false;
}
@@ -6227,7 +6228,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// outside the frontend's array, which cannot be addressed at all.
Uint BoundImageUnitFormat(Int unit) {
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);
return static_cast<Uint>(MGB_CTX->GetImageTextureBinding(unit).Format);
}
// Combines one (unit, format) pair into a running digest. Commutative, so the order
@@ -7189,19 +7190,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// patch size - so a program built for one value is stale for another. Recorded here
// and compared on the draw path (SyncCurrentProgram), the same shape as the
// storage-block and image-format signatures next to it.
const Uint patchVertices = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetPatchVertices()
const Uint patchVertices = MGB_CTX_LIVE
? MGB_CTX->GetPatchVertices()
: 3u;
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
// into the same stage (ES has no such state and no entry point to forward them to), so
// they are recorded and compared alongside the patch size - the two move together, as
// BuildPassthroughTessControlEssl's contract says.
m_passthroughTessControlOuterLevel = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetPatchDefaultOuterLevel()
m_passthroughTessControlOuterLevel = MGB_CTX_LIVE
? MGB_CTX->GetPatchDefaultOuterLevel()
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
m_passthroughTessControlInnerLevel = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetPatchDefaultInnerLevel()
m_passthroughTessControlInnerLevel = MGB_CTX_LIVE
? MGB_CTX->GetPatchDefaultInnerLevel()
: FloatVec2(1.0f, 1.0f);
if (tessEvalShaderIndex < 0 ||
@@ -8747,8 +8748,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
stateRBOObject->GetExternalIndex(), width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
if (MG_State::pGLContext) {
MG_State::pGLContext->RecordError(
if (MGB_CTX_LIVE) {
MGB_CTX->RecordError(
ErrorCode::OutOfMemory,
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
"The ES driver could not allocate the renderbuffer storage."));
+6 -5
View File
@@ -9,6 +9,7 @@
#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>
@@ -42,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
@@ -84,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;
@@ -92,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();
}
+3 -2
View File
@@ -17,6 +17,7 @@
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -2294,11 +2295,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
@@ -12,6 +12,7 @@
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -385,8 +386,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
@@ -783,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();
}
@@ -10,6 +10,7 @@
#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"
@@ -267,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);
@@ -334,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;
@@ -399,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;
@@ -442,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;
@@ -462,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;
@@ -493,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;
@@ -520,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) {
@@ -530,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;
@@ -564,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;
@@ -579,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;
@@ -613,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,
@@ -628,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);
}
@@ -713,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;
@@ -738,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);
}
@@ -783,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);
@@ -814,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) {
@@ -839,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;
@@ -862,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;
}
@@ -907,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) {
@@ -968,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)) {
@@ -998,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);
}
@@ -1233,8 +1234,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
!query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) {
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
*outNanoseconds = primitives;
@@ -1281,7 +1282,7 @@ 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();
@@ -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"
@@ -503,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;
@@ -552,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 &&
@@ -779,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;
@@ -809,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(),
@@ -818,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
@@ -834,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(),
@@ -844,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).
@@ -989,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(),
@@ -1013,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
@@ -1149,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);
@@ -1186,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
@@ -1263,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);
@@ -1292,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
@@ -1648,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(),
@@ -1659,7 +1660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
MG_State::GLState::ITextureObject* texture =
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
@@ -1803,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.
@@ -1847,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
@@ -1885,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;
@@ -1935,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 &&
@@ -1965,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,
@@ -2010,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'",
@@ -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
@@ -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,6 +11,7 @@
#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"
@@ -806,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 {
@@ -949,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 =
@@ -14,6 +14,7 @@
#include "VertexInputStateBuilder.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/SamplerState/SamplerObject.h"
@@ -460,12 +461,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// round trip), which is why the honest-but-lossy path was kept over widening every
// default-framebuffer Y-flip/pre-transform helper to floats. See the KNOWN INFIDELITY
// note in MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp.
const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);
const FloatVec4& stored = MGB_CTX->GetViewportIndexed(index);
const IntVec4 viewportState(static_cast<Int>(std::lround(stored.x())),
static_cast<Int>(std::lround(stored.y())),
static_cast<Int>(std::lround(stored.z())),
static_cast<Int>(std::lround(stored.w())));
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);
const FloatVec2& depthRange = MGB_CTX->GetDepthRangeIndexed(index);
const IntVec2 logicalExtent = isDefaultFramebuffer
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
: framebufferExtent;
@@ -520,7 +521,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void ApplyBlendConstants(VkCommandBuffer commandBuffer) {
const FloatVec4& blendColor = MG_State::pGLContext->GetBlendColor();
const FloatVec4& blendColor = MGB_CTX->GetBlendColor();
const float blendConstants[4] = {
blendColor.x(),
blendColor.y(),
@@ -553,8 +554,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void ApplyPolygonOffsetState(VkCommandBuffer commandBuffer) {
const Float constantFactor = MG_State::pGLContext->GetPolygonOffsetUnits();
const Float slopeFactor = MG_State::pGLContext->GetPolygonOffsetFactor();
const Float constantFactor = MGB_CTX->GetPolygonOffsetUnits();
const Float slopeFactor = MGB_CTX->GetPolygonOffsetFactor();
auto& shadow = g_dynamicStateShadow;
if (shadow.depthBiasValid && shadow.depthBiasConstantFactor == constantFactor &&
shadow.depthBiasSlopeFactor == slopeFactor) {
@@ -567,7 +568,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void ApplyLineWidthState(VkCommandBuffer commandBuffer) {
Float lineWidth = MG_State::pGLContext->GetLineWidth();
Float lineWidth = MGB_CTX->GetLineWidth();
if (MG_Backend::pActiveBackendObject != nullptr) {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin;
@@ -646,8 +647,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void ApplyStencilState(VkCommandBuffer commandBuffer) {
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back);
const Uint32 frontReference = static_cast<Uint32>(std::max(frontStencil.Ref, 0));
const Uint32 backReference = static_cast<Uint32>(std::max(backStencil.Ref, 0));
@@ -1240,11 +1241,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void RecordClearBufferError(const char* func, ErrorCode code, const char* message) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
MGB_CTX->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
}
static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
MGB_CTX->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
}
static Bool HasDistinctCompleteDepthStencilTextureAttachments(
@@ -1300,7 +1301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static void RecordUnsupportedFramebufferError(const char* func) {
MG_State::pGLContext->RecordError(
MGB_CTX->RecordError(
ErrorCode::InvalidFramebufferOperation,
MakeUnique<GenericErrorInfo>(
"DirectVulkan", func,
@@ -2768,7 +2769,7 @@ void main() {
// glReadPixels final conversion: GL_CLAMP_READ_COLOR defaults to GL_FIXED_ONLY,
// clamping fixed-point (normalized) buffers to [0,1] - visible for SNORM reads.
if (applyReadColorClamp && wideType == GL_FLOAT) {
const GLenum clampMode = MG_State::pGLContext->GetClampReadColor();
const GLenum clampMode = MGB_CTX->GetClampReadColor();
const Bool clamp = clampMode == GL_TRUE ||
(clampMode == GL_FIXED_ONLY && !IsFloatingPointReadbackFormat(srcFormat));
if (clamp) {
@@ -2985,7 +2986,7 @@ void main() {
inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) {
ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap;
const auto& currentDrawFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer()) {
flags |= ProgramFactory::CompileOptionBit::PositionYFlip;
// gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow
@@ -3445,8 +3446,8 @@ void main() {
// with restart off it is a legitimate index and excluding it would truncate the
// converted stream by exactly that vertex.
const Bool primitiveRestartActive =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
const Uint32 restartSentinel = indexSize == 1 ? 0xFFu : indexSize == 2 ? 0xFFFFu : 0xFFFFFFFFu;
Uint32 maxIndex = 0;
Bool sawIndex = false;
@@ -3923,7 +3924,7 @@ void main() {
}
const auto glType = programObj.vertexInputTypes[location];
const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location);
const auto& currentValue = MGB_CTX->GetCurrentVertexAttribute(location);
VkFormat format = VK_FORMAT_UNDEFINED;
const void* sourceData = nullptr;
VkDeviceSize sourceSize = 0;
@@ -4057,7 +4058,7 @@ void main() {
Bool substituteRestart = false;
// One bulk parameters fetch instead of up to three accessor calls per indexed
// draw; all three inputs are pure reads of these fields.
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) {
const Uint32 restartIndex = rsp.PrimitiveRestartIndex;
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
@@ -4812,9 +4813,9 @@ void main() {
Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const {
constexpr Uint32 kFullCoverage = 0xffffffffu;
if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage;
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
return MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue;
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage;
if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage;
return MGB_CTX->GetRenderStateParameters().SampleMaskValue;
}
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount,
@@ -4825,7 +4826,7 @@ void main() {
// field (RenderState.cpp), so the hashed values are bit-identical. This runs on
// every draw whose pipeline-state version moved (a per-draw GL_BLEND toggle),
// where the accessor-call overhead dominated the hash itself.
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters();
Uint64 capabilityBits = 0;
capabilityBits |= p.CullFaceEnabled ? 1ull << 0 : 0;
capabilityBits |= p.DepthTestEnabled ? 1ull << 1 : 0;
@@ -4937,7 +4938,7 @@ void main() {
if (!(aspects & DrawSetupAspect::IndexBuffer) || pIndexBufferView == nullptr) {
return false;
}
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
if (rsp.PrimitiveRestartFixedIndexEnabled) {
return true;
}
@@ -4981,7 +4982,7 @@ void main() {
// the VALUE hash of that subset, never the version itself: the version is monotonic, so
// per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds.
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount ||
m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) {
@@ -5169,22 +5170,22 @@ void main() {
syntheticVertexInputState.pNext = vis.state.pNext;
pipelineVertexInputState = &syntheticVertexInputState;
}
auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);
auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);
auto cullFaceEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::CullFace);
auto depthTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest);
auto polygonOffsetFillEnabled =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) &&
DrawModeUsesPolygonFill(mode);
auto rasterizerDiscardEnabled =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);
MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard);
auto colorLogicOpEnabled =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;
auto stencilTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
MGB_CTX->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled;
auto stencilTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest);
// A framebuffer without a depth (stencil) attachment behaves as if the depth
// (stencil) test always passes and nothing is written - even when the bound
// image is a packed depth-stencil texture attached through only one half.
{
const auto& gatingFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) {
const auto& depthAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Depth);
const auto& stencilAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Stencil);
@@ -5196,10 +5197,10 @@ void main() {
}
}
}
const StencilFaceState& frontStencil = MG_State::pGLContext->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MG_State::pGLContext->GetStencilState(StencilFace::Back);
const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front);
const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back);
const VkPolygonMode requestedPolygonMode =
MG_Util::ConvertPolygonModeToVkEnum(MG_State::pGLContext->GetPolygonModeFront());
MG_Util::ConvertPolygonModeToVkEnum(MGB_CTX->GetPolygonModeFront());
// VK_POLYGON_MODE_LINE/_POINT require the fillModeNonSolid device feature; fall back to
// VK_POLYGON_MODE_FILL when the device lacks it.
const VkPolygonMode effectivePolygonMode =
@@ -5284,18 +5285,18 @@ void main() {
// driver's own rate. Both halves move the render state's PIPELINE version, so a cached
// pipeline built at the old rate cannot be handed back for the new one.
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleShading),
.minSampleShading = MGB_CTX->GetMinSampleShadingValue(),
// Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63.
.sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu},
.subpass = 0,
.topology = vkTopology,
.primitiveRestartEnable = primitiveRestartEnabled,
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
.patchControlPoints = static_cast<Uint32>(MGB_CTX->GetPatchVertices()),
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
.polygonMode = effectivePolygonMode,
.cullMode = cullFaceEnabled
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
? MG_Util::ConvertCullFaceModeToVkEnum(MGB_CTX->GetCullFaceMode(), invertClockwise)
: VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_CLOCKWISE,
// Read the geometry stage off the program's own shader list rather than
@@ -5309,13 +5310,13 @@ void main() {
.provokingVertexMode = SelectProvokingVertexMode(
vkTopology, ProgramCapturesXfbFromGeometryStage(program)),
.depthTestEnable = depthTestEnabled,
.depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(),
.depthWriteEnable = depthTestEnabled && MGB_CTX->GetDepthMask(),
.depthBiasEnable = polygonOffsetFillEnabled,
.rasterizerDiscardEnable = rasterizerDiscardEnabled,
.logicOpEnable = colorLogicOpEnabled,
.stencilTestEnable = stencilTestEnabled,
.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MG_State::pGLContext->GetDepthFunc()),
.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MG_State::pGLContext->GetLogicOp()),
.depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MGB_CTX->GetDepthFunc()),
.logicOp = MG_Util::ConvertLogicOperationToVkEnum(MGB_CTX->GetLogicOp()),
.frontStencilFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.FailOp),
.frontStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthPassOp),
.frontStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthFailOp),
@@ -5351,8 +5352,8 @@ void main() {
// handed back after the application changed them.
if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable &&
vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();
const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();
const FloatVec4& defaultOuterLevel = MGB_CTX->GetPatchDefaultOuterLevel();
const FloatVec2& defaultInnerLevel = MGB_CTX->GetPatchDefaultInnerLevel();
payload.passthroughTessControlKey = ProgramFactory::ComputePassthroughTessControlKey(
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
programObj.passthroughPerVertexMembers);
@@ -5404,7 +5405,7 @@ void main() {
"GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity",
payload.colorAttachmentCount);
const auto& drawFboBinding =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null");
const Bool isDefaultDrawFbo = drawFboBinding->IsDefaultFramebuffer();
const auto& drawBuffers = drawFboBinding->GetDrawBuffers();
@@ -5432,14 +5433,14 @@ void main() {
BlendFactor dstAlpha = BlendFactor::Zero;
BlendEquation colorEquation = BlendEquation::Add;
BlendEquation alphaEquation = BlendEquation::Add;
MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
MG_State::pGLContext->GetBlendEquationIndexed(i, colorEquation, alphaEquation);
const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
MGB_CTX->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
MGB_CTX->GetBlendEquationIndexed(i, colorEquation, alphaEquation);
const Bool blendEnabled = MGB_CTX->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i);
// Per-draw-buffer color write mask (glColorMaski). Divergent per-attachment masks require
// the independentBlend device feature; when it is absent, fall back to draw buffer 0's
// mask for every attachment (matching the non-indexed glColorMask broadcast).
const BoolVec4 bufferMask =
MG_State::pGLContext->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
MGB_CTX->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
VkColorComponentFlags attachmentColorWriteMask = static_cast<VkColorComponentFlags>(
(bufferMask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) |
(bufferMask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) |
@@ -5855,7 +5856,7 @@ void main() {
VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFbo) const {
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
const auto& parameters = MGB_CTX->GetRenderStateParameters();
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
VkRect2D full{};
full.offset = {0, 0};
@@ -5916,7 +5917,7 @@ void main() {
// One compare for the whole tail: see the gate's declaration in
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
// input the six Apply* below read.
const Uint paramsVersion = MG_State::pGLContext->GetRenderStateParametersVersion();
const Uint paramsVersion = MGB_CTX->GetRenderStateParametersVersion();
if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion &&
shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() &&
shadow.dynamicTailIsDefaultFbo == isDefaultFbo) {
@@ -5940,7 +5941,7 @@ void main() {
// re-derive the value its shadow already holds.
DynamicStateShadow::DynamicTailKey key;
{
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters();
// Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two
// (per-index state for indices > 0 is keyed separately, see multiViewportKey below).
key.viewport[0] = p.Viewports[0].x();
@@ -6016,7 +6017,7 @@ void main() {
MOBILEGL_ASSERT(
[&] {
const auto& fbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
return isDefaultFbo == (fbo != nullptr && fbo->IsDefaultFramebuffer());
}(),
"GetBaseTransformFlagsRaw: isDefaultFbo does not match the bound draw framebuffer");
@@ -6040,7 +6041,7 @@ void main() {
// Entry select: by the draw program's lifetime id, MRU first (the id pins
// the entry; every other fact is re-guarded below, so probing a stale
// entry can only decline, never serve stale state).
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
const auto& program = *MGB_CTX->GetProgramForDraw();
const Uint64 programLifetimeId = program.GetLifetimeId();
SetupDrawSnapshot* snapPtr = nullptr;
{
@@ -6088,7 +6089,7 @@ void main() {
// captured draw after glBeginTransformFeedback would bind the undecorated
// variant and silently capture nothing while the CPU bookkeeping advances.
const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled &&
MG_State::pGLContext->IsTransformFeedbackActive() &&
MGB_CTX->IsTransformFeedbackActive() &&
program.GetTransformFeedbackVaryingCount() > 0;
const Bool snapHasXfbCapture =
static_cast<Bool>(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) &
@@ -6102,12 +6103,12 @@ void main() {
// buffer binds (re-run every draw anyway). Declining here would send every
// draw of a VAO-cycling stream (Minecraft chunk rendering) through the full
// path, re-resolving descriptors and texture layouts nothing invalidated.
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
const Bool vaoMoved =
static_cast<const void*>(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId ||
vao.GetConfigVersion() != snap.vaoConfigVersion;
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
drawFbo->GetLifetimeId() != snap.drawFboLifetimeId ||
drawFbo->GetObjectVersion() != snap.fboVersion) {
@@ -6118,8 +6119,8 @@ void main() {
// back on what the snapshot already describes (a GL_BLEND toggle between
// two draws, a redundant glBindSampler), and declining here sends every
// such draw through the full SetupDraw.
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion();
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
const Bool renderStateMoved = renderStateVersion != snap.renderStateVersion;
const Bool bindsMoved = bindGeneration != snap.bindGeneration;
if (renderStateMoved) {
@@ -6127,7 +6128,7 @@ void main() {
// flavor input (depth/stencil participation); a flip of that must take
// the full path's pass selection. One bulk parameters fetch instead of
// two capability-accessor calls; both are pure reads of the same fields.
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
const Bool drawUsesDepthStencil = rsp.DepthTestEnabled || rsp.StencilTestEnabled;
if (drawUsesDepthStencil != snap.drawUsesDepthStencil) {
return false;
@@ -6301,7 +6302,7 @@ void main() {
if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) {
return false;
}
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
const Uint64 samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration();
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
// Decline, not re-arm: snap.resolvedTransformFlags bakes the
// ExplicitLod0Sampling verdict, which reads the effective sampler's
@@ -6431,7 +6432,7 @@ void main() {
// was cancelled has no usable optimized module - and on an in-place
// SanitizeAndOptimizeBinary failure GetGeneratedSpirv() still holds the RAW
// glslang words, which must never reach vkCreateShaderModule. Drop the draw.
const auto& drawProgram = *MG_State::pGLContext->GetProgramForDraw();
const auto& drawProgram = *MGB_CTX->GetProgramForDraw();
if (!drawProgram.GetLinkStatus() || !drawProgram.GetSpirvStatus()) {
MGLOG_D("SetupDraw skipped: program=%u is linked=%d spirv=%d",
drawProgram.GetExternalIndex(), static_cast<int>(drawProgram.GetLinkStatus()),
@@ -6461,15 +6462,15 @@ void main() {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& drawFbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {
// Nothing was mutated: other entries' per-probe guards (FBO identity +
// version among them) stay authoritative, so none need invalidating.
RecordUnsupportedFramebufferError(__func__);
return false;
}
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
const auto& program = *MGB_CTX->GetProgramForDraw();
// The fast path declined (or had no entry for this program): whatever THIS
// program's entry saw may be stale, and the full path below mutates state as
// it goes, so the entry must not stay matchable if that path fails mid-way.
@@ -6509,7 +6510,7 @@ void main() {
ProgramFactory::CompileOptionFlags transformFlags =
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(drawFboIsDefault));
// Captured draws take the xfb-decorated program variant.
if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&
if (m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() &&
program.GetTransformFeedbackVaryingCount() > 0) {
transformFlags |= ProgramFactory::CompileOptionBit::XfbCapture;
}
@@ -6523,13 +6524,13 @@ void main() {
{
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
const Uint64 lodBindGeneration = MGB_CTX->GetTextureBindGeneration();
// The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range
// (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the
// sampling-resolution generation - not the texture params version the sum
// below covers. Without this key a filter/aniso change would keep serving
// the stale verdict.
const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
const Uint64 lodSamplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
Bool lodMemoHit = false;
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
@@ -6647,7 +6648,7 @@ void main() {
{
const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 programVersion = program.GetBackendStateVersion();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
// The bind generation alone stopped covering this set the moment ResolveSampledBinding
// started asking SamplesAsIncompleteTexture: membership now depends on the effective
// sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on
@@ -6666,7 +6667,7 @@ void main() {
// object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind
// generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo
// already carry.
const Uint64 samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
const Uint64 samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
const Bool sampledSetUnchanged =
m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId &&
m_lastSampledSetProgramVersion == programVersion &&
@@ -6827,8 +6828,8 @@ void main() {
// pass flavor (GL: a disabled depth/stencil test neither reads nor writes
// its buffer).
const Bool drawUsesDepthStencil =
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest);
auto* renderPassEntry =
m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
// nullptr: the framebuffer has an attachment DirectVulkan cannot represent (a texture the
@@ -6969,8 +6970,8 @@ void main() {
snap.fboVersion = drawFbo->GetObjectVersion();
snap.drawFboIsDefault = drawFboIsDefault;
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
snap.renderStateVersion = MGB_CTX->GetPipelineStateVersion();
snap.bindGeneration = MGB_CTX->GetTextureBindGeneration();
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
snap.resolvedTransformFlags = transformFlags.GetRaw();
snap.renderPassHash = nowActiveRenderPass->hash;
@@ -6996,7 +6997,7 @@ void main() {
snap.programObj = nullptr;
snap.programFactoryEpoch = 0;
}
snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
snap.samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration();
Uint64 snapContentSum = 0;
Uint64 snapParamsSum = 0;
// Per-entry copies of this draw's sampled set (the scratch vectors
@@ -7033,7 +7034,7 @@ void main() {
auto& frame = m_frameContext.GetCurrent();
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
// itself, never the graphics composite (which carries no compute stage at all).
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
const auto& program = *MGB_CTX->GetProgramForDispatch();
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
MGLOG_E_ONCE("DispatchCompute skipped: program=%u has no optimized SPIR-V",
program.GetExternalIndex());
@@ -7085,7 +7086,7 @@ void main() {
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
// See DispatchCompute: the dispatch accessor, not the draw one.
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
const auto& program = *MGB_CTX->GetProgramForDispatch();
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V",
program.GetExternalIndex());
@@ -7129,7 +7130,7 @@ void main() {
return;
}
auto indirectBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
auto indirectBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
if (!indirectBuffer) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: GL_DISPATCH_INDIRECT_BUFFER is not bound");
return;
@@ -7203,10 +7204,10 @@ void main() {
VkClearRect clearRect{};
clearRect.rect = framebuffer.IsDefaultFramebuffer()
? MakeDefaultFramebufferScissorRect(MG_State::pGLContext->GetScissorBox(),
? MakeDefaultFramebufferScissorRect(MGB_CTX->GetScissorBox(),
renderPassEntry->extent,
m_swapchainObject.GetPreTransform())
: MakeClampedScissorRect(MG_State::pGLContext->GetScissorBox(), renderPassEntry->extent);
: MakeClampedScissorRect(MGB_CTX->GetScissorBox(), renderPassEntry->extent);
clearRect.baseArrayLayer = 0;
// GL 3.3 §4.4.7: clearing a layered framebuffer clears every layer.
clearRect.layerCount = renderPassEntry->layers;
@@ -7254,10 +7255,10 @@ void main() {
return;
}
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return;
}
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)");
if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) {
RecordUnsupportedFramebufferError(__func__);
@@ -7265,16 +7266,16 @@ void main() {
}
ClearFramebufferPayload payload {
.color = MG_State::pGLContext->GetClearColor(),
.depth = MG_State::pGLContext->GetClearDepth(),
.stencil = MG_State::pGLContext->GetClearStencil()
.color = MGB_CTX->GetClearColor(),
.depth = MGB_CTX->GetClearDepth(),
.stencil = MGB_CTX->GetClearStencil()
};
// A render-pass loadOp clear always covers the complete attachment, while
// OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for
// GuiItemAtlas: animated items clear only their atlas slot before being
// redrawn. Queueing that clear as a loadOp erases every cached static item.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
VkClearRect clearRect{};
switch (PrepareScissoredClear(*fbo, clearRect)) {
case ScissoredClearPrep::NoOp:
@@ -7297,7 +7298,7 @@ void main() {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
continue;
}
@@ -7324,7 +7325,7 @@ void main() {
}
VkImageAspectFlags depthStencilAspects = 0;
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask()) {
if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask()) {
const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsComplete()) {
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
@@ -7337,7 +7338,7 @@ void main() {
// vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or
// zero mask can be expressed; treat a partial mask like a partial color mask.
const Uint32 stencilWriteMask =
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
} else if (stencilWriteMask != 0) {
@@ -7366,11 +7367,11 @@ void main() {
// gating for the deferred path: drop fully-masked planes, warn on partial
// masks vkCmdClear*/loadOp clears cannot express.
GLbitfield deferredMask = mask;
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MG_State::pGLContext->GetDepthMask()) {
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MGB_CTX->GetDepthMask()) {
deferredMask &= ~static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT);
}
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) != 0xFFu) {
if (stencilWriteMask != 0) {
MGLOG_W_ONCE("DirectVulkan: deferred glClear with a partial stencil write mask is not supported");
@@ -7386,7 +7387,7 @@ void main() {
if (drawBuffers[drawBufferIndex] == FramebufferAttachmentType::None) {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
const Bool full = colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a();
if (full) {
anyFullMask = true;
@@ -7407,7 +7408,7 @@ void main() {
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(drawBufferIndex);
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex);
if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) {
continue;
}
@@ -7436,7 +7437,7 @@ void main() {
const ClearAttachmentPayload& clearPayload) {
m_clearManager->CollectGarbage();
// GL 3.3 §3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) {
return;
}
if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) {
@@ -7478,7 +7479,7 @@ void main() {
}
// GL 3.3 §4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
VkClearRect clearRect{};
switch (PrepareScissoredClear(framebuffer, clearRect)) {
case ScissoredClearPrep::NoOp:
@@ -7509,9 +7510,9 @@ void main() {
// GL 3.3 §4.2.3: ClearBuffer* honors the write masks like Clear. Deferred
// clears cannot express partial masks; warn and skip those.
const auto depthClearAllowed = [&]() -> Bool { return MG_State::pGLContext->GetDepthMask(); };
const auto depthClearAllowed = [&]() -> Bool { return MGB_CTX->GetDepthMask(); };
const auto stencilClearAllowed = [&]() -> Bool {
const Uint32 stencilWriteMask = MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
return true;
}
@@ -7523,7 +7524,7 @@ void main() {
switch (buffer) {
case GL_COLOR: {
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast<Uint32>(drawbuffer));
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
return;
}
@@ -7580,7 +7581,7 @@ void main() {
if (!attachment.IsComplete()) {
return;
}
const BoolVec4 colorMask = MG_State::pGLContext->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast<Uint>(drawbuffer));
if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) {
return;
}
@@ -7598,7 +7599,7 @@ void main() {
MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(colorTexture));
} else {
VkImageAspectFlags aspects = 0;
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MG_State::pGLContext->GetDepthMask() &&
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask() &&
framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) {
aspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
}
@@ -7606,7 +7607,7 @@ void main() {
framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) {
// GL 3.3 §4.2.3: the clear is masked by the front stencil write mask (see Clear).
const Uint32 stencilWriteMask =
MG_State::pGLContext->GetStencilState(StencilFace::Front).WriteMask;
MGB_CTX->GetStencilState(StencilFace::Front).WriteMask;
if ((stencilWriteMask & 0xFFu) == 0xFFu) {
aspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
} else if (stencilWriteMask != 0) {
@@ -7625,7 +7626,7 @@ void main() {
void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload) {
auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get();
if (!fbo) {
return;
}
@@ -8583,8 +8584,8 @@ void main() {
void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter) {
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
}
@@ -8616,8 +8617,8 @@ void main() {
// The scissor test clips blit writes: intersect the destination rectangle with
// the scissor box and shrink the source proportionally.
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const IntVec4& scissor = MG_State::pGLContext->GetScissorBox();
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const IntVec4& scissor = MGB_CTX->GetScissorBox();
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
const Bool dstFlipped = d1 < d0;
GLint lo = dstFlipped ? d1 : d0;
@@ -9211,7 +9212,7 @@ void main() {
return;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
auto destinationTexture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject();
if (destinationTexture == nullptr) {
RecordTextureCopyError(__func__, ErrorCode::InvalidOperation,
@@ -9219,7 +9220,7 @@ void main() {
return;
}
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (readFbo == nullptr) {
RecordTextureCopyError(__func__, ErrorCode::InvalidOperation,
"CopyTexSubImage2D requires a framebuffer bound to GL_READ_FRAMEBUFFER.");
@@ -9930,7 +9931,7 @@ void main() {
return;
}
auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (readFbo == nullptr) {
MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: no read framebuffer is bound");
return;
@@ -10685,8 +10686,8 @@ void main() {
// Store honoring the client pack state (single slice).
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT packAlignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
const SizeT dstRowStride = ((rowPixels * dstPixelBytes) + packAlignment - 1) / packAlignment * packAlignment;
@@ -10716,7 +10717,7 @@ void main() {
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& activeUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
auto textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
GetTextureImage(textureObject, textureUploadTarget, level, format, type, -1, pixels);
}
@@ -10960,7 +10961,7 @@ void main() {
return;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit());
auto texture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject();
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture.");
@@ -11216,7 +11217,7 @@ void main() {
// resets its counter state, because those bytes describe the previous owner's span.
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
const Uint64 identity = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId();
const Uint64 identity = MGB_CTX->GetBoundTransformFeedbackLifetimeId();
MOBILEGL_ASSERT(identity != 0,
"transform feedback object reported the free-slot sentinel (0) as its identity - "
"every slot would then read as 'mine' without ever being claimed");
@@ -11233,7 +11234,7 @@ void main() {
Uint32 slot = freeSlot;
if (slot == kNoSlot) {
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
if (MG_State::pGLContext->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
if (MGB_CTX->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
continue;
}
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
@@ -11264,17 +11265,17 @@ void main() {
}
Bool VulkanRenderer::BeginXfbCaptureForDraw(FrameContext::FrameData& frame) {
if (!m_transformFeedbackFeatureEnabled || MG_State::pGLContext == nullptr ||
!MG_State::pGLContext->IsTransformFeedbackActive()) {
if (!m_transformFeedbackFeatureEnabled || !MGB_CTX_LIVE ||
!MGB_CTX->IsTransformFeedbackActive()) {
return false;
}
// A paused span captures nothing, and the counter buffers keep their values, so the
// next resumed draw appends exactly where the last captured one stopped - which is
// what pause/resume means (ARB_transform_feedback2).
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
if (MGB_CTX->IsTransformFeedbackPaused()) {
return false;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
if (!program || program->GetTransformFeedbackVaryingCount() == 0) {
return false;
}
@@ -11313,7 +11314,7 @@ void main() {
VkDeviceSize offsets[4] = {};
VkDeviceSize sizes[4] = {};
for (SizeT i = 0; i < bufferCount; ++i) {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
const auto& bufferObject = point.GetBoundObject();
if (bufferObject == nullptr) {
@@ -11344,7 +11345,7 @@ void main() {
offsets, sizes);
const Uint32 counterSlot = CurrentXfbCounterSlot();
const Uint64 generation = MG_State::pGLContext->GetTransformFeedbackGeneration();
const Uint64 generation = MGB_CTX->GetTransformFeedbackGeneration();
const Bool resume = m_xfbCountersValid[counterSlot] && m_xfbLastSeenGeneration[counterSlot] == generation;
m_xfbLastSeenGeneration[counterSlot] = generation;
@@ -11367,7 +11368,7 @@ void main() {
if (!began) {
return;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
const auto& program = MGB_CTX->GetTransformFeedbackProgram();
const SizeT bufferCount = program ? std::min<SizeT>(program->GetTransformFeedbackBufferCount(), 4) : 0;
const Uint32 counterSlot = CurrentXfbCounterSlot();
VkBuffer counterBuffers[4] = {};
@@ -12015,7 +12016,7 @@ void main() {
default: break;
}
if (mergeGranularity != 0) {
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters();
if (rsp.PrimitiveRestartEnabled || rsp.PrimitiveRestartFixedIndexEnabled) {
mergeGranularity = 0;
}
@@ -12093,7 +12094,7 @@ void main() {
return;
}
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get();
if (!indexBuffer) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no element array buffer is bound");
@@ -12103,13 +12104,13 @@ void main() {
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset +
static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) + kGLDrawElementsIndirectCommandBytes;
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
}
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!parameterBuffer || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
@@ -12194,7 +12195,7 @@ void main() {
return;
}
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& vao = *MGB_CTX->GetBoundVertexArray();
const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get();
if (!indexBuffer) {
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: no element array buffer is bound");
@@ -12204,7 +12205,7 @@ void main() {
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset +
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + kGLDrawElementsIndirectCommandBytes;
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
@@ -12274,7 +12275,7 @@ void main() {
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset +
static_cast<SizeT>(stride) * static_cast<SizeT>(drawcount - 1) + kGLDrawArraysIndirectCommandBytes;
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!drawBuffer || commandBytes > drawBuffer->GetSize()) {
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
@@ -12763,8 +12764,8 @@ void main() {
if (!m_provokingVertexModePerPipeline) {
return VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
return (MG_State::pGLContext != nullptr &&
MG_State::pGLContext->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
return (MGB_CTX_LIVE &&
MGB_CTX->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
? VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT
: VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
+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
@@ -11,6 +11,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include "../Getter/GL_Getter.h"
namespace MobileGL::MG_Impl::GLImpl {
@@ -527,6 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(Clear);
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
}
@@ -535,6 +537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElements);
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
}
@@ -544,6 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElements);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
}
@@ -553,6 +557,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
basevertex);
}
@@ -562,6 +567,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawArrays);
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
}
@@ -570,6 +576,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArrays);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
}
@@ -579,6 +586,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
}
@@ -588,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsIndirect);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
@@ -596,6 +605,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArraysIndirect);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
}
@@ -605,6 +615,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawElementsIndirectCount);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
maxdrawcount, stride);
}
@@ -615,6 +626,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(MultiDrawArraysIndirectCount);
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
stride);
}
@@ -625,6 +637,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawRangeElementsBaseVertex);
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
basevertex);
}
@@ -635,6 +648,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DrawRangeElements);
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
}
@@ -645,6 +659,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);
}
@@ -655,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);
}
@@ -665,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);
}
@@ -675,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);
}
@@ -683,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,
@@ -691,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);
}
@@ -700,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);
}
@@ -708,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);
}
@@ -739,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);
}
@@ -791,6 +814,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (!ValidateCurrentProgramForCompute(__func__)) return;
if (ConditionalRenderDiscardsCommand()) return;
MGP_FILL(DispatchComputeIndirect);
dispatchComputeIndirect(indirect);
}
@@ -812,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);
}
}
@@ -882,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);
}
@@ -903,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);
}
@@ -916,6 +943,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Backend does not support regional memory barriers."));
return;
}
MGP_FILL(MemoryBarrierByRegion);
memoryBarrierByRegion(barriers);
}
@@ -1238,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);
}
}
@@ -1320,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();
@@ -1328,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);
}
}
@@ -1349,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();
}
}
@@ -1363,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();
}
}
@@ -1568,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);
@@ -1599,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);
}
@@ -12,6 +12,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -164,6 +165,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void ResetQueryObjectLocked(QueryObject* queryObject) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -178,6 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
if (endTimeElapsedQuery && queryObject->backendHandle) {
MGP_FILL(EndTimeElapsedQuery);
endTimeElapsedQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -257,6 +260,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
Uint64 result = 0;
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (queryObject->backendHandle && getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
// Not ready. The whole point of the no-wait form is that the caller's
@@ -271,6 +275,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -286,6 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
MGP_FILL(IsQueryResultAvailable);
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
return true;
}
@@ -297,6 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Uint64 result = 0;
if (queryObject->backendHandle) {
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
// The backend could not produce the result YET (e.g. a
@@ -317,6 +324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads.
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -422,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -441,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -519,6 +529,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
MGP_FILL(BeginXfbPrimitivesQuery);
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
@@ -527,9 +538,11 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
} else if (isOcclusionQuery) {
MGP_FILL(BeginOcclusionQuery);
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
MGP_FILL(BeginTimeElapsedQuery);
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
@@ -579,6 +592,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
MGP_FILL(EndXfbPrimitivesQuery);
endXfbPrimitivesQuery(queryObject->backendHandle);
}
}
@@ -588,6 +602,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
@@ -604,6 +619,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
@@ -642,6 +658,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
MGP_FILL(QueryCounterTimestamp);
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true;
@@ -771,6 +788,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
MGP_FILL(IsTimerQuerySupported);
const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0;
@@ -912,6 +930,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
+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;
@@ -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);
}
@@ -6453,6 +6460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject != nullptr &&
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
@@ -6657,6 +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
+180
View File
@@ -127,6 +127,8 @@ add_executable(MobileGLIntegrationTest
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
Scenarios/RenderbufferBlendFormatScenario.cpp
Scenarios/DualSourceBlendScenario.cpp
Scenarios/PipeVerifyArmingScenario.cpp
Scenarios/PoisonOmissionScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -631,3 +633,181 @@ gtest_discover_tests(MobileGLIntegrationTest
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,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,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
+28 -3
View File
@@ -31,11 +31,13 @@
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 once the */ \
/* inventory carries the target argument (P1). Named for the plan's explicit */ \
/* 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) \
@@ -94,7 +96,30 @@
X(IsTransformFeedbackPaused, PauseStreamOutput) \
X(InvalidateCompileEnv, kClientResolved) \
X(ValidateProgramName, kClientResolved) \
X(RecordError, kReverseChannel)
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
+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
+5
View File
@@ -85,6 +85,11 @@ namespace MobileGL::MG_Pipe {
// 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"
+10 -9
View File
@@ -22,19 +22,20 @@
// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a
// protocol break that no test would otherwise see.
//
// P0.5 DEBT, recorded here so it is impossible to miss: two payloads reach into headers
// this directory is eventually forbidden to see - MGPCaps embeds MG_Backend's
// DynamicBackendParameters, and ResidualValueBlock embeds MG_State's RenderStateParameters
// and PixelStoreParameters. Both are deliberate: the caps block IS that struct (section
// 4.4.1) and the residual block is the migration carrier for Track V (section 6.3). P0.5
// extracts MGPipeValueTypes.h and both includes below go away; until then purity gate A
// (section 10.3) cannot be armed for this header.
// 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 <MG_State/GLState/RenderState/RenderState.h>
#include "MGPipeValueTypes.h"
namespace MobileGL::MG_Pipe {
using MG_Backend::DynamicBackendParameters;
// Both live directly in namespace MobileGL today; P0.5 moves them into
// Both live directly in namespace MobileGL and, since P0.5, are declared in
// MG_Pipe/MGPipeValueTypes.h.
using MobileGL::PixelStoreParameters;
using MobileGL::RenderStateParameters;
+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
+79 -4
View File
@@ -12,9 +12,11 @@
// exactly what makes a memcmp of RenderStateParameters false-DIFFER
// (DirectGLES.cpp documents that behaviour where it does the same comparison itself).
//
// Hand maintained alongside MGPipeTypes.h. Adding a field to a payload without adding it
// here makes the comparator blind to it; that gap closes in P1, when the verify harness
// goes live and the comparator's coverage is itself asserted.
// 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
@@ -218,6 +220,77 @@
#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) \
@@ -233,6 +306,8 @@
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(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
@@ -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");
+27 -14
View File
@@ -22,8 +22,8 @@
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
// 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,
@@ -87,11 +87,13 @@ enum class MGPipeInputField : Uint16 {
InvalidateCompileEnv,
ValidateProgramName,
RecordError,
GetBoundTransformFeedbackLifetimeId,
HasOpenTransformFeedbackSpan,
kFieldCount,
};
inline constexpr SizeT kMGPipeInputFieldCount = static_cast<SizeT>(MGPipeInputField::kFieldCount);
static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");
static_assert(kMGPipeInputFieldCount == 63, "the PipeInputs field set moved");
inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"GetActiveTextureUnit",
@@ -155,11 +157,13 @@ inline constexpr const char* kMGPipeInputFieldNames[kMGPipeInputFieldCount] = {
"InvalidateCompileEnv",
"ValidateProgramName",
"RecordError",
"GetBoundTransformFeedbackLifetimeId",
"HasOpenTransformFeedbackSpan",
};
// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each
// true has to be argued for in P1 when the fillers land: a sticky field is a field
// the poison cannot protect.
// 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
@@ -169,7 +173,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetBoundVertexArray
false, // GetBufferBindingSlot
false, // GetBufferBindingPoint
false, // GetBufferBindingPointCount
true, // GetBufferBindingPointCount: keyed by target: a constexpr capacity table, not verb state
false, // GetTouchedBufferBindingPointCount
false, // GetClampReadColor
false, // GetClearColor
@@ -198,7 +202,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetPrimitiveRestartIndex
false, // GetProgramForDispatch
false, // GetProgramForDraw
false, // GetProgramObject
true, // GetProgramObject: keyed by GL name: an object lookup, not verb state
false, // GetProvokingVertexMode
false, // GetRenderStateParameters
false, // GetRenderStateParametersVersion
@@ -207,7 +211,7 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // GetStencilState
false, // GetTextureBindGeneration
false, // GetTextureContextId
false, // GetTextureObject
true, // GetTextureObject: keyed by GL name: an object lookup, not verb state
false, // GetTextureUnitObject
false, // GetTransformFeedbackCapturedVertices
false, // GetTransformFeedbackGeneration
@@ -219,10 +223,13 @@ inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {
false, // IsCapabilityEnabledIndexed
false, // IsTransformFeedbackActive
false, // IsTransformFeedbackPaused
false, // InvalidateCompileEnv
false, // ValidateProgramName
false, // RecordError
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.
@@ -288,6 +295,8 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
"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 {
@@ -301,8 +310,12 @@ struct MGPipeFilledState {
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);
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
}
+82 -7
View File
@@ -27,6 +27,23 @@
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);
@@ -90,6 +107,12 @@ inline Bool MGPipeVerify(const MGPFlush& a, const MGPFlush& b, const char** outF
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 {};
@@ -217,12 +240,26 @@ 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>) {
@@ -230,15 +267,23 @@ inline Bool MGPipeFieldEqual(const T& a, const T& b) {
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
return a == b;
} else {
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
// and this branch stops being reachable from any payload.
return std::memcmp(&a, &b, sizeof(T)) == 0;
// 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) {
@@ -568,6 +613,36 @@ inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const
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 = 63;
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
@@ -11,6 +11,7 @@
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
#include <MG_Pipe/MGPipeValueTypes.h>
namespace MobileGL {
enum class FramebufferTarget {
@@ -103,7 +104,7 @@ namespace MobileGL {
class FramebufferObject {
public:
static constexpr Uint MAX_DRAW_BUFFERS = 8;
static constexpr Uint MAX_DRAW_BUFFERS = MobileGL::kMGMaxDrawBuffers;
using TargetEnum = FramebufferTarget;
using FramebufferAttachmentObjectArray =
@@ -0,0 +1,576 @@
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.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> // String/Vector/Array/UnorderedMap/SharedPtr + GL enums. DEBT NOTE (MGPipeTypes.h:24-31 style):
// Includes.h:80-84 still pulls glslang and :59 spirv_cross_c.h; this header is glslang-free BY
// SYMBOL (what P7's `nm -D | grep glslang` measures), not by preprocessed text. A textually
// glslang-free closure needs MG_Util/Types.h split off Includes.h (Types.h:11 includes it
// back) - out of scope for P0.5.
#include <set> // std::set<String> (LinkArtifacts); NOT provided by Includes.h on its own terms
// PURITY: no ShaderObject.h, no SpvcSession.h, nothing under MG_Util/ShaderTranspiler/, no Config.h,
// no MG_Backend/, no BufferState/. scripts/check_include_closure.py probe "artifacts-header" (ROADMAP P0.5;
// ARCHITECTURE.md:260) asserts that closure. The glslang scope token appears exactly twice below (B.0 D5: the two
// glslang-typed LinkArtifacts members moved verbatim) and the gate pins that count.
namespace MobileGL::MG_State::GLState {
// Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
// Namespace scope so SpirvArtifacts::reservedNumSamplesOffset can default to it;
// ProgramObject::kInvalidUniformOffset is defined from this one.
inline constexpr Uint kInvalidUniformOffset = ~0u;
// Everything the query surface ever asked a glslang TType, flattened. Twenty
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
// AST, so a POD covers the whole surface exactly.
struct TypeFacts {
Bool isArray = false;
// A runtime-sized array (a storage block's unsized trailing member) is an array
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
Bool isSizedArray = false;
Bool isMatrix = false;
Bool isVector = false;
Bool isOpaque = false;
Bool isTexture = false;
Bool isImage = false;
Bool isDouble = false; // getBasicType() == EbtDouble
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
Bool isPatch = false; // getQualifier().patch
Bool hasIndex = false; // getQualifier().hasIndex()
Bool hasFormat = false; // getQualifier().hasFormat()
Int vectorSize = 0;
Int matrixCols = 0;
Int matrixRows = 0;
Int layoutIndex = 0; // getQualifier().layoutIndex
Uint layoutFormat = 0; // getQualifier().getFormat()
// glslang TLayoutMatrix, widened. For a uniform this is already RESOLVED against
// the owning block's qualifier, so the getUniformBlock() fallback the old
// accessors carried is gone.
Int layoutMatrix = 0;
// glslang TBasicType, widened - ApplyUniformInitialValues and the typed
// glGetUniform* paths compare against a handful of enumerators.
Int basicType = 0;
};
// One glslang TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
struct ResourceReflection {
String name;
GLenum glDefineType = 0;
Int offset = -1;
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
// the resolved GL_UNIFORM_SIZE answer.
Int size = 0;
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
Int index = -1;
Int counterIndex = -1;
Int arrayStride = 0;
Int topLevelArraySize = 0;
Int topLevelArrayStride = 0;
Int binding = -1;
Int location = -1; // layoutLocation()
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
// nobody", which is what the dead-default-block-uniform filter tests.
Uint32 stages = 0;
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
// isSizedArray()/getOuterArraySize()/size fallback.
GLint arraySize = 1;
TypeFacts type;
};
using UniformReflection = ResourceReflection;
using BlockReflection = ResourceReflection;
using PipeInputReflection = ResourceReflection;
using PipeOutputReflection = ResourceReflection;
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
// the NEXT link; the linked snapshot below is what draws and queries see).
struct XfbVarying {
String name;
GLenum type = GL_FLOAT;
GLint size = 1; // array element count
Uint32 bufferIndex = 0; // capture buffer slot
Uint32 offsetBytes = 0; // offset within the capture buffer
Uint32 byteSize = 0; // bytes captured per vertex for this varying
// Offset within the gap-free record a backend that cannot express the GL
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
Uint32 packedOffsetBytes = 0;
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
// what the interface queries and the ESSL backend's driver-side capture list
// need, since SPIRV-Cross re-emits the block under its own type name), while
// the three fields below carry what a SPIR-V backend needs instead: the
// decoration target is the block's *instance* variable and the member index
// inside it. blockMemberIndex < 0 means "not a block member".
String blockInstanceName;
String blockName;
Int blockMemberIndex = -1;
// Which element of an arrayed block member this capture names, -1 for "the
// member as a whole". SPIR-V cannot decorate a single array element, so a
// backend needs the element index to tell a full run from a partial one.
Int blockMemberElement = -1;
};
// ---- P1: everything a link PRODUCES, in one movable block ----
//
// The membership rule is mechanical, not editorial: this is exactly the field list
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
// as much link output). Nothing else belongs here.
//
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
// of thirty cross-thread field assignments. Until then this is a pure refactor.
//
// Access rule (invariant I5): the member below is private and reachable ONLY
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
// what makes "every read of link output joins the pending link" a property the
// compiler checks rather than a review item - a new reader cannot spell the field
// without going through the gate. m_artifacts lives in ProgramObject and is private
// there; the type being namespace-scope changes nothing about that gate.
// ---- the owned mirror of glslang's reflection ----
//
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
// asking the live glslang TProgram - program->getUniform(i).getType()->isMatrix()
// and friends. That made the TProgram part of the program's PERMANENT state, which
// in turn made the whole front end (parse + link) unskippable: the L1 shader
// translation memo could hand back the SPIR-V but the reflection still had to be
// rebuilt from a freshly parsed AST.
//
// These three tables are a snapshot of everything the query surface ever reads off
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
// they are copyable, immutable after the link, and safe to memoize and share between
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
// everything except DoReflection itself.
//
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
// instead, unchanged in every other respect.
struct LinkArtifacts {
// Live only between LinkProgram() and the end of DoReflection. Everything after
// that reads the owned mirror below; a link served from the L1 memo never
// constructs one at all, so this is null for such a program and MUST NOT be
// dereferenced outside DoReflection.
SharedPtr<glslang::TProgram> program;
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
Vector<UniformReflection> uniformReflection;
Vector<BlockReflection> blockReflection;
Vector<PipeInputReflection> pipeInputReflection;
Vector<PipeOutputReflection> pipeOutputReflection;
// Program-level scalars glslang answers off the linked intermediates.
// Whether the program's LAST stage is the fragment stage. A color number - and so a
// color index - exists only there; a separable tess/geometry/vertex program's
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
// separate-programs-tess-control).
Bool lastStageIsFragment = false;
Array<GLuint, 3> computeLocalSize{};
// Replaces program->getUniformIndex(name). Maps the reflected name to its
// TProgram uniform index.
UnorderedMap<String, Int> uniformIndexByName;
// Attributes (Vertex in)
Vector<String> attribs;
Vector<GLenum> attribTypes;
// FragData (Frag out): the per-link snapshot of the explicit request maps.
UnorderedMap<String, Uint> linkedFragDataLocation;
UnorderedMap<String, Uint> linkedFragDataIndex;
// GL-facing index spaces (see the translation helpers above): GL active-uniform
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
Vector<Int> glUniformIndexToTProgram;
Vector<Int> tProgramUniformIndexToGl;
Vector<Int> glBlockIndexToTProgram;
Vector<Int> tProgramBlockIndexToGl;
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
// glBlockIndexToTProgram above.
//
// That list is the BLOCK space - everything the relaxed parse produced except
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
// through indexToUniformBlock too, and the list therefore also carries every shader
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
//
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
// DirectGLES.cpp), so compacting that list would renumber every backend binding
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
// shaders' default-block uniforms declared, as glslang recorded them at the point
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
// DoReflection assigner restores them from here).
UnorderedMap<String, Int> linkedExplicitUniformLocations;
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
// reads until the application overwrites it, and relinking restores it - but the
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
// cannot carry an initializer, so the value only survives as this side-channel.
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
UnorderedMap<String, Uint> uniformLocations;
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
// write (GL resets uniforms to their initial values), so living here means the set
// is cleared by the same three paths that clear the rest of a link's output -
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
// fourth reset site can be forgotten. Empty (and never allocated) for a program
// that never asked to be separable.
Vector<Uint64> writtenUniformLocationBits;
Vector<Uint64> writtenUniformIndexBits;
Vector<Uint> writtenUniformIndices;
// Ordered by location,
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
Vector<Int> uniformIndexInTProgram;
// ditto. Will be set at glUniform1i
Vector<Int> uniformSamplerOrImageUnitIndex;
// Sampler/image layout(binding = N) initial texture/image units, captured by
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
// qualifier still says what the shader declared. An OUTPUT of the link, not an
// input to it: nothing supplies this map, the resolver fills it.
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
// Ordered by uniform block index
// index is DIFFERENT from binding!!!
//
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
// `prog->getUniformBlock(i) == "BlockName"`
// These stuff are present for GL semantics, not for backend inspection
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
UnorderedMap<String, Uint> uniformBlockIndexByName;
Vector<Int> uniformBlockBinding;
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
// SetShaderStorageBlockBinding for why this one is by name and not by index.
//
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
// GL-mandated binding 0 for every storage block whose shader declared no
// layout(binding = N). Those blocks have no other way to be told apart from a block
// that declared one: glslang's IO mapper invents a binding and writes it into the
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
// default binding for this block", and a later glShaderStorageBlockBinding simply
// overwrites it - default and rebind travel one path.
UnorderedMap<String, Int> shaderStorageBlockBinding;
// Block type names of the storage blocks the program's shaders declared with NO
// layout(binding = N). Input to the seeding above; filled during mapIO by
// TMglGlslIoResolver, which is the last observer that can still tell a declared
// binding from an invented one - and, unlike the per-shader lexer this replaced,
// sees the declaration with its macros expanded.
std::set<String> storageBlocksWithoutBinding;
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
// auto-mapper assigns every uniform block a binding whether or not the shader asked
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
std::set<String> uniformBlocksWithoutBinding;
Uint activeUniformCount = 0;
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
// and the draw path owes it the draw framebuffer's sample count before every draw.
//
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
// gate has to be answerable without joining the SPIR-V job, or every draw of every
// program would pay a join to discover it has nothing to write.
Bool usesReservedNumSamples = false;
Uint maxUniformLocation = 0;
Int uniformNameMaxLength = 0;
Int attribInNameMaxLength = 0;
Int uniformBlockNameMaxLength = 0;
String infoLog;
Bool linkStatus = false;
// Transform feedback: the linked snapshot (the request lives outside, on the
// GL-thread-owned side).
Vector<XfbVarying> xfbVaryings;
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
// xfbVaryings deliberately drops (they steer the capture layout and must never
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
// full request, pseudo-varyings and all, so the interface query needs its own copy.
Vector<String> xfbInterfaceNames;
Vector<Uint32> xfbStrides;
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
GLenum gsInputPrimitive = GL_NONE;
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
// tessellation control stage, or 0 when the program has none. Checked against
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
Int tcsOutputVertices = 0;
// The rest of the geometry stage's link properties, and the tessellation evaluation
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
// the query surface listed the geometry pnames only to fall through to
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
// come from the linked intermediates for the same reason gsInputPrimitive and
// tcsOutputVertices do - glslang has already merged the compilation units' layout
// qualifiers and diagnosed contradictions, so the linked program is the thing that
// knows.
GLenum gsOutputPrimitive = GL_NONE;
Int gsMaxVertices = 0;
Int gsInvocations = 0;
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
GLenum tessGenMode = GL_NONE;
GLenum tessGenSpacing = GL_NONE;
GLenum tessGenVertexOrder = GL_NONE;
Bool tessGenPointMode = false;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false;
Uint32 xfbPackedStride = 0;
};
// ---- everything phase B of a link produces, in one movable block ----
//
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
// readers of SPIR-V-derived data through their own join gate by compiler rather than
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
//
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
// cannot be lifted out of glslang's reflection instead.
struct SpirvArtifacts {
Vector<Vector<unsigned>> generatedSpirv;
Bool enableSpirvValidation = false;
// Byte offset of each uniform location inside globalUboScratch, or
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
Vector<Uint> uniformOffsets;
Vector<Uint8> globalUboScratch;
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
// uniformOffsets, because the member has no GL location at all: the link task keeps
// it out of the GL-visible uniform index space so no application can see or write it.
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
// False for a program whose SPIR-V was never produced (phase B cancelled at
// teardown or by a relink) or whose optimizer run failed. GL has no way to
// retract a LINK_STATUS it already reported true, so such a program stays
// "linked" and every reflection answer it has given stays correct - it is simply
// not drawable, which the backends already express through their link-status
// gates.
Bool spirvStatus = false;
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
// different offset in each. Recorded here rather than re-derived from the backend
// because it is the layout THESE modules were built with: it is what the routing
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
// width the shader actually declares.
Bool nativeFloat64 = false;
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
// the built-in there. Per PROGRAM by construction - a consumer whose producer
// kept the built-in would read garbage - and recorded here rather than
// re-derived because it cannot be: the rewrite's whole point is that the final
// bytes no longer declare the capability that armed it. The backends read it to
// respell a "gl_PointSize" transform-feedback capture as the carrier
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
// so every query keeps the truthful GL spelling.
Bool pointSizeDemoted = false;
};
// ---- the archive field tables (ARCHITECTURE.md:259): ONE table per type, serving both directions ----
//
// Visitor contract: v(const char* name, Field&) - Field is const when Self is const, so a
// single table serves the serializer (const) and the deserializer (non-const); `Self`
// deduces either. A visitor recurses into TypeFacts / ResourceReflection / XfbVarying by
// calling VisitFields on the element it was handed; the tables never recurse themselves.
//
// Free constrained templates rather than members so the struct bodies above stay a verbatim
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
// struct changes its size, trips the assertion, and the message sends the author here.
template <class Self, class V>
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
void VisitFields(Self& a, V&& v) {
v("isArray", a.isArray);
v("isSizedArray", a.isSizedArray);
v("isMatrix", a.isMatrix);
v("isVector", a.isVector);
v("isOpaque", a.isOpaque);
v("isTexture", a.isTexture);
v("isImage", a.isImage);
v("isDouble", a.isDouble);
v("isVoid", a.isVoid);
v("isBuffer", a.isBuffer);
v("isPatch", a.isPatch);
v("hasIndex", a.hasIndex);
v("hasFormat", a.hasFormat);
v("vectorSize", a.vectorSize);
v("matrixCols", a.matrixCols);
v("matrixRows", a.matrixRows);
v("layoutIndex", a.layoutIndex);
v("layoutFormat", a.layoutFormat);
v("layoutMatrix", a.layoutMatrix);
v("basicType", a.basicType);
} // 20 fields
template <class Self, class V>
requires std::same_as<std::remove_const_t<Self>, ResourceReflection>
void VisitFields(Self& a, V&& v) {
v("name", a.name);
v("glDefineType", a.glDefineType);
v("offset", a.offset);
v("size", a.size);
v("index", a.index);
v("counterIndex", a.counterIndex);
v("arrayStride", a.arrayStride);
v("topLevelArraySize", a.topLevelArraySize);
v("topLevelArrayStride", a.topLevelArrayStride);
v("binding", a.binding);
v("location", a.location);
v("stages", a.stages);
v("arraySize", a.arraySize);
v("type", a.type); // visited as a value; the visitor recurses with VisitFields(a.type, v) if it wants to
} // 14 fields
template <class Self, class V>
requires std::same_as<std::remove_const_t<Self>, XfbVarying>
void VisitFields(Self& a, V&& v) {
v("name", a.name);
v("type", a.type);
v("size", a.size);
v("bufferIndex", a.bufferIndex);
v("offsetBytes", a.offsetBytes);
v("byteSize", a.byteSize);
v("packedOffsetBytes", a.packedOffsetBytes);
v("blockInstanceName", a.blockInstanceName);
v("blockName", a.blockName);
v("blockMemberIndex", a.blockMemberIndex);
v("blockMemberElement", a.blockMemberElement);
} // 11 fields
// Every member EXCEPT `program`: it is null for every archived instance by construction
// (ProgramTranslationCache.h asserts that at insert) and must never be serialized - it is
// the live glslang TProgram that only DoReflection may touch. 57 of the 58 members.
template <class Self, class V>
requires std::same_as<std::remove_const_t<Self>, LinkArtifacts>
void VisitFields(Self& a, V&& v) {
v("uniformReflection", a.uniformReflection);
v("blockReflection", a.blockReflection);
v("pipeInputReflection", a.pipeInputReflection);
v("pipeOutputReflection", a.pipeOutputReflection);
v("lastStageIsFragment", a.lastStageIsFragment);
v("computeLocalSize", a.computeLocalSize);
v("uniformIndexByName", a.uniformIndexByName);
v("attribs", a.attribs);
v("attribTypes", a.attribTypes);
v("linkedFragDataLocation", a.linkedFragDataLocation);
v("linkedFragDataIndex", a.linkedFragDataIndex);
v("glUniformIndexToTProgram", a.glUniformIndexToTProgram);
v("tProgramUniformIndexToGl", a.tProgramUniformIndexToGl);
v("glBlockIndexToTProgram", a.glBlockIndexToTProgram);
v("tProgramBlockIndexToGl", a.tProgramBlockIndexToGl);
v("glUniformBlockIndexToBlock", a.glUniformBlockIndexToBlock);
v("blockIndexToGlUniformBlock", a.blockIndexToGlUniformBlock);
v("linkedExplicitUniformLocations", a.linkedExplicitUniformLocations);
v("uniformInitialValues", a.uniformInitialValues);
v("uniformLocations", a.uniformLocations);
v("writtenUniformLocationBits", a.writtenUniformLocationBits);
v("writtenUniformIndexBits", a.writtenUniformIndexBits);
v("writtenUniformIndices", a.writtenUniformIndices);
v("uniformIndexInTProgram", a.uniformIndexInTProgram);
v("uniformSamplerOrImageUnitIndex", a.uniformSamplerOrImageUnitIndex);
v("explicitOpaqueUniformBindings", a.explicitOpaqueUniformBindings);
v("uniformBlockIndexByName", a.uniformBlockIndexByName);
v("uniformBlockBinding", a.uniformBlockBinding);
v("shaderStorageBlockBinding", a.shaderStorageBlockBinding);
v("storageBlocksWithoutBinding", a.storageBlocksWithoutBinding);
v("uniformBlocksWithoutBinding", a.uniformBlocksWithoutBinding);
v("activeUniformCount", a.activeUniformCount);
v("usesReservedNumSamples", a.usesReservedNumSamples);
v("maxUniformLocation", a.maxUniformLocation);
v("uniformNameMaxLength", a.uniformNameMaxLength);
v("attribInNameMaxLength", a.attribInNameMaxLength);
v("uniformBlockNameMaxLength", a.uniformBlockNameMaxLength);
v("infoLog", a.infoLog);
v("linkStatus", a.linkStatus);
v("xfbVaryings", a.xfbVaryings);
v("xfbInterfaceNames", a.xfbInterfaceNames);
v("xfbStrides", a.xfbStrides);
v("gsStripTriangles", a.gsStripTriangles);
v("gsStripCaptureFixup", a.gsStripCaptureFixup);
v("gsInputPrimitive", a.gsInputPrimitive);
v("tcsOutputVertices", a.tcsOutputVertices);
v("gsOutputPrimitive", a.gsOutputPrimitive);
v("gsMaxVertices", a.gsMaxVertices);
v("gsInvocations", a.gsInvocations);
v("tessGenMode", a.tessGenMode);
v("tessGenSpacing", a.tessGenSpacing);
v("tessGenVertexOrder", a.tessGenVertexOrder);
v("tessGenPointMode", a.tessGenPointMode);
v("xfbBufferMode", a.xfbBufferMode);
v("xfbVaryingNameMaxLength", a.xfbVaryingNameMaxLength);
v("xfbNeedsScatteredCapture", a.xfbNeedsScatteredCapture);
v("xfbPackedStride", a.xfbPackedStride);
} // 57 fields (58 members minus `program`)
template <class Self, class V>
requires std::same_as<std::remove_const_t<Self>, SpirvArtifacts>
void VisitFields(Self& a, V&& v) {
v("generatedSpirv", a.generatedSpirv);
v("enableSpirvValidation", a.enableSpirvValidation);
v("uniformOffsets", a.uniformOffsets);
v("globalUboScratch", a.globalUboScratch);
v("reservedNumSamplesOffset", a.reservedNumSamplesOffset);
v("spirvStatus", a.spirvStatus);
v("nativeFloat64", a.nativeFloat64);
v("pointSizeDemoted", a.pointSizeDemoted);
} // 8 fields
// ---- trip wires ----
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and its serializer when one exists), then update this number");
// The container-bearing structs have one size per standard library (std::string and
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
// unasserted. ProgramArtifactsTest records every sizeof as a ctest property on every
// platform, which is where a new toolchain's numbers are read from.
#if defined(__GLIBCXX__) && !defined(_GLIBCXX_DEBUG) && (SIZE_MAX == UINT64_MAX)
#define MGL_RESOURCEREFLECTION_SIZE 128
#define MGL_XFBVARYING_SIZE 128
#define MGL_LINKARTIFACTS_SIZE 1056
#define MGL_SPIRVARTIFACTS_SIZE 88
#elif defined(_LIBCPP_VERSION) && (SIZE_MAX == UINT64_MAX) && defined(MGL_ARTIFACT_SIZES_LIBCXX_PINNED)
// The integrator pins these from the NDK build (brief C.4); until then this branch is inert.
#endif
#ifdef MGL_LINKARTIFACTS_SIZE
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and its serializer when one exists), then update this number");
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and its serializer when one exists), then update this number");
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and its serializer when one exists), then update this number");
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and its serializer when one exists), then update this number");
#endif
} // namespace MobileGL::MG_State::GLState
@@ -9,9 +9,9 @@
#pragma once
#include <Includes.h>
#include "ShaderObject.h"
#include "ProgramArtifacts.h"
#include <MG_Util/Metrics/BufferMetrics.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
namespace MobileGL::MG_State::GLState {
// The link job. Only ever held by SharedPtr here, so a forward declaration is enough -
@@ -38,70 +38,19 @@ namespace MobileGL::MG_State::GLState {
// 1024 GL 4.3 requires.
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
// AST, so a POD covers the whole surface exactly.
struct TypeFacts {
Bool isArray = false;
// A runtime-sized array (a storage block's unsized trailing member) is an array
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
Bool isSizedArray = false;
Bool isMatrix = false;
Bool isVector = false;
Bool isOpaque = false;
Bool isTexture = false;
Bool isImage = false;
Bool isDouble = false; // getBasicType() == EbtDouble
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
Bool isPatch = false; // getQualifier().patch
Bool hasIndex = false; // getQualifier().hasIndex()
Bool hasFormat = false; // getQualifier().hasFormat()
Int vectorSize = 0;
Int matrixCols = 0;
Int matrixRows = 0;
Int layoutIndex = 0; // getQualifier().layoutIndex
Uint layoutFormat = 0; // getQualifier().getFormat()
// glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against
// the owning block's qualifier, so the getUniformBlock() fallback the old
// accessors carried is gone.
Int layoutMatrix = 0;
// glslang::TBasicType, widened - ApplyUniformInitialValues and the typed
// glGetUniform* paths compare against a handful of enumerators.
Int basicType = 0;
};
// One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
struct ResourceReflection {
String name;
GLenum glDefineType = 0;
Int offset = -1;
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
// the resolved GL_UNIFORM_SIZE answer.
Int size = 0;
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
Int index = -1;
Int counterIndex = -1;
Int arrayStride = 0;
Int topLevelArraySize = 0;
Int topLevelArrayStride = 0;
Int binding = -1;
Int location = -1; // layoutLocation()
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
// nobody", which is what the dead-default-block-uniform filter tests.
Uint32 stages = 0;
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
// isSizedArray()/getOuterArraySize()/size fallback.
GLint arraySize = 1;
TypeFacts type;
};
// The five reflection/artifact types live at namespace scope in ProgramArtifacts.h
// (P0.5). Re-exported here so every existing spelling (ProgramObject::LinkArtifacts,
// ProgramObject::TypeFacts, ...) compiles unchanged. Fully qualified on the right-hand
// side on purpose: an unqualified `TypeFacts` would name the alias being declared.
using TypeFacts = MobileGL::MG_State::GLState::TypeFacts;
using ResourceReflection = MobileGL::MG_State::GLState::ResourceReflection;
using UniformReflection = ResourceReflection;
using BlockReflection = ResourceReflection;
using PipeInputReflection = ResourceReflection;
using PipeOutputReflection = ResourceReflection;
using XfbVarying = MobileGL::MG_State::GLState::XfbVarying;
using LinkArtifacts = MobileGL::MG_State::GLState::LinkArtifacts;
using SpirvArtifacts = MobileGL::MG_State::GLState::SpirvArtifacts;
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
@@ -544,7 +493,7 @@ namespace MobileGL::MG_State::GLState {
}
// Sentinel for a uniform location without global-UBO backing storage (should not
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
static constexpr Uint kInvalidUniformOffset = ~0u;
static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
//
// BOUNDS-CHECKED, and that is not defensive padding - it is the load-bearing half of
@@ -1141,313 +1090,6 @@ namespace MobileGL::MG_State::GLState {
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
}
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
// the NEXT link; the linked snapshot below is what draws and queries see).
struct XfbVarying {
String name;
GLenum type = GL_FLOAT;
GLint size = 1; // array element count
Uint32 bufferIndex = 0; // capture buffer slot
Uint32 offsetBytes = 0; // offset within the capture buffer
Uint32 byteSize = 0; // bytes captured per vertex for this varying
// Offset within the gap-free record a backend that cannot express the GL
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
Uint32 packedOffsetBytes = 0;
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
// what the interface queries and the ESSL backend's driver-side capture list
// need, since SPIRV-Cross re-emits the block under its own type name), while
// the three fields below carry what a SPIR-V backend needs instead: the
// decoration target is the block's *instance* variable and the member index
// inside it. blockMemberIndex < 0 means "not a block member".
String blockInstanceName;
String blockName;
Int blockMemberIndex = -1;
// Which element of an arrayed block member this capture names, -1 for "the
// member as a whole". SPIR-V cannot decorate a single array element, so a
// backend needs the element index to tell a full run from a partial one.
Int blockMemberElement = -1;
};
// ---- P1: everything a link PRODUCES, in one movable block ----
//
// The membership rule is mechanical, not editorial: this is exactly the field list
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
// as much link output). Nothing else belongs here.
//
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
// of thirty cross-thread field assignments. Until then this is a pure refactor.
//
// Access rule (invariant I5): the member below is private and reachable ONLY
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
// what makes "every read of link output joins the pending link" a property the
// compiler checks rather than a review item - a new reader cannot spell the field
// without going through the gate.
// ---- the owned mirror of glslang's reflection ----
//
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
// asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix()
// and friends. That made the TProgram part of the program's PERMANENT state, which
// in turn made the whole front end (parse + link) unskippable: the L1 shader
// translation memo could hand back the SPIR-V but the reflection still had to be
// rebuilt from a freshly parsed AST.
//
// These three tables are a snapshot of everything the query surface ever reads off
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
// they are copyable, immutable after the link, and safe to memoize and share between
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
// everything except DoReflection itself.
//
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
// instead, unchanged in every other respect.
struct LinkArtifacts {
// Live only between LinkProgram() and the end of DoReflection. Everything after
// that reads the owned mirror below; a link served from the L1 memo never
// constructs one at all, so this is null for such a program and MUST NOT be
// dereferenced outside DoReflection.
SharedPtr<glslang::TProgram> program;
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
Vector<UniformReflection> uniformReflection;
Vector<BlockReflection> blockReflection;
Vector<PipeInputReflection> pipeInputReflection;
Vector<PipeOutputReflection> pipeOutputReflection;
// Program-level scalars glslang answers off the linked intermediates.
// Whether the program's LAST stage is the fragment stage. A color number - and so a
// color index - exists only there; a separable tess/geometry/vertex program's
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
// separate-programs-tess-control).
Bool lastStageIsFragment = false;
Array<GLuint, 3> computeLocalSize{};
// Replaces program->getUniformIndex(name). Maps the reflected name to its
// TProgram uniform index.
UnorderedMap<String, Int> uniformIndexByName;
// Attributes (Vertex in)
Vector<String> attribs;
Vector<GLenum> attribTypes;
// FragData (Frag out): the per-link snapshot of the explicit request maps.
UnorderedMap<String, Uint> linkedFragDataLocation;
UnorderedMap<String, Uint> linkedFragDataIndex;
// GL-facing index spaces (see the translation helpers above): GL active-uniform
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
Vector<Int> glUniformIndexToTProgram;
Vector<Int> tProgramUniformIndexToGl;
Vector<Int> glBlockIndexToTProgram;
Vector<Int> tProgramBlockIndexToGl;
// GL_UNIFORM_BLOCK index space: ACTUAL uniform blocks only, a strict subsequence of
// glBlockIndexToTProgram above.
//
// That list is the BLOCK space - everything the relaxed parse produced except
// MGL_GLOBAL_UBO - and it is what the backends walk and what every block-keyed table
// here (uniformBlockBinding, uniformBlockIndexByName, blockReflection ordering) is
// indexed by. It is NOT the GL uniform-block list: MobileGL does not pass
// EShReflectionSeparateBuffers to buildReflection, so glslang routes BUFFER blocks
// through indexToUniformBlock too, and the list therefore also carries every shader
// storage block and every synthesized gl_AtomicCounterBlock_N. GL 4.6 core 7.6 gives
// those their own enumerations (GL_SHADER_STORAGE_BLOCK and
// GL_ACTIVE_ATOMIC_COUNTER_BUFFERS respectively), and GL_ACTIVE_UNIFORM_BLOCKS /
// glGetActiveUniformBlock*/glGetUniformBlockIndex must not see either.
//
// Kept as a SECOND space rather than filtering the first in place: DirectGLES assigns
// one ESSL uniform-buffer binding point per entry of the block list as it walks it
// (Managers.cpp CacheResourceLocations and the matching per-draw loop in
// DirectGLES.cpp), so compacting that list would renumber every backend binding
// point, and tProgramBlockIndexToGl[i] < 0 is what DoReflection and
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
// shaders' default-block uniforms declared, as glslang recorded them at the point
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
// DoReflection assigner restores them from here).
UnorderedMap<String, Int> linkedExplicitUniformLocations;
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
// reads until the application overwrites it, and relinking restores it - but the
// relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V
// cannot carry an initializer, so the value only survives as this side-channel.
// Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues).
Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;
UnorderedMap<String, Uint> uniformLocations;
// ---- "written since link" (see MarkUniformWrittenAtLocation) ----
// In LinkArtifacts deliberately: a link is exactly the event that retracts every
// write (GL resets uniforms to their initial values), so living here means the set
// is cleared by the same three paths that clear the rest of a link's output -
// Link()'s whole-struct reset, ResetLinkArtifacts, and the publish's move - and no
// fourth reset site can be forgotten. Empty (and never allocated) for a program
// that never asked to be separable.
Vector<Uint64> writtenUniformLocationBits;
Vector<Uint64> writtenUniformIndexBits;
Vector<Uint> writtenUniformIndices;
// Ordered by location,
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
Vector<Int> uniformIndexInTProgram;
// ditto. Will be set at glUniform1i
Vector<Int> uniformSamplerOrImageUnitIndex;
// Sampler/image layout(binding = N) initial texture/image units, captured by
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
// qualifier still says what the shader declared. An OUTPUT of the link, not an
// input to it: nothing supplies this map, the resolver fills it.
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
// Ordered by uniform block index
// index is DIFFERENT from binding!!!
//
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
// `prog->getUniformBlock(i) == "BlockName"`
// These stuff are present for GL semantics, not for backend inspection
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
UnorderedMap<String, Uint> uniformBlockIndexByName;
Vector<Int> uniformBlockBinding;
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
// SetShaderStorageBlockBinding for why this one is by name and not by index.
//
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
// GL-mandated binding 0 for every storage block whose shader declared no
// layout(binding = N). Those blocks have no other way to be told apart from a block
// that declared one: glslang's IO mapper invents a binding and writes it into the
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
// default binding for this block", and a later glShaderStorageBlockBinding simply
// overwrites it - default and rebind travel one path.
UnorderedMap<String, Int> shaderStorageBlockBinding;
// Block type names of the storage blocks the program's shaders declared with NO
// layout(binding = N). Input to the seeding above; filled during mapIO by
// TMglGlslIoResolver, which is the last observer that can still tell a declared
// binding from an invented one - and, unlike the per-shader lexer this replaced,
// sees the declaration with its macros expanded.
std::set<String> storageBlocksWithoutBinding;
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
// auto-mapper assigns every uniform block a binding whether or not the shader asked
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
std::set<String> uniformBlocksWithoutBinding;
Uint activeUniformCount = 0;
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
// and the draw path owes it the draw framebuffer's sample count before every draw.
//
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
// gate has to be answerable without joining the SPIR-V job, or every draw of every
// program would pay a join to discover it has nothing to write.
Bool usesReservedNumSamples = false;
Uint maxUniformLocation = 0;
Int uniformNameMaxLength = 0;
Int attribInNameMaxLength = 0;
Int uniformBlockNameMaxLength = 0;
String infoLog;
Bool linkStatus = false;
// Transform feedback: the linked snapshot (the request lives outside, on the
// GL-thread-owned side).
Vector<XfbVarying> xfbVaryings;
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
// xfbVaryings deliberately drops (they steer the capture layout and must never
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
// full request, pseudo-varyings and all, so the interface query needs its own copy.
Vector<String> xfbInterfaceNames;
Vector<Uint32> xfbStrides;
Vector<Uint32> gsStripTriangles;
Bool gsStripCaptureFixup = false;
GLenum gsInputPrimitive = GL_NONE;
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
// tessellation control stage, or 0 when the program has none. Checked against
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
Int tcsOutputVertices = 0;
// The rest of the geometry stage's link properties, and the tessellation evaluation
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
// the query surface listed the geometry pnames only to fall through to
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
// come from the linked intermediates for the same reason gsInputPrimitive and
// tcsOutputVertices do - glslang has already merged the compilation units' layout
// qualifiers and diagnosed contradictions, so the linked program is the thing that
// knows.
GLenum gsOutputPrimitive = GL_NONE;
Int gsMaxVertices = 0;
Int gsInvocations = 0;
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
GLenum tessGenMode = GL_NONE;
GLenum tessGenSpacing = GL_NONE;
GLenum tessGenVertexOrder = GL_NONE;
Bool tessGenPointMode = false;
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int xfbVaryingNameMaxLength = 0;
Bool xfbNeedsScatteredCapture = false;
Uint32 xfbPackedStride = 0;
};
// ---- everything phase B of a link produces, in one movable block ----
//
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
// readers of SPIR-V-derived data through their own join gate by compiler rather than
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
//
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
// cannot be lifted out of glslang's reflection instead.
struct SpirvArtifacts {
Vector<Vector<unsigned>> generatedSpirv;
Bool enableSpirvValidation = false;
// Byte offset of each uniform location inside globalUboScratch, or
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
Vector<Uint> uniformOffsets;
Vector<Uint8> globalUboScratch;
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
// uniformOffsets, because the member has no GL location at all: the link task keeps
// it out of the GL-visible uniform index space so no application can see or write it.
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
// False for a program whose SPIR-V was never produced (phase B cancelled at
// teardown or by a relink) or whose optimizer run failed. GL has no way to
// retract a LINK_STATUS it already reported true, so such a program stays
// "linked" and every reflection answer it has given stays correct - it is simply
// not drawable, which the backends already express through their link-status
// gates.
Bool spirvStatus = false;
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
// different offset in each. Recorded here rather than re-derived from the backend
// because it is the layout THESE modules were built with: it is what the routing
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
// width the shader actually declares.
Bool nativeFloat64 = false;
// Whether gl_PointSize was demoted out of THESE modules' tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) because the backend cannot host
// the built-in there. Per PROGRAM by construction - a consumer whose producer
// kept the built-in would read garbage - and recorded here rather than
// re-derived because it cannot be: the rewrite's whole point is that the final
// bytes no longer declare the capability that armed it. The backends read it to
// respell a "gl_PointSize" transform-feedback capture as the carrier
// (ShaderCompiler::POINT_SIZE_CAPTURE_CARRIER_NAME). The GL reflection surface
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
// so every query keeps the truthful GL spelling.
Bool pointSizeDemoted = false;
};
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
// Static and taking the block explicitly, because from stage 4 the link BODY needs
// them while its artifacts still live on the job node, not on any ProgramObject. The
@@ -7,6 +7,7 @@
// End of Source File Header
#include "RenderState.h"
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include "MG_Util/Debug/Log.h"
#include "MG_Util/Types.h"
@@ -8,367 +8,9 @@
#pragma once
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_Pipe/MGPipeValueTypes.h>
namespace MobileGL {
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, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> 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, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> 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;
};
namespace MG_State {
namespace GLState {
class RenderState {
@@ -8,93 +8,9 @@
#pragma once
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h>
#include <MG_Pipe/MGPipeValueTypes.h>
namespace MobileGL {
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 {
namespace GLState {
class SamplerObject {
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_Util/Miscellany/IndexGenerator.h>
#include "MG_State/GLState/TextureState/TextureObject.h"
#include "MG_Util/Types.h"
@@ -75,7 +76,14 @@ namespace MobileGL::MG_State::GLState {
// Units above it have provably-empty binding slots, so per-draw backend scans
// can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units.
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) {
m_maxTouchedUnit = unit;
// Push-on-mutation (MG_Pipe/PipeMutation.h): the high-water mark is a pushed
// PipeInputs field, and a bind reached from inside a verb - a backend binding
// its own synthesised fallback texture - would otherwise leave the block
// describing a smaller scan range than the live context has.
MGP_NOTE_MUTATION(GetMaxTouchedTextureUnit);
}
// Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit /
// glBindTextures / glBindSampler) routes through here, so bumping the generation here
// - plus in MarkTextureObjectForDeletion for delete-unbind - covers every change to
@@ -85,11 +93,23 @@ namespace MobileGL::MG_State::GLState {
// Re-binding the object a slot already holds changes nothing that the generation
// guards; such callers pass bindingChanged=false so only the high-water mark advances
// and the backend fast path survives the redundant re-binds apps issue every frame.
if (bindingChanged) ++m_textureBindGeneration;
if (bindingChanged) {
++m_textureBindGeneration;
MGP_NOTE_MUTATION(GetTextureBindGeneration);
}
}
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
void BumpTextureBindGeneration() { ++m_textureBindGeneration; }
// Both counters below are pushed PipeInputs fields AND are moved by writes the
// backends make into frontend objects during their own verb - a synthesised fallback
// texture's AllocateStorage/SetInternalFormat, a sampler override's SetMinFilter, a
// default texture becoming defined. Every such path funnels through these two
// methods (and the bind branch above), so noticing here covers the whole family
// rather than each writer (P1 lane finding F2; MG_Pipe/PipeMutation.h).
void BumpTextureBindGeneration() {
++m_textureBindGeneration;
MGP_NOTE_MUTATION(GetTextureBindGeneration);
}
// Sibling of the bind generation for everything that changes WHICH native texture a
// backend ends up putting on a unit WITHOUT any binding moving. Two families feed it:
@@ -108,7 +128,10 @@ namespace MobileGL::MG_State::GLState {
// its sampled-set memo carries THIS generation alongside the bind one. Any memo of a
// resolved per-unit binding - or of which textures a draw samples at all - needs both.
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
void BumpSamplingResolutionGeneration() {
++m_samplingResolutionGeneration;
MGP_NOTE_MUTATION(GetSamplingResolutionGeneration);
}
// Globally-unique, never-reused id of THIS texture state, i.e. of the context that owns
// it. Both generations above restart at 0 with a new context, so a backend memo keyed on
@@ -10,65 +10,11 @@
#include <Includes.h>
#include "../BufferState/BufferObject.h"
#include "MG_Util/Types.h"
#include <MG_Pipe/MGPipeValueTypes.h>
namespace MobileGL {
namespace MG_State {
namespace GLState {
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;
};
class VertexArrayObject {
public:
// Storage capacity, not the GL-visible limit. GL_MAX_VERTEX_ATTRIBS is reported as
+3
View File
@@ -88,6 +88,9 @@ add_subdirectory(Pipeline)
# The MGPipe catalogue arithmetic: no GL context and no driver, just the .def, the seven
# generated files and the payload layouts.
add_subdirectory(Pipe)
# The P0.5 interface-purity gate: a python walk of #include lines, so it registers with no
# MobileGL build, no GL context and no submodules (scripts/check_include_closure.py).
add_subdirectory(Purity)
add_subdirectory(ShaderTranspiler)
add_subdirectory(Util)
add_subdirectory(SelfTest)
@@ -22,6 +22,7 @@
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h>
#include <MG_Test/ScopedPipeVerb.h>
using namespace MobileGL;
@@ -1229,6 +1230,11 @@ TEST_F(FramebufferTest, DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWrite
// What the application asked for: write every channel of every draw buffer.
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// SyncRenderState is reached from a verb, never on its own: a test that calls it directly
// has to say which verb it stands in, or the block it reads is unfilled and unstamped and
// its first read is Fatal{UnmigratedPipeInput} in a push build. forColorClear=false is the
// draw arm.
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
// What the driver was told. Draw buffer 0 is untouched; draw buffer 1 loses alpha.
@@ -1260,14 +1266,22 @@ TEST_F(FramebufferTest, ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitu
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// A draw first, so the mask really is doctored when the clear arrives...
{
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
}
ASSERT_EQ(g_driverIndexedColorMasks[1].a, GL_FALSE);
// ...and now the clear, with NOTHING changed in the frontend parameter block. The frontend's
// render-state version has not moved, so only the purpose-aware memo can force this push -
// without it the clear would inherit the draw's alpha-off mask and never write the 1.0.
ResetRecordedColorMasks();
{
// A different verb CLASS, so a scope of its own: kClear's fill set is what a clear may
// read, and this half has to go through on that set alone.
MG_Test::ScopedPipeVerb clear(MG_Pipe::MGPipeVerb::Clear);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true);
}
ASSERT_TRUE(g_driverIndexedColorMasks[1].seen) << "the clear must re-push the colour mask";
EXPECT_EQ(g_driverIndexedColorMasks[1].a, GL_TRUE) << "a clear is what puts the 1.0 in the stored alpha";
@@ -1305,6 +1319,7 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
MG_Impl::GLImpl::ColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
MG_Impl::GLImpl::ColorMaski(1, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
MG_Impl::GLImpl::ColorMaski(2, GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
EXPECT_EQ(g_driverIndexedColorMasks[0].a, GL_FALSE) << "the application's own alpha mask survives";
@@ -1337,6 +1352,7 @@ TEST_F(FramebufferTest, DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsTh
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_SRC1_* is core since 3.3; glBlendFunc must take it";
ResetRecordedBlend();
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
@@ -1357,6 +1373,7 @@ TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensio
ResetRecordedBlend();
// The whole point: this used to be `throw std::runtime_error` straight through the GL ABI.
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
ASSERT_TRUE(g_driverBlend[0].enableSeen) << "the blend enable still has to be pushed";
@@ -1374,6 +1391,7 @@ TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensio
// is what has to push it.
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
ResetRecordedBlend();
draw.Renew();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_TRUE(g_driverBlend[0].enabled);
@@ -1400,6 +1418,7 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
ResetRecordedBlend();
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
@@ -1416,7 +1435,10 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
// the flag only steers the alpha-widen colour mask, so it must not reopen this either.
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
ResetRecordedBlend();
{
MG_Test::ScopedPipeVerb clear(MG_Pipe::MGPipeVerb::Clear);
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/true));
}
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR)) << "draw buffer " << i;
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR)) << "draw buffer " << i;
@@ -1427,6 +1449,7 @@ TEST_F(FramebufferTest, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled) {
MG_Impl::GLImpl::Enable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
ResetRecordedBlend();
draw.Renew();
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
EXPECT_TRUE(g_driverBlend[0].enabled) << "the enable has to be pushed - the shadow said 'off' because it was";
@@ -1444,6 +1467,7 @@ TEST_F(FramebufferTest, DualSourceFactorsWithBlendingDisabledStillReachACapableD
MG_Impl::GLImpl::Disable(GL_BLEND);
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
ResetRecordedBlend();
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
+30
View File
@@ -24,5 +24,35 @@ if (MSVC)
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
endif()
# The P1 poison and verify shapes at the block level: a fake GLContext, the real filler and
# accessors out of the static library. Links gtest (not gtest_main): the suite needs its own
# main() to point MOBILEGL_LOG_FILE_PATH at a temp file before anything logs, because its
# abort cases read the Fatal line back out of that file. Every case is a visible SKIP in a
# pull build.
add_executable(
PipeInputsTest
PipeInputsTest.cpp
)
target_include_directories(PipeInputsTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
PipeInputsTest PRIVATE
GTest::gtest
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(PipeInputsTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+151 -1
View File
@@ -13,6 +13,7 @@
#include <gtest/gtest.h>
#include <cstring>
#include <limits>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
@@ -113,6 +114,39 @@ TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
EXPECT_GE(sizeof(ResidualValueBlock), sizeof(RenderStateParameters) + sizeof(PixelStoreParameters));
}
// P0.5 moved the value structs into MG_Pipe/MGPipeValueTypes.h. These are the runtime twins
// of that header's static assertions, so the numbers show up in ctest output on every
// platform - including one where a static assertion is skipped. Every number here is also
// what MGL_RESIDUAL_BLOCK_SIZE (MGPipeTypes.h) and the Espryt offsetof spans depend on.
TEST(PipeCatalogue, ValueTypeLayoutsArePinned) {
EXPECT_EQ(sizeof(PixelStoreParameters), 28u);
EXPECT_EQ(sizeof(PerBufferBlendState), 28u);
EXPECT_EQ(sizeof(StencilFaceState), 28u);
EXPECT_EQ(sizeof(RenderStateParameters), 1168u);
EXPECT_EQ(sizeof(SamplerParameters), 100u);
EXPECT_EQ(sizeof(MG_State::GLState::VertexAttributeVersion), 6u);
EXPECT_TRUE(std::is_trivially_copyable_v<PixelStoreParameters>);
EXPECT_TRUE(std::is_trivially_copyable_v<PerBufferBlendState>);
EXPECT_TRUE(std::is_trivially_copyable_v<StencilFaceState>);
EXPECT_TRUE(std::is_trivially_copyable_v<RenderStateParameters>);
EXPECT_TRUE(std::is_standard_layout_v<RenderStateParameters>);
EXPECT_TRUE(std::is_trivially_copyable_v<SamplerParameters>);
EXPECT_TRUE(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion>);
EXPECT_LT(offsetof(RenderStateParameters, BlendStates), offsetof(RenderStateParameters, LogicOp));
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)>, static_cast<SizeT>(kMGMaxDrawBuffers));
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::ColorMasks)>, static_cast<SizeT>(kMGMaxDrawBuffers));
EXPECT_EQ(kMGMaxDrawBuffers, 8u);
}
// The move did not alter the carrier: the residual block is still the render-state struct,
// then the pack struct, then the 8-aligned capability word, at the offsets it had before.
TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
EXPECT_EQ(offsetof(ResidualValueBlock, RenderState), 0u);
EXPECT_EQ(offsetof(ResidualValueBlock, Pack), sizeof(RenderStateParameters));
EXPECT_EQ(offsetof(ResidualValueBlock, CapabilityBits), 1200u);
EXPECT_EQ(offsetof(ResidualValueBlock, PatchVertices), 1208u);
}
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
@@ -189,8 +223,14 @@ TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
// G5's field ids come from the same accessor list as the coverage table, and every field
// starts un-filled: reading one before its verb fills it is the poison's whole job.
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
EXPECT_EQ(kMGPipeInputFieldCount, 61u);
EXPECT_EQ(kMGPipeInputFieldCount, 63u);
MGPipeFilledState state{};
// Before the first fill the serial is 0 as well: 0 == 0 must not read as fresh, on the
// sticky branch either (the window D6 names "<Field>@<none>").
EXPECT_EQ(state.CurrentVerbSerial, 0u);
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, static_cast<MGPipeInputField>(f))) << kMGPipeInputFieldNames[f];
}
state.CurrentVerbSerial = 1;
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
state.FilledGen[static_cast<SizeT>(MGPipeInputField::GetRenderStateParameters)] = 1;
@@ -200,6 +240,116 @@ TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
}
// G5b: the verb enum is GLFunctionsTable's member list (69 entries), every class has verbs,
// and the seven sticky fields ride in every class mask (P1 brief D7).
TEST(PipeCatalogue, VerbTableIsTheFunctionTable) {
EXPECT_EQ(kMGPipeVerbCount, 69u);
EXPECT_EQ(kMGPipeVerbClassCount, 9u);
SizeT perClass[kMGPipeVerbClassCount] = {};
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
++perClass[static_cast<SizeT>(kMGPipeVerbClass[v])];
}
for (SizeT c = 0; c < kMGPipeVerbClassCount; ++c) {
EXPECT_GT(perClass[c], 0u) << kMGPipeVerbClassNames[c];
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
if (kMGPipeInputFieldSticky[f]) {
EXPECT_TRUE(MGPipeFieldMaskHas(kMGPipeClassFieldMask[c], static_cast<MGPipeInputField>(f)))
<< kMGPipeInputFieldNames[f] << " in " << kMGPipeVerbClassNames[c];
}
}
}
// The class table of D7, spot-checked at its edges: a draw reads the render state, a
// query reads only the paused-primitive counter, and GenerateMipmap is a texture op.
const auto& draw = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
const auto& query = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kQuery)];
EXPECT_TRUE(MGPipeFieldMaskHas(draw, MGPipeInputField::GetRenderStateParameters));
EXPECT_FALSE(MGPipeFieldMaskHas(query, MGPipeInputField::GetRenderStateParameters));
EXPECT_TRUE(MGPipeFieldMaskHas(query, MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter));
EXPECT_EQ(kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)], MGPipeVerbClass::kTextureOp);
EXPECT_STREQ(kMGPipeVerbNames[static_cast<SizeT>(MGPipeVerb::GetGpuTimestampNs)], "GetGpuTimestampNs");
}
// The sticky set is exactly the seven forwarded, argument-keyed accessors (P1 brief D6); no
// version or generation accessor is among them.
TEST(PipeCatalogue, StickyFieldsAreExactlyTheSeven) {
const char* const expected[] = {"GetBufferBindingPointCount", "GetProgramObject", "GetTextureObject",
"HasOpenTransformFeedbackSpan", "InvalidateCompileEnv", "ValidateProgramName",
"RecordError"};
SizeT count = 0;
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
Bool listed = false;
for (const char* name : expected) {
if (std::strcmp(kMGPipeInputFieldNames[f], name) == 0) listed = true;
}
EXPECT_EQ(kMGPipeInputFieldSticky[f], listed) << kMGPipeInputFieldNames[f];
if (kMGPipeInputFieldSticky[f]) ++count;
}
EXPECT_EQ(count, 7u);
EXPECT_EQ(kMGPipeInputStickyFieldCount, 7u);
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetTextureContextId)]);
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetSamplingResolutionGeneration)]);
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetPipelineStateVersion)]);
}
// G4 compares floating point BY BITS (P1 brief D8): a NaN equals itself, a negative zero
// does not equal a positive one, and a vector type inside an Array inside a value struct is
// reached field by field - the differing member of the residual block is named.
TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
const Float nan = std::numeric_limits<Float>::quiet_NaN();
const FloatVec4 a{nan, 1.f, 2.f, 3.f};
const FloatVec4 b{nan, 1.f, 2.f, 3.f};
EXPECT_TRUE(MGPipeFieldEqual(a, b));
EXPECT_FALSE(a == b); // IEEE ==, the comparison the comparator must NOT use
const FloatVec4 zero{0.f, 0.f, 0.f, 0.f};
const FloatVec4 negativeZero{-0.f, 0.f, 0.f, 0.f};
EXPECT_FALSE(MGPipeFieldEqual(zero, negativeZero));
EXPECT_TRUE(zero == negativeZero);
EXPECT_TRUE(MGPipeFieldEqual(1.5f, 1.5f));
EXPECT_FALSE(MGPipeFieldEqual(-0.f, 0.f));
ResidualValueBlock left{};
ResidualValueBlock right{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(left, right, &field));
right.RenderState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
EXPECT_FALSE(MGPipeVerify(left, right, &field));
EXPECT_STREQ(field, "RenderState");
const char* inner = nullptr;
EXPECT_FALSE(MGPipeVerify(left.RenderState, right.RenderState, &inner));
EXPECT_STREQ(inner, "BlendStates");
// A NaN patch level in the render state equals itself too.
right = left;
left.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
right.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
EXPECT_TRUE(MGPipeVerify(left, right, &field));
}
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
// the struct that used to memcmp is compared member by member.
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 69u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
static_assert(MGPipeHasFieldVerifier<StencilFaceState>::value);
static_assert(MGPipeHasFieldVerifier<DynamicBackendParameters>::value);
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
PixelStoreParameters p{};
PixelStoreParameters q{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(p, q, &field));
q.SkipRows = 2;
EXPECT_FALSE(MGPipeVerify(p, q, &field));
EXPECT_STREQ(field, "SkipRows");
MGHostSpan s{};
MGHostSpan t{};
t.Pad0 = 0x5A; // padding is not a field
EXPECT_TRUE(MGPipeVerify(s, t, &field));
t.Offset = 8;
EXPECT_FALSE(MGPipeVerify(s, t, &field));
EXPECT_STREQ(field, "Offset");
}
// G7 pins the member list the pipeline/dynamic split is derived from.
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
+644
View File
@@ -0,0 +1,644 @@
// MobileGL - MobileGL/MG_Test/Pipe/PipeInputsTest.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 P1 poison and verify shapes at the block level (P1 brief C.1): a fake GLContext, the
// real filler, the real accessors. Needs the push sources, so every case is a visible SKIP
// in a pull build rather than a vanishing test. The abort cases fork (HeadlessGL.cpp's
// pre-flight shape): the child performs the read that must be Fatal{UnmigratedPipeInput}
// and the parent reads SIGABRT out of waitpid and the exact line out of the log file that
// main() below points MOBILEGL_LOG_FILE_PATH at (LogLevelTest's shape). Never EXPECT_DEATH.
//
// The log file is per PROCESS: gtest_discover_tests runs every case as its own process, in
// parallel under ctest -j, and a name shared between them let a sibling's unlink or Fatal
// line land in this process's read. A forked child inherits its parent's path on purpose.
#include <gtest/gtest.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_State/GLState/Core.h>
#endif
#if !defined(_WIN32)
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
#define MGTEST_HAVE_FORK 1
#else
#include <process.h>
#define MGTEST_HAVE_FORK 0
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
std::string g_logPath;
std::string ReadLog() {
std::ifstream in(g_logPath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
long ProcessId() {
#if defined(_WIN32)
return static_cast<long>(::_getpid());
#else
return static_cast<long>(::getpid());
#endif
}
#if MOBILEGL_PIPE_PUSH
using GLContext = MG_State::GLState::GLContext;
// A live frontend context for the filler to read (SanityTest's idiom), restored on the
// way out so the cases stay independent.
class PipeInputsTest : public ::testing::Test {
protected:
void SetUp() override {
m_previous = Move(MG_State::pGLContext);
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeSetPoisonOmission(nullptr, nullptr);
}
void TearDown() override {
MGPipeSetPoisonOmission(nullptr, nullptr);
MG_State::pGLContext = Move(m_previous);
}
UniquePtr<GLContext> m_previous;
};
#if MOBILEGL_PIPE_POISON
Bool Fresh(MGPipeInputField field) { return MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field); }
#endif
[[maybe_unused]] constexpr const char* kOmittedFatal ="Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}";
[[maybe_unused]] constexpr const char* kOmissionKnob = "GenerateMipmap:GetActiveTextureUnit";
#if MGTEST_HAVE_FORK
struct ChildResult {
int Status = -1; // waitpid's status; -1 when fork or waitpid failed
std::string Log; // the lines the child appended to the log file
};
// Runs `body` in a forked child and returns its wait status and log delta. The child
// must not use gtest assertions; it _exit(0)s when `body` returns, so a body that is
// expected to die must be asserted dead by the parent (WIFSIGNALED), never assumed.
template <class Body>
ChildResult RunInChild(Body body) {
ChildResult result;
const std::string before = ReadLog();
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return result;
if (pid == 0) {
body();
::_exit(0);
}
int status = 0;
if (::waitpid(pid, &status, 0) != pid) return result;
result.Status = status;
result.Log = ReadLog().substr(before.size());
return result;
}
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
Bool ExitedWith(const ChildResult& r, int code) { return WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == code; }
std::string DescribeStatus(const ChildResult& r) {
if (r.Status < 0) return "fork/waitpid failed";
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
return "status " + std::to_string(r.Status);
}
#endif // MGTEST_HAVE_FORK
#endif // MOBILEGL_PIPE_PUSH
} // namespace
#if !MOBILEGL_PIPE_PUSH
TEST(PipeInputsTest,OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,ReadingAnOmittedFieldAbortsNamingTheVerb) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,ReadingAFilledFieldCompletes) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,ReadingBeforeAnyFillAbortsNamingNoVerb) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,CorruptedSnapshotFieldIsNamedWithItsSerial) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,EveryVerbFillsItsClassAndNothingElse) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,MutatedFieldIsNamedAtRead) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,PoisonOmitKnobArmsTheOmission) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,BadPoisonOmitKnobIsFatalNamingTheKnob) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,VerifyCorruptKnobNamesTheFieldAtEntry) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,BadVerifyCorruptKnobIsFatalNamingTheKnob) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,VerifyFatalOffLogsTheDivergenceAndContinues) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,AFrontendMutationInsideAVerbRefreshesThePushedField) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,TheMutationNoticeRefreshesTheValueButNotTheStamp) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(PipeInputsTest,AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
#else // MOBILEGL_PIPE_PUSH
// Negative control B, layer 1 (P1 brief D6 / G5): the omitted (verb, field) pair is the
// ONLY thing that goes stale - every other bit of the verb class's mask is fresh, every
// field outside it is not, and the verb after it neither heals the field (not in kDraw's
// mask) nor loses one of its own.
TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
// The value was still copied: only the stamp is withheld.
EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::GenerateMipmap);
// Exactly that field: a field is fresh iff its bit is in kTextureOp's mask and it is not
// the omitted one.
{
const auto cls = kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)];
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(cls)];
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
const auto field = static_cast<MGPipeInputField>(f);
const Bool expected = MGPipeFieldMaskHas(mask, field) && field != MGPipeInputField::GetActiveTextureUnit;
EXPECT_EQ(Fresh(field), expected) << kMGPipeInputFieldNames[f] << " after the omitted GenerateMipmap fill";
}
}
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetBoundVertexArray));
EXPECT_TRUE(Fresh(MGPipeInputField::GetRenderStateParameters));
// A sticky field stays fresh across every verb.
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
// And the omission is scoped to its verb: a different verb of the same class keeps it.
MGPipeFillForVerb(MGPipeVerb::BindImageTexture);
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
#endif
}
// Negative control B, layer 2 (G5): the read itself. The child fills GenerateMipmap with the
// omission and reads gPipeInputs.GetActiveTextureUnit(); the parent expects SIGABRT and the
// exact Fatal line, and that nothing else was fatal.
TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3); // reached only if the poison failed
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("@DrawArrays"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
#endif
}
// The sibling: the same reads without the omission complete, and no Fatal is logged.
TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) {
#if !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetRenderStateParameters();
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0);
(void)gPipeInputs.GetActiveTextureUnit();
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
#endif
}
// The window before the first verb (P1 brief D6: "<Field>@<none>"). Nothing has filled this
// process's block, so a backend-style read of a value field is Fatal naming no verb: the
// serial and every stamp are 0, and 0 == 0 is not fresh. In a verify build the child also
// sets the lane's knob first, the shape of a read reached from initialisation: the poison
// fires at MGP_INPUT_CHECK, before the read hook (armed only by the first fill) could matter.
TEST_F(PipeInputsTest, ReadingBeforeAnyFillAbortsNamingNoVerb) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
if (gPipeInputs.FilledState().CurrentVerbSerial != 0) {
GTEST_SKIP() << "another case already filled in this process; gtest_discover_tests runs each case alone";
}
MG_State::pGLContext->SetLineWidth(7.0f); // a live value the default storage (0) does not hold
const ChildResult r = RunInChild([] {
#if MOBILEGL_PIPE_VERIFY
MG_Config::Features.PipeVerify = true;
#endif
(void)gPipeInputs.GetLineWidth(); // Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}
::_exit(3); // reached only if the pre-fill window read as fresh
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetLineWidth@<none>\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("verify armed"), std::string::npos) << r.Log;
#endif
}
// The MOBILEGL_PIPE_POISON_OMIT parser, through the Features field the loader fills: the
// child sets the knob after its parent filled without it, and the first fill after that
// parses it, logs the arming line and withholds the stamp exactly as the programmatic form.
TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap"), std::string::npos)
<< r.Log;
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
#endif
}
// An unknown verb in the knob is Fatal{PipeVerifyBadKnob} naming the knob and the value,
// before any stamp is withheld.
TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
#if !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit\": "
"no such verb in kMGPipeVerbNames}"),
std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("poison omission armed"), std::string::npos) << r.Log;
#endif
}
// Negative control A at the block level (G4): a clean snapshot compares equal to the pushed
// block over the verb's mask; corrupting one field of the snapshot makes the compare name
// exactly that field, at the serial of the fill it belongs to.
TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#else
const Uint64 serialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial;
EXPECT_EQ(serial, serialBefore + 1);
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
static PipeInputs snapshot{};
SnapshotFromGLContext(snapshot, mask);
MGPipeInputField field = MGPipeInputField::kFieldCount;
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
EXPECT_EQ(field, MGPipeInputField::kFieldCount);
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetRenderStateParameters));
EXPECT_FALSE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
EXPECT_EQ(field, MGPipeInputField::GetRenderStateParameters);
EXPECT_STREQ(kMGPipeInputFieldNames[static_cast<SizeT>(field)], "GetRenderStateParameters");
// The serial the report would print is the fill's, and it stamped that field.
EXPECT_EQ(gPipeInputs.FilledState().FilledGen[static_cast<SizeT>(field)], serial);
// A forwarded field has nothing to corrupt, and the corruption of a field outside the
// mask is not seen by a compare over that mask.
EXPECT_FALSE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::RecordError));
SnapshotFromGLContext(snapshot, mask);
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetPixelStoreParameters));
EXPECT_FALSE(MGPipeFieldMaskHas(mask, MGPipeInputField::GetPixelStoreParameters));
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
#endif
}
// The compare-at-read arm (P1 brief D8, the arm that is real in P1): a value that changes in
// the live context between the verb boundary and the read is Fatal{PipeVerifyDiffer,
// "Field@Verb", verb=<serial>, where=read}; the same read before the mutation completes.
TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; // the child's DrawArrays fill
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
const Float boundary = gPipeInputs.GetLineWidth(); // boundary == live: completes
(void)gPipeInputs.GetRenderStateParameters();
MG_State::pGLContext->SetLineWidth(boundary + 1.0f);
if (MG_State::pGLContext->GetLineWidth() == boundary) ::_exit(7); // the mutation did not take
(void)gPipeInputs.GetLineWidth(); // the stored value is stale against the live one: Fatal
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify read of GetLineWidth (index 0, 0) differs from the live context"),
std::string::npos)
<< r.Log;
const std::string expected = "Fatal{PipeVerifyDiffer, \"GetLineWidth@DrawArrays\", verb=" + std::to_string(serial) +
", where=read}";
EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log;
EXPECT_EQ(r.Log.find("where=entry"), std::string::npos) << r.Log;
#endif
}
// The MOBILEGL_PIPE_VERIFY_CORRUPT parser through Features: the fill after the child arms it
// logs both arming lines and the entry compare names the corrupted field at that fill's
// serial, where=entry.
TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify corruption armed - GetRenderStateParameters"), std::string::npos) << r.Log;
const std::string expected = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" +
std::to_string(serial) + ", where=entry}";
EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log;
#endif
}
// An unknown field name in MOBILEGL_PIPE_VERIFY_CORRUPT is Fatal{PipeVerifyBadKnob} at
// arming, before the comparator reports anything.
TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "NoSuchField";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField\": "
"no such field in kMGPipeInputFieldNames}"),
std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("verify armed"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
#endif
}
// MOBILEGL_PIPE_VERIFY_FATAL=0: the divergence is logged with its field and serial and the
// process goes on (the fill returns, the next fill's compare runs again), and the teardown
// summary counts both. The child leaves through std::exit so the comparator's static
// destructor runs (a _exit would skip the summary line).
TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyFatal = false;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeFillForVerb(MGPipeVerb::DrawElements);
std::exit(0);
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=0"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify summary - 2 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0"),
std::string::npos)
<< r.Log;
const std::string first = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" +
std::to_string(serial) + ", where=entry}";
const std::string second = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawElements\", verb=" +
std::to_string(serial + 1) + ", where=entry}";
EXPECT_NE(r.Log.find(first), std::string::npos) << "expected " << first << " in\n" << r.Log;
EXPECT_NE(r.Log.find(second), std::string::npos) << "expected " << second << " in\n" << r.Log;
#endif
}
// Every verb fills exactly its class mask: after a fill, a field is fresh iff its bit is set
// (sticky fields included, since every class mask carries them).
TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
const auto verb = static_cast<MGPipeVerb>(v);
MGPipeFillForVerb(verb);
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[v])];
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
const auto field = static_cast<MGPipeInputField>(f);
EXPECT_EQ(Fresh(field), MGPipeFieldMaskHas(mask, field))
<< kMGPipeInputFieldNames[f] << " after " << kMGPipeVerbNames[v];
}
}
EXPECT_EQ(gPipeInputs.ContextIdentity(), static_cast<const void*>(MG_State::pGLContext.get()));
EXPECT_TRUE(gPipeInputs.IsLive());
#endif
}
// Push on mutation (P1 lane finding F2, MG_Pipe/PipeMutation.h). The backends write into
// frontend objects inside their own verb - Magma synthesises a fallback texture for an
// unbound sampler and overrides a unit's sampler filter (VulkanRenderer/UniformManager) -
// and every such write moves a counter the verb boundary already copied. The frontend
// mutator refreshes that field, so the block still equals the live context at every read.
// Without the notice the two reads below differ and the pushed value is the stale one.
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbRefreshesThePushedField) {
auto& ctx = *MG_State::pGLContext;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
ASSERT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
ASSERT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
ASSERT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
// What UniformManager's fallback path does mid-draw: change a sampler object's filter,
// which bumps the context-wide sampling-resolution generation (SamplerObject.cpp).
const Uint64 samplingBefore = ctx.GetSamplingResolutionGeneration();
auto sampler = MakeShared<MG_State::GLState::SamplerObject>(0u);
sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear
: SamplerFilterMode::Nearest);
ASSERT_NE(ctx.GetSamplingResolutionGeneration(), samplingBefore) << "the mutation did not move the counter";
EXPECT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
// And a bind reached from inside a verb moves both the bind generation and the
// high-water mark of touched units (TextureState::NoteUnitTouched).
const Uint64 bindBefore = ctx.GetTextureBindGeneration();
const Int unit = ctx.GetMaxTouchedTextureUnit() + 1;
ctx.NoteTextureUnitTouched(unit);
ASSERT_NE(ctx.GetTextureBindGeneration(), bindBefore) << "the bind did not move the counter";
ASSERT_EQ(ctx.GetMaxTouchedTextureUnit(), unit);
EXPECT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
EXPECT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
}
// The notice refreshes the VALUE and never the stamp: a field this verb withheld the stamp
// of (negative control B) must stay stale, and a field outside the verb class's mask must
// stay unfilled rather than be healed by an unrelated frontend write.
TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
auto& ctx = *MG_State::pGLContext;
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
<< "the notice restamped a field whose stamp the fill withheld";
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
MGPipeSetPoisonOmission(nullptr, nullptr);
MGPipeFillForVerb(MGPipeVerb::FenceSync);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
<< "the notice stamped a field the verb class never fills";
#endif
}
// The lane failure itself (six DirectVulkan.Verify.UnboundImageDescriptorScenario entries,
// two SampledSetStalenessScenario, two retrace cases): a frontend write made inside a draw
// used to make the very next read of the pushed block differ from the live context. The
// child reproduces it end to end under the armed comparator and must survive; without the
// notice it dies of Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
// where=read}.
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
#if !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetSamplingResolutionGeneration(); // boundary == live: completes
auto& ctx = *MG_State::pGLContext;
const Uint64 before = ctx.GetSamplingResolutionGeneration();
auto sampler = MakeShared<MG_State::GLState::SamplerObject>(0u);
sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear
: SamplerFilterMode::Nearest);
if (ctx.GetSamplingResolutionGeneration() == before) {
::_exit(7); // the mutation did not take: the case would pass for the wrong reason
}
(void)gPipeInputs.GetSamplingResolutionGeneration(); // Fatal{PipeVerifyDiffer} without the notice
ctx.NoteTextureUnitTouched(ctx.GetMaxTouchedTextureUnit() + 1);
(void)gPipeInputs.GetTextureBindGeneration();
(void)gPipeInputs.GetMaxTouchedTextureUnit();
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: verify armed"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log;
#endif
}
#endif // MOBILEGL_PIPE_PUSH
int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. The name carries this process's pid (see the file header),
// and the file is removed on the way out; a forked child that aborts leaves it to us.
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-pipeinputs-test-" + std::to_string(ProcessId()) + ".log");
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}
+21
View File
@@ -275,3 +275,24 @@ gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
# Same reason again: several cases leave A links outstanding while B compiles and links.
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
# P0.5: the ProgramArtifacts.h header on its own - the alias identity proofs, the POD trip
# wire and the archive field tables. Its first include is the artifacts header, so this
# binary is also the compile-time proof that the header is self-contained.
add_executable(
ProgramArtifactsTest
ProgramArtifactsTest.cpp
)
target_include_directories(ProgramArtifactsTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
ProgramArtifactsTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
gtest_discover_tests(ProgramArtifactsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,133 @@
// MobileGL - MobileGL/MG_Test/Program/ProgramArtifactsTest.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
// First include on purpose: the artifacts header must be self-contained (P0.5 gate A).
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
#include <gtest/gtest.h>
// For the alias checks only.
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#include <cstdio>
#include <set>
#include <string>
#include <type_traits>
namespace {
using namespace MobileGL;
using namespace MobileGL::MG_State::GLState;
// P0.5 moved the five types to namespace scope and left in-class aliases behind so the
// existing spellings compile unchanged. The classic failure of that move is a COPY: two
// same-named definitions that both compile and silently split the type. is_same_v is the
// compile-time proof that ProgramObject::X and GLState::X are one type.
TEST(ProgramArtifacts, AliasesAreTheSameTypes) {
static_assert(std::is_same_v<ProgramObject::TypeFacts, TypeFacts>);
static_assert(std::is_same_v<ProgramObject::ResourceReflection, ResourceReflection>);
static_assert(std::is_same_v<ProgramObject::XfbVarying, XfbVarying>);
static_assert(std::is_same_v<ProgramObject::LinkArtifacts, LinkArtifacts>);
static_assert(std::is_same_v<ProgramObject::SpirvArtifacts, SpirvArtifacts>);
static_assert(std::is_same_v<ProgramObject::UniformReflection, UniformReflection>);
static_assert(std::is_same_v<ProgramObject::BlockReflection, BlockReflection>);
static_assert(std::is_same_v<ProgramObject::PipeInputReflection, PipeInputReflection>);
static_assert(std::is_same_v<ProgramObject::PipeOutputReflection, PipeOutputReflection>);
static_assert(ProgramObject::kInvalidUniformOffset == kInvalidUniformOffset);
EXPECT_EQ(ProgramObject::kInvalidUniformOffset, kInvalidUniformOffset);
EXPECT_EQ(kInvalidUniformOffset, ~0u);
}
// 13 Bool + 3 bytes of padding + 7 x 4-byte scalars on every ABI.
TEST(ProgramArtifacts, TypeFactsIsPodOf44Bytes) {
static_assert(std::is_trivially_copyable_v<TypeFacts>);
static_assert(std::is_standard_layout_v<TypeFacts>);
EXPECT_EQ(sizeof(TypeFacts), 44u);
EXPECT_EQ(alignof(TypeFacts), 4u);
}
// A visitor that counts what a table hands it and checks the names are distinct - the
// archive's one field table per type has to name every member exactly once.
struct CountingVisitor {
std::size_t count = 0;
std::set<std::string> names;
template <class Field>
void operator()(const char* name, Field&) {
++count;
names.insert(name);
}
};
template <class T>
std::size_t CountFields() {
T value{};
CountingVisitor mutableVisitor;
VisitFields(value, mutableVisitor);
const T& constValue = value;
CountingVisitor constVisitor;
VisitFields(constValue, constVisitor);
EXPECT_EQ(mutableVisitor.count, constVisitor.count) << "const and non-const walks disagree";
EXPECT_EQ(mutableVisitor.names.size(), mutableVisitor.count) << "a field name is listed twice";
EXPECT_EQ(mutableVisitor.names, constVisitor.names);
return mutableVisitor.count;
}
// The field counts are the member counts of the moved structs (LinkArtifacts minus its
// never-archived `program`). A member added without a table entry is caught by the sizeof
// trip wires in the header; a table entry dropped without a member change is caught here.
TEST(ProgramArtifacts, VisitFieldsCoversEveryMember) {
EXPECT_EQ(CountFields<TypeFacts>(), 20u);
EXPECT_EQ(CountFields<ResourceReflection>(), 14u);
EXPECT_EQ(CountFields<XfbVarying>(), 11u);
EXPECT_EQ(CountFields<LinkArtifacts>(), 57u);
EXPECT_EQ(CountFields<SpirvArtifacts>(), 8u);
}
// A const walk hands the visitor const references, a non-const walk mutable ones: the one
// table really serves both directions.
TEST(ProgramArtifacts, VisitFieldsPassesConstnessThrough) {
LinkArtifacts artifacts;
std::size_t mutableFields = 0;
VisitFields(artifacts, [&](const char*, auto& field) {
static_assert(!std::is_const_v<std::remove_reference_t<decltype(field)>>);
++mutableFields;
});
const LinkArtifacts& constArtifacts = artifacts;
std::size_t constFields = 0;
VisitFields(constArtifacts, [&](const char*, auto& field) {
static_assert(std::is_const_v<std::remove_reference_t<decltype(field)>>);
++constFields;
});
EXPECT_EQ(mutableFields, constFields);
// The table can write through: a deserializer's shape.
VisitFields(artifacts, [](const char* name, auto& field) {
if constexpr (std::is_same_v<std::remove_reference_t<decltype(field)>, Bool>) {
if (std::string(name) == "linkStatus") field = true;
}
});
EXPECT_TRUE(artifacts.linkStatus);
}
// The sizeof numbers, visible in every `ctest -V` log on every platform: this is where the
// integrator reads a new toolchain's values from before pinning them in the header.
TEST(ProgramArtifacts, SizesArePinnedOnThisToolchain) {
RecordProperty("sizeof_TypeFacts", static_cast<int>(sizeof(TypeFacts)));
RecordProperty("sizeof_ResourceReflection", static_cast<int>(sizeof(ResourceReflection)));
RecordProperty("sizeof_XfbVarying", static_cast<int>(sizeof(XfbVarying)));
RecordProperty("sizeof_LinkArtifacts", static_cast<int>(sizeof(LinkArtifacts)));
RecordProperty("sizeof_SpirvArtifacts", static_cast<int>(sizeof(SpirvArtifacts)));
std::printf("sizeof: TypeFacts=%zu ResourceReflection=%zu XfbVarying=%zu LinkArtifacts=%zu SpirvArtifacts=%zu\n",
sizeof(TypeFacts), sizeof(ResourceReflection), sizeof(XfbVarying), sizeof(LinkArtifacts),
sizeof(SpirvArtifacts));
#ifdef MGL_LINKARTIFACTS_SIZE // whichever STL branch the header pinned, this twin follows it
EXPECT_EQ(sizeof(ResourceReflection), static_cast<std::size_t>(MGL_RESOURCEREFLECTION_SIZE));
EXPECT_EQ(sizeof(XfbVarying), static_cast<std::size_t>(MGL_XFBVARYING_SIZE));
EXPECT_EQ(sizeof(LinkArtifacts), static_cast<std::size_t>(MGL_LINKARTIFACTS_SIZE));
EXPECT_EQ(sizeof(SpirvArtifacts), static_cast<std::size_t>(MGL_SPIRVARTIFACTS_SIZE));
#else
RecordProperty("sizes_pinned", "no: not the libstdc++ 64-bit toolchain");
#endif
}
} // namespace
+15
View File
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.14)
# The P0.5 include-closure assertions (ROADMAP P0.5; ARCHITECTURE.md:501 gate A): no MobileGL
# compilation, no GL context. Text mode only here, because this CTestTestfile also runs on the
# `test` job's runner (test.yml:149-203), which has no submodules; the compiler-backed run is the
# include-graph-check job and the developer's local `--mode both`.
find_package(Python3 COMPONENTS Interpreter)
if (Python3_Interpreter_FOUND)
add_test(NAME MobileGLPurity.IncludeClosure
COMMAND ${Python3_EXECUTABLE} ${MGL_ROOT}/scripts/check_include_closure.py --mode text --self-test --require-all)
# ctest -L unit (test.yml:198-203). NO ENVIRONMENT property: it replaces the job env (ARCHITECTURE.md:567).
set_tests_properties(MobileGLPurity.IncludeClosure PROPERTIES LABELS unit)
else()
message(STATUS "MobileGLPurity.IncludeClosure not registered: no python3 interpreter (Windows is not a correctness gate)")
endif()
+16 -2
View File
@@ -25,6 +25,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_Test/ScopedPipeVerb.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
@@ -321,7 +322,10 @@ TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
// The symptom itself: the per-unit walk has to actually bind it.
// The symptom itself: the per-unit walk has to actually bind it. BindCurrentTextures() is
// the per-draw walk - it reads GetProgramForDraw - so the block it reads is the one a draw
// fills; without saying so, its first read is Fatal{UnmigratedPipeInput} in a push build.
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 1u)
<< "the multisample texture was not bound; every texelFetch against it reads zero";
@@ -362,6 +366,9 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
// Each walk below is the texture half of one draw, so each gets its own verb (the second
// and third stand after frontend state moved, exactly as a second entry point's fill would).
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 1u);
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
@@ -369,6 +376,7 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
// redundant rebind while the real 2D frontend object remains current.
draw.Renew();
DirectGLES::BindCurrentTextures();
EXPECT_EQ(state.bindCalls.size(), 1u);
@@ -379,6 +387,7 @@ TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
.GetBoundObject()
.get()));
draw.Renew();
DirectGLES::BindCurrentTextures();
ASSERT_EQ(state.bindCalls.size(), 2u);
@@ -3015,7 +3024,9 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
ASSERT_NE(resident, nullptr);
// First sync: builds the memo with unit 0 -> `resident`, and gives `resident`'s twin its
// 16x16 backend storage.
// 16x16 backend storage. The unit walk is the per-draw one, so both syncs below stand in a
// draw - the fill is what a real glDraw* would have done before reaching this helper.
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
auto* residentSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(resident.get());
ASSERT_NE(residentSlot, nullptr);
@@ -3031,6 +3042,9 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(foreign);
const SizeT specsBeforeReplay = specs.size();
// The second draw. Its fill re-reads the memo's keys off the live context, which is the
// premise being tested: the silent slot swap moved none of them.
draw.Renew();
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
// `foreign` must have been synced through its OWN twin...
+79
View File
@@ -0,0 +1,79 @@
// MobileGL - MobileGL/MG_Test/ScopedPipeVerb.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>
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/PipeFill.h>
#endif
namespace MobileGL::MG_Test {
// "This test is standing inside verb X."
//
// A unit test that constructs a GLContext by hand and then calls a BACKEND helper
// directly enters through no GL entry point, so no MGP_FILL ever fires (P1 brief D7) and
// in a push build the PipeInputs block the helper's MGB_CTX reads is empty and unstamped:
// its first accessor read is Fatal{UnmigratedPipeInput, "Field@<none>"}. The test is
// right and the poison is right - what was missing is the verb, and this object is how a
// test states it. Constructing it runs the real filler for `verb`, exactly the call
// MG_Impl makes before that verb reaches a backend; destroying it leaves the verb again.
//
// It is not an escape hatch and it weakens nothing:
// - it fills exactly kMGPipeClassFieldMask[class of verb] out of the live GLContext, so
// a read of a field that verb does not fill is still Fatal, naming the field and this
// verb - the fill table stays the only thing that says what a verb may read;
// - leaving the scope re-arms the poison. The exit fill is a kQuery verb, whose class
// mask is a single field, so every field this scope stamped goes stale the moment the
// scope ends. That matters when the suite runs as one process (a developer running
// the test binary directly, rather than one ctest entry per case): without it, one
// case's declaration would cover a later case that forgot to make one;
// - it is a no-op in the pull build, where MGB_CTX is the live context and there is
// nothing to fill, so the pull build stays byte-identical.
//
// Place it the way MGP_FILL is placed in production (P1 brief D7): immediately BEFORE the
// backend call, after every frontend mutation that call is meant to see. A test that
// drives the helper again with frontend state changed in between issues a second verb -
// Renew() - because that is what a second GL entry point would have done. A test that
// drives a helper of a DIFFERENT verb class opens a nested scope for it.
class ScopedPipeVerb {
public:
explicit ScopedPipeVerb([[maybe_unused]] MG_Pipe::MGPipeVerb verb)
#if MOBILEGL_PIPE_PUSH
: m_verb(verb) {
MG_Pipe::MGPipeFillForVerb(m_verb);
}
#else
{
}
#endif
ScopedPipeVerb(const ScopedPipeVerb&) = delete;
ScopedPipeVerb& operator=(const ScopedPipeVerb&) = delete;
// A second verb of the same kind begins: re-fill and re-stamp, so a helper driven
// again after the test moved frontend state sees the new values, the way the next GL
// entry point's MGP_FILL would.
void Renew() {
#if MOBILEGL_PIPE_PUSH
MG_Pipe::MGPipeFillForVerb(m_verb);
#endif
}
#if MOBILEGL_PIPE_PUSH
// Leaving bumps the serial and puts the current verb back to "none": every field
// this scope stamped goes stale, and a later test that forgets its own declaration
// aborts with "<Field>@<none>" rather than with the name of a verb it never issued.
// That matters when the suite runs as one process (a developer running the test
// binary directly, rather than one ctest entry per case).
~ScopedPipeVerb() { MG_Pipe::MGPipeLeaveVerb(); }
private:
MG_Pipe::MGPipeVerb m_verb;
#endif
};
} // namespace MobileGL::MG_Test
+7
View File
@@ -27,6 +27,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/TextureState/TextureObject2D.h>
#include <MG_Test/ScopedPipeVerb.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
@@ -4477,6 +4478,10 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams)
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 8); // rows of 3 words (12 B) pad to 16 B
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 1);
// The store reads the PACK block out of the frontend, and it is only ever reached from a
// readback verb: a test that calls it directly says so, or the block is unfilled and the
// first read is Fatal{UnmigratedPipeInput} in a push build.
MG_Test::ScopedPipeVerb readback(MG_Pipe::MGPipeVerb::ReadPixels);
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/2, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
@@ -4499,6 +4504,8 @@ TEST_F(TextureTest, StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams)
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SWAP_BYTES, GL_TRUE);
// A second readback, after the PACK parameters moved.
readback.Renew();
ASSERT_TRUE(ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(source), /*width=*/3,
/*sliceHeight=*/1, /*sliceCount=*/1,
GL_UNSIGNED_INT_5_9_9_9_REV, destination,
+2 -2
View File
@@ -257,7 +257,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 clientSPIRV-Cross`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex``uniformBlockBinding``shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings``xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture``computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples``uniformOffsets``XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts``ResourceReflection``XfbVarying``LinkArtifacts``SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),更新 7 个 includer,加 CI `-H` 闭包断言。同批抽取 `MG_Pipe/MGPipeValueTypes.h``MAX_DRAW_BUFFERS``PerBufferBlendState``StencilFaceState``PixelStoreParameters``RenderStateParameters``SamplerParameters``BorderColorForm``VertexAttribute``VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h``RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts``ResourceReflection``XfbVarying``LinkArtifacts``SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),8 个 includer 靠类内 `using` 别名零改动,加 CI `-H` 闭包断言(已落地,见 `ROADMAP.md` P0.5 行;`DynamicBackendParameters` 留在 `BackendObject.h`,所以闭包门 A 断言的是 `MGPipeValueTypes.h` 而不是 `MGPipeTypes.h`。同批抽取 `MG_Pipe/MGPipeValueTypes.h``MAX_DRAW_BUFFERS``PerBufferBlendState``StencilFaceState``PixelStoreParameters``RenderStateParameters``SamplerParameters``BorderColorForm``VertexAttribute``VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h``RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。
- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。
- Magma 的两个内部 shaderblit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 `MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。
@@ -341,7 +341,7 @@ struct PipeInputs {
| 阶段 | 改什么 | 证明 |
|---|---|---|
| A 别名 | 机械 `sed``MG_State::pGLContext->``MGB_CTX->`293 处)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
| A 别名 | 机械 `sed``MG_State::pGLContext->``MGB_CTX->`**实测 277 处**293 出自过期的 vendored 清单)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
| B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 |
| C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) |
+48 -1
View File
@@ -1,4 +1,4 @@
# P0 实测
# 实测记录(P0、P1
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74CAdreno 830Android 16`3B159D009VZ00000` = Oppo PLG110MaliAndroid 16ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
@@ -94,3 +94,50 @@ python3 tools/trace_replay/run_android_retrace_local.py \
3. `--env` 值里嵌入的 `/data/...` 会被 runner 的 bash.exe 做 MSYS 路径转换(`MSYS2_ARG_CONV_EXCL="/data/*"` 只覆盖开头匹配)——用 `MSYS_NO_PATHCONV=1` 跑。
4. `coherent_as_flush` 管线完好:`--ez coherent_as_flush true``trace_replay_core.cpp``setenv`,独立于 `--env` 透传。
5. **`minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败**Adreno 830Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`MaliSSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。
---
# P1 实测(`feat/disaggregated`lavapipe / llvmpipe
## 6. 规模:站点、访问器、填充点
| 量 | 值 | 出处 |
|---|---|---|
| 后端 `pGLContext->` 箭头站点 | 277Espryt 113、Magma 164 | 计划写 293,出自过期的 vendored 清单 |
| 非箭头行 | 58Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT` | 与计划一致 |
| `PipeInputs` 字段 | 63 | 计划写 61`GetBoundTransformFeedbackLifetimeId``HasOpenTransformFeedbackSpan` 是 D21 之后新增的读点 |
| 后端调用的不同访问器 | 62Espryt 32、Magma 56 | `GetBoundTransformFeedbackName` 已无人读,留作已标注的死行 |
| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 | `MG_Pipe/FillPoints.def` |
| `SyncPersistentMappedRange` / `SyncGpuWrites` | 20 / 6 | 与计划一致 |
## 7. verify 通道发现的两类真问题
**缺填充行(9 处)。** 逐 verb poison 在 79 例 retrace 与 818 条 integration-verify 上抓出三组:`kReadback``IsTransformFeedbackActive`/`IsTransformFeedbackPaused`(深度/模板回读仿真的 `ScopedEmulationDrawState` 会暂停在飞的捕获)、`kTextureOp``kDispatch``IsCapabilityEnabled``kBlitOrCopy`/`kTextureOp` 缺着色器 blit 用到的 viewport 与顶点/缓冲绑定。其中 8 行是静态过近似(代码路径可达但通道未跑到),过近似会让那对 (类, 字段) 的 poison 永久失效,**P2 收紧填充表时先复查这 8 行**。
**verb 内后端改前端(3 个字段)。** Magma 在自己的 draw 里写前端对象(为未绑定采样器合成回退纹理、材质化排队清除、覆写采样器 filter),把边界已经拷走的值挪了位,8 条 DirectVulkan 用例与 2 条 trace 因此报 `Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@Draw*", where=read}`。**选定的解法是 push-on-mutation**:前端计数器移动时用 `MGP_NOTE_MUTATION(Field)``MG_Pipe/PipeMutation.h`)刷新推送块里的那一个字段(只刷值不刷戳记),"推送块在每次读取时都等于活上下文"这条不变式因此字面成立,也正是 P2 tracker 需要的形状。
三个被这样处理的字段与它们的钩子:
| 字段 | 钩子 |
|---|---|
| `GetSamplingResolutionGeneration` | `TextureState::BumpSamplingResolutionGeneration()` |
| `GetTextureBindGeneration` | `TextureState::BumpTextureBindGeneration()``NoteUnitTouched()``bindingChanged` 分支 |
| `GetMaxTouchedTextureUnit` | `NoteUnitTouched()` 的高水位分支 |
钩子挂在**计数器**上而不是四十个写入点上:后端写前端的全部路径(`SamplerObject` 的 8 个 setter、`ITextureObject` 的 6 个、纹理单元绑定路径)都经由这三个计数器,`BufferObject`/`ProgramObject`/`VertexArrayObject` 的后端写入不移动任何推送字段(它们是句柄类读点)。
## 8. P1 验收(合入后在 `~/w7/pipe` 实测)
| 门 | 结果 |
|---|---|
| pull 构建符号与 `.text` | 0 增 / 0 删 / 0 改尺寸 / 0 重命名,`.text` 字节不变 |
| `MG_Backend` 里的 `pGLContext` | 0(唯一保留处是开关头文件的 pull 分支) |
| 单元(pull / push / verify | 1485 全绿 × 3 |
| `integration-gpu`pull | 878 全绿 |
| `integration-verify` | 818 全绿,零 `Fatal{` |
| 79 例 retrace`MOBILEGL_PIPE_VERIFY=1` | 79/79 通过,79/79 带 armed 摘要,零 `Fatal{` |
| 两个阴性对照 | 4 条专用条目全绿(篡改字段变红、抽掉一个填充戳记在那条 verb 上变红) |
| 测试名 | 0 删除,+29 |
**verify 构建的代价**`integration-verify` 818 条在 4 路并行下约与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。
+1 -1
View File
@@ -1,6 +1,6 @@
# MGPipeMobileGL 前后端拆分
> 状态:**P0 已落地**`feat/disaggregated@458ccde1`,基线 `dev@81b17c0b`)。下一步 P0.5 → P1 → P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`
> 状态:**P0、P0.5、P1 已落地**`feat/disaggregated`,基线 `dev@50fb1343`)。下一步 P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`
## 是什么
+2 -2
View File
@@ -13,8 +13,8 @@
| 阶段 | 天 | 落地什么 | 验收门 | 依赖 |
|---|---|---|---|---|
| **P0** 卫生、度量、门、骨架 | 9–11 | ✅ 边界计数器(字节 / 动态 accessor / 六个 memo 门 / 上传形状);`PipeCalls.def` 完整目录 + payload POD + 七个生成器 + CI `pipe-gates``gen_pipe_dirty_surface.py``check_doc_citations.py`;八个 `MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架(`SCM_RIGHTS` 第一优先、双 tail 双三元组的 `RingControl`、双向 doorbell、校验型 `Framing``ShmSegment``InProcessTransport`+ `protocol.fbs` + `flatc-check` + `MG_Test/Wire` 五个套件;三个严格 no-op 收益(`GetInteger64i_v`/`GetProgramiv` 退役、`RenderbufferObject::GetLifetimeId()`、D21 XFB 计数槽重键);compute 限制进 `DynamicBackendParameters`spike A、spike Bretrace 通道 `--env` 透传 | ✅ 单元/集成/40 trace 逐名不变;wire 层测试(fd 传递、doorbell、ring、封帧、inproc)绿;两台设备的字节/调用基线在案;spike A/B 出结论;citation lint 绿 | — |
| **P0.5** 值头与制品头抽取 | 69 | `MG_Pipe/MGPipeValueTypes.h``RenderStateParameters``SamplerParameters``PixelStoreParameters``VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言 | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
| **P1** `PipeInputs` 替换与 verify harness | 1013 | `MG_Backend/MGPipe/PipeInputs.h`Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
| **P0.5** 值头与制品头抽取 | 69 | ✅(`5d99ee43``MG_Pipe/MGPipeValueTypes.h``RenderStateParameters``SamplerParameters``PixelStoreParameters``VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`8 个 includer 零改动——类内 `using` 别名保住每一种既有拼写);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言`scripts/check_include_closure.py`text + clang 两模式、自带阴性对照、`--require-all` 棘轮;`scripts/symbol_report.py` 做逐符号归因)。实测落地:测试名零删除、`MG_Backend` 零 diff、`.text` 字节不变、符号 0 增 / 0 删 / 42 重命名;`DynamicBackendParameters` 未搬(含 `SizeT``TextureTarget` 成员,搬动不是纯移动),`MGPipeTypes.h` 仍 include `BackendObject.h`,闭包门 A 因此断言 `MGPipeValueTypes.h` | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** |
| **P1** `PipeInputs` 替换与 verify harness | 1013 | `MG_Backend/MGPipe/PipeInputs.h`63 字段;Espryt 32 / Magma 56 访问器);**实测 277 处箭头 + 58 行非箭头**Espryt 113+9、Magma 164+49)逐条转换;逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 |
| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | 1826 | `MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门;`MGPipeRenderStateSpans` + G7 setter 一致性测试;`CsoCache`64 项,键 = pipeline 子集);`create/bind_render_state` + `set_dynamic_state`Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state``set_patch_state``set_vertex_attrib_defaults``set_residual_value_state` + `ResidualValueBlock` 绊线;**第一片 Track H**Espryt 0b`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针);`MOBILEGL_PIPE_LEGACY_MEMOS`;补 `FramebufferSrgb`/`DepthClamp` 存储 | 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照 | P1 |
| **P3a** handle wave 1Espryt):buffer、VAO | 1823 | 7 个 `BufferBackendOps``resource_*``buffer_subdata_resident`(可 null)、`resource_flush_range``resource_readback``map_persistent`(不碰实现);pool 与延迟释放原样搬;vertex elements 三件(两个视图都带);`set_vertex_buffers``baseInstance` 显式字段);`set_index_buffer`Adreno SIGSEGV workaround 保留 | 全套门;buffer/VAO 族场景(`LargeArenaAdoption``StorageBufferRegrow` 发布 `map-persistent-roundtrips``VertexAttribBinding``MultiDraw``PrimitiveRestart`…);Create/rd12/26.3/sodium traceMC 26.3 在 Adreno 上 p99 不变。**再基线检查点 1:超过 27 天必须重定基线** | P2 |
| **P4a** handle wave 2Espryt):FBO / 纹理 / sampler / program 身份与描述符 | 2634 | `set_framebuffer_state`(解析后的 `ReadSurface`、内联格式、`ContentHash``{0,1}`);sampler CSO(含 `borderColorForm`);sampler view + `set_texture_params``set_sampler_views`/`bind_sampler_states`/`set_shader_images`shader CSOSPIR-V + 归档);`set_draw/dispatch_program``set_global_constants``CompositeResolver`;纹理/renderbuffer 的 `resource_*`。emulation 在 split 下显式 Fatal 直到 P8 | 全套门;framebuffer/纹理/program 族场景;**新增"只作 attachment / image 单元 / CopyImage 端点的纹理其 `glTexParameter` 生效"场景(落地前必须红)**;两台设备 `KHR-GL46.direct_state_access.framebuffers*` 与整个 `packed_pixels` 块(~3300 例,句柄复用压力测试)。**再基线检查点 1b:超过 39 天** | P3a |
+696
View File
@@ -0,0 +1,696 @@
#!/usr/bin/env python3
# MobileGL - scripts/check_include_closure.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""The P0.5 interface-purity gate: what the extracted headers are allowed to include.
ROADMAP.md P0.5 asks for a CI assertion on the include CLOSURE of the two headers the
phase extracts, and for a negative control that goes red when an `MG_State` include is
added back (ROADMAP.md:7 - every gate must be able to fail for the reason it exists).
`nm --undefined-only` cannot do that job: 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 be able to sever. So the arbiter here is the preprocessor's own
`-H` transcript, plus a literal `#include` walk that needs no compiler at all.
probe header forbidden in its closure
---------------- --------------------------------------------- -------------------------------
value-header MG_Pipe/MGPipeValueTypes.h MG_State/ MG_Impl/ MG_Backend/ MG_Remote/
artifacts-header MG_State/.../ProgramState/ProgramArtifacts.h ShaderObject.h, ShaderTranspiler/,
Config.h, MG_Backend/, BufferState/,
ProgramState/Shader* (+ <= 2 `glslang::`)
wire-header MG_Remote/Transport/ITransport.h MobileGL/Includes.h
The forbidden sets deliberately say nothing about glslang, spirv-cross or vulkan:
MobileGL/Includes.h pulls all three unconditionally (:53,:56,:59,:80-84,:130) and both
new headers are allowed `<Includes.h>`, so a "textually glslang-free closure" assertion
would be unsatisfiable by construction. P7 measures that with `nm -D | grep glslang` on
the server binary instead - a symbol gate, not a text gate.
Two modes, and they check different things:
--mode text transitive walk of literal `#include` lines. No compiler, no
submodules, blind to `#if`. This is what the ctest runs, because the
CI `test` job's runner has no submodules checked out.
--mode clang `-H -E` on a one-line probe TU, which is the ground truth, plus a
`-fsyntax-only` pass that proves the header is self-contained.
--mode both run both and additionally fail if they disagree about the violations.
D9 (brief section B.0): a probe whose header does not exist yet prints SKIP and is
counted; `--require-all` turns every SKIP into a failure. That is what lets this package
land BEFORE the two headers do, and what stops an all-SKIP run from passing for free
once they exist - the integrator flips `--require-all` on as the ratchet.
python3 scripts/check_include_closure.py --mode text --self-test
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test
python3 scripts/check_include_closure.py --mode both --self-test --require-all
"""
import argparse
import atexit
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PREFIX = "include-closure: "
# The probe manifest. Header/Tu/Forbidden are the fixed contract of brief section B.0:
# the gate is coded against these paths before the headers they name exist.
#
# Allow entries are deliberately awkward: each one needs a Reason that is printed in
# the summary of every run, so that a whitelist entry has to be argued for in the pull
# request instead of appearing quietly (test.yml:831-835 house rule).
PROBES = [
{
"Name": "value-header",
"Header": "MobileGL/MG_Pipe/MGPipeValueTypes.h",
"Tu": "#include <MG_Pipe/MGPipeValueTypes.h>\n",
"Forbidden": [
"MobileGL/MG_State/",
"MobileGL/MG_Impl/",
"MobileGL/MG_Backend/",
"MobileGL/MG_Remote/",
],
"Allow": [],
"TextLimits": {},
"Why": "MG_Pipe is below MG_State (ARCHITECTURE.md:550-561): the value types P1 hands "
"the backend must not drag the frontend state tree back in.",
},
{
"Name": "artifacts-header",
"Header": "MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h",
"Tu": "#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>\n",
"Forbidden": [
"MobileGL/MG_State/GLState/ProgramState/ShaderObject.h",
"MobileGL/MG_Util/ShaderTranspiler/",
"MobileGL/Config.h",
"MobileGL/MG_Backend/",
"MobileGL/MG_State/GLState/BufferState/",
# prefix, on purpose: ShaderStage.h, ShaderCompileTask.h,
# ShaderCompileAdoptionMap.h, ShaderPreprocessCache.h, ShaderSourceKey.h
"MobileGL/MG_State/GLState/ProgramState/Shader",
],
"Allow": [],
# D5: LinkArtifacts keeps exactly two glslang-typed members and no more.
"TextLimits": {"glslang::": 2},
"Why": "P7 ships the reflection artifacts over the wire; the archive header must not "
"depend on the compiler front end that produced them.",
},
{
"Name": "mutation-header",
"Header": "MobileGL/MG_Pipe/PipeMutation.h",
"Tu": "#include <MG_Pipe/PipeMutation.h>\n",
"Forbidden": [
"MobileGL/MG_Impl/",
"MobileGL/MG_State/GLState/Core.h",
"MobileGL/MG_Remote/",
],
"Allow": [],
"TextLimits": {},
"Why": "P1 finding F2 put MGP_NOTE_MUTATION in frontend mutators, so MG_State now "
"includes this header (TextureState.h, SamplerObject.h). It may only DECLARE "
"the notice: reaching MG_Impl would pull the fill implementation into the "
"state machine that calls it, and reaching GLState/Core.h would close the "
"cycle back onto the context. The MGPipeTypes -> BackendObject -> TextureEnum "
"reach it inherits from MGPipe.h is D1's known exception, which is why gate A "
"asserts MGPipeValueTypes.h rather than MGPipeTypes.h.",
},
{
"Name": "wire-header",
"Header": "MobileGL/MG_Remote/Transport/ITransport.h",
"Tu": "#include <MG_Remote/Transport/ITransport.h>\n",
"Forbidden": ["MobileGL/Includes.h"],
"Allow": [],
"TextLimits": {},
"Why": "WireLog.h:9-24 states the rule: nothing about a byte pipe needs the GL "
"frontend's umbrella header. Green today - it is the gate's own canary.",
},
]
# Deleting a probe must be red, not quietly green.
REQUIRED_PROBE_NAMES = ("value-header", "artifacts-header")
# Measured to reproduce the closure the real build sees (666 lines for the negative
# control TU), without a CMake configure.
DEFAULT_CLANG_FLAGS = [
"-std=gnu++23",
"-Iinclude",
"-IMobileGL",
"-IMobileGL/MG_Pipe",
"-I3rdparty/xxHash",
"-I3rdparty/Vulkan-Headers/include",
]
# The only submodule headers Includes.h reaches; glslang and spirv_cross are vendored.
CLANG_MODE_PREREQS = [
"include/ska/flat_hash_map.hpp",
"3rdparty/xxHash/xxhash.h",
"3rdparty/Vulkan-Headers/include/vulkan/vulkan.h",
]
COMPILER_CANDIDATES = ["clang++-20", "clang++", "c++", "g++"]
# Text-mode angle-bracket search path, in the order MOBILEGL_INCLUDE_DIR lists it
# (CMakeLists.txt:529-541); MG_Pipe is there twice on purpose (CMakeLists.txt:531,535).
SEARCH_DIRS = [
"include",
"MobileGL",
"MobileGL/MG_Pipe",
"3rdparty/xxHash",
"3rdparty/Vulkan-Headers/include",
]
# `-H` writes to stderr, one line per file actually opened: dots for depth, then the path
# as spelled on the search path. g++ appends a "Multiple include guards may be useful
# for:" paragraph of bare paths, which must not be mistaken for depth-0 includes.
H_LINE_RE = re.compile(r"^(\.+) (.*)$")
INCLUDE_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*([<"])([^>"]+)[>"]')
def say(message):
print(PREFIX + message)
def error(message):
print("::error::" + message)
def repo_relative(path, cwd):
"""normpath the -H spelling, then express it repo-relative with forward slashes.
Today's transcripts contain e.g. TextureState/../SamplerState/SamplerObject.h, which
only matches a forbidden prefix after normalisation. Anything outside the repo (the
toolchain's own headers, /usr/include) is not our business and returns None.
"""
absolute = os.path.normpath(os.path.join(cwd, path))
try:
relative = os.path.relpath(absolute, REPO_ROOT)
except ValueError: # different drive on Windows
return None
if relative == os.pardir or relative.startswith(os.pardir + os.sep):
return None
return relative.replace(os.sep, "/")
def parse_h_output(text, cwd):
"""-H transcript -> [(depth, repo_relative_or_None, raw_spelling)] in file order."""
entries = []
for line in text.splitlines():
match = H_LINE_RE.match(line)
if not match:
continue
entries.append((len(match.group(1)), repo_relative(match.group(2), cwd), match.group(2)))
return entries
def resolve_include(spec, quoted, from_dir):
candidates = []
if quoted:
candidates.append(os.path.join(from_dir, spec))
for directory in SEARCH_DIRS:
candidates.append(os.path.join(REPO_ROOT, directory, spec))
for candidate in candidates:
candidate = os.path.normpath(candidate)
if os.path.isfile(candidate):
return candidate
return None
def text_closure(tu_text, tu_dir):
"""Transitive walk of literal #include lines.
There are no macro-driven includes under MobileGL/ (3377 include lines, all literal),
so a literal walk is exact apart from `#if`, which this mode is blind to by design -
the compiler mode is the arbiter and --mode both cross-checks the two.
A file is walked once, which is what an include guard does to the -H transcript too.
"""
entries = []
seen = set()
def walk(text, from_dir, depth):
for line in text.splitlines():
match = INCLUDE_RE.match(line)
if not match:
continue
path = resolve_include(match.group(2), match.group(1) == '"', from_dir)
if path is None: # unresolved (libstdc++, EGL/, GL/, glslang/, ...) = leaf
continue
if path in seen:
continue
seen.add(path)
entries.append((depth, repo_relative(path, REPO_ROOT), path))
try:
with open(path, "r", encoding="utf-8", errors="replace") as handle:
nested = handle.read()
except OSError:
continue
walk(nested, os.path.dirname(path), depth + 1)
walk(tu_text, tu_dir, 1)
return entries
def find_violations(entries, forbidden, allow):
violations = []
for index, (depth, relative, _raw) in enumerate(entries):
if relative is None:
continue
hit = next((f for f in forbidden if relative.startswith(f)), None)
if hit is None:
continue
if any(relative.startswith(a["Path"]) for a in allow):
continue
violations.append({"index": index, "depth": depth, "path": relative, "rule": hit})
return violations
def chain_for(entries, index):
"""Walk back to depth-1, depth-2, ... 1: the include chain that pulled the violation in."""
chain = []
wanted = entries[index][0]
cursor = index
while cursor >= 0 and wanted >= 1:
depth, relative, raw = entries[cursor]
if depth == wanted:
chain.append((depth, relative or raw))
wanted -= 1
cursor -= 1
return list(reversed(chain))
def count_text_limits(header_path, limits):
"""Token budgets on the header's own text (D5 pins `glslang::` at 2)."""
problems = []
if not limits:
return problems
with open(header_path, "r", encoding="utf-8", errors="replace") as handle:
text = handle.read()
for token, budget in sorted(limits.items()):
found = text.count(token)
if found > budget:
problems.append("{} occurs {} times, budget is {}".format(token, found, budget))
return problems
def pick_compiler(explicit):
if explicit:
found = shutil.which(explicit) or (explicit if os.path.isfile(explicit) else None)
if not found:
error("compiler not found: " + explicit)
sys.exit(1)
return found
env_cxx = os.environ.get("CXX")
if env_cxx:
found = shutil.which(env_cxx) or (env_cxx if os.path.isfile(env_cxx) else None)
if found:
return found
for candidate in COMPILER_CANDIDATES:
found = shutil.which(candidate)
if found:
return found
error("no C++ preprocessor found (tried $CXX, " + ", ".join(COMPILER_CANDIDATES) + ")")
sys.exit(1)
def flags_from_compile_commands(path):
"""Any entry whose `file` is under MobileGL/ carries the flags the real build uses."""
with open(path, "r", encoding="utf-8") as handle:
database = json.load(handle)
keep_prefixes = ("-D", "-I", "-isystem", "-std")
for entry in database:
source = entry.get("file", "")
if "MobileGL/" not in source.replace(os.sep, "/"):
continue
tokens = entry.get("arguments")
if tokens is None:
tokens = shlex.split(entry.get("command", ""))
flags = []
skip_next = False
keep_next = False
for token in tokens[1:]:
if keep_next:
keep_next = False
flags.append(token)
continue
if skip_next:
skip_next = False
continue
if token in ("-o", "-c", "-MD", "-MF", "-MT", "-MMD", "-MQ"):
skip_next = token in ("-o", "-MF", "-MT", "-MQ")
continue
if token == "-isystem":
# two-token form: keep the flag AND its directory
flags.append(token)
keep_next = True
continue
if token.startswith(keep_prefixes):
flags.append(token)
continue
# the input file and everything else is dropped
return flags, entry.get("directory", REPO_ROOT)
error("no MobileGL/ entry in " + path)
sys.exit(1)
def check_clang_prereqs():
missing = [p for p in CLANG_MODE_PREREQS if not os.path.isfile(os.path.join(REPO_ROOT, p))]
if missing:
error("clang mode needs " + ", ".join(missing) + " - run from a full checkout: "
"git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers")
sys.exit(1)
def run_preprocessor(compiler, flags, tu_path, cwd):
command = [compiler] + list(flags) + ["-H", "-E", "-o", os.devnull, tu_path]
completed = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
return completed
def run_syntax_only(compiler, flags, tu_path, cwd):
command = [compiler] + list(flags) + ["-fsyntax-only", tu_path]
return subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
def closure_for_tu(mode, tu_text, context):
"""Return (entries, note). `context` carries compiler/flags/cwd/tmpdir."""
if mode == "text":
return text_closure(tu_text, REPO_ROOT), None
tu_path = os.path.join(context["tmpdir"], "probe_%d.cpp" % context["counter"][0])
context["counter"][0] += 1
with open(tu_path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(tu_text)
completed = run_preprocessor(context["compiler"], context["flags"], tu_path, context["cwd"])
if completed.returncode != 0:
return parse_h_output(completed.stderr, context["cwd"]), completed.stderr.strip()
return parse_h_output(completed.stderr, context["cwd"]), None
def print_chain(entries, violation):
for depth, path in chain_for(entries, violation["index"]):
say(" " + (" " * (depth - 1)) + path)
def run_probe(probe, mode, context, results):
header_abs = os.path.join(REPO_ROOT, probe["Header"])
entries, note = closure_for_tu(mode, probe["Tu"], context)
if note:
error("{} preprocessing failed:\n{}".format(probe["Name"], note))
results.append({"probe": probe["Name"], "mode": mode, "status": "ERROR",
"headers": len(entries), "forbidden": 0})
return "ERROR", set()
violations = find_violations(entries, probe["Forbidden"], probe["Allow"])
text_problems = count_text_limits(header_abs, probe["TextLimits"])
status = "OK" if not violations and not text_problems else "FORBIDDEN"
say("{} {} {} headers in closure, {} forbidden (mode={}, cxx={})".format(
probe["Name"], status, len(entries), len(violations), mode,
context["compiler_label"] if mode == "clang" else "-"))
for allowed in probe["Allow"]:
say(" ALLOW {} - {}".format(allowed["Path"], allowed["Reason"]))
for violation in violations:
say(" forbidden: {} (rule {}) reached by:".format(violation["path"], violation["rule"]))
print_chain(entries, violation)
for problem in text_problems:
say(" text limit: " + problem)
results.append({"probe": probe["Name"], "mode": mode, "status": status,
"headers": len(entries), "forbidden": len(violations),
"violations": [v["path"] for v in violations],
"text_problems": text_problems})
return status, {v["path"] for v in violations}
def run_self_contained_check(probe, context):
"""Second assertion in clang mode: the header compiles on its own."""
tu_path = os.path.join(context["tmpdir"], "syntax_%s.cpp" % probe["Name"].replace("-", "_"))
with open(tu_path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(probe["Tu"])
completed = run_syntax_only(context["compiler"], context["flags"], tu_path, context["cwd"])
if completed.returncode == 0:
say("{} SELF-CONTAINED OK (-fsyntax-only, cxx={})".format(
probe["Name"], context["compiler_label"]))
return True
say("{} SELF-CONTAINED FAILED (-fsyntax-only, cxx={})".format(
probe["Name"], context["compiler_label"]))
for line in completed.stderr.strip().splitlines()[:20]:
say(" " + line)
return False
# --------------------------------------------------------------------------------------
# self-test: the always-on proof that the gate is capable of failing
# --------------------------------------------------------------------------------------
PARSER_CANNED_GXX = """. MobileGL/MG_Pipe/MGPipeHandles.h
.. MobileGL/Includes.h
... MobileGL/MG_State/GLState/TextureState/../SamplerState/SamplerObject.h
Multiple include guards may be useful for:
/usr/include/bits/byteswap.h
/usr/include/bits/confname.h
"""
def self_test_parser():
"""Check 4: the trailer paragraph is discarded and a `../` spelling normalises."""
problems = []
entries = parse_h_output(PARSER_CANNED_GXX, REPO_ROOT)
if len(entries) != 3:
problems.append("g++ -H trailer not discarded: parsed {} lines, expected 3".format(len(entries)))
depths = [e[0] for e in entries]
if depths != [1, 2, 3]:
problems.append("depths misparsed: {}".format(depths))
normalised = entries[-1][1] if entries else None
if normalised != "MobileGL/MG_State/GLState/SamplerState/SamplerObject.h":
problems.append("`../` spelling did not normalise: {}".format(normalised))
elif not find_violations(entries, PROBES[0]["Forbidden"], []):
problems.append("normalised path did not match the value-header forbidden list")
if problems:
for problem in problems:
say(" self-test parser: " + problem)
return False
say(" self-test parser: trailer discarded, `../` normalised and matched")
return True
def negative_controls():
"""The three canned TUs of B.3, gated on which headers exist yet."""
controls = [
{
"Name": "control-1 (independent of P0.5)",
"Tu": "#include <MG_Pipe/MGPipeHandles.h>\n"
"#include <MG_State/GLState/RenderState/RenderState.h>\n",
"Forbidden": PROBES[0]["Forbidden"],
"ExpectDepth1": "MobileGL/MG_State/GLState/RenderState/RenderState.h",
},
]
value_header = os.path.join(REPO_ROOT, PROBES[0]["Header"])
if os.path.isfile(value_header):
controls.append({
"Name": "control-2 (MG_State include added back to the value header's TU)",
"Tu": PROBES[0]["Tu"] + "#include <MG_State/GLState/RenderState/RenderState.h>\n",
"Forbidden": PROBES[0]["Forbidden"],
"ExpectDepth1": "MobileGL/MG_State/GLState/RenderState/RenderState.h",
})
artifacts_header = os.path.join(REPO_ROOT, PROBES[1]["Header"])
if os.path.isfile(artifacts_header):
controls.append({
"Name": "control-3 (ShaderObject.h added back to the artifacts header's TU)",
"Tu": PROBES[1]["Tu"] + "#include <MG_State/GLState/ProgramState/ShaderObject.h>\n",
"Forbidden": PROBES[1]["Forbidden"],
"ExpectDepth1": "MobileGL/MG_State/GLState/ProgramState/ShaderObject.h",
})
return controls
def self_test_text_limit(context):
"""Check 3b: a third `glslang::` token in a copy of the artifacts header must trip."""
header = os.path.join(REPO_ROOT, PROBES[1]["Header"])
if not os.path.isfile(header):
return None
copy = os.path.join(context["tmpdir"], "ProgramArtifacts_with_a_third_glslang.h")
with open(header, "r", encoding="utf-8", errors="replace") as handle:
text = handle.read()
with open(copy, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text + "\n// synthesized third token: glslang::TIntermediate\n")
problems = count_text_limits(copy, PROBES[1]["TextLimits"])
if problems:
say(" self-test text limit: tripped as expected ({})".format(problems[0]))
return True
say(" self-test text limit: a third `glslang::` token did NOT trip the budget")
return False
def self_test(modes, context):
say("self-test: negative controls (must trip) in mode(s) " + ", ".join(modes))
ok = self_test_parser()
trips = 0
for control in negative_controls():
for mode in modes:
entries, note = closure_for_tu(mode, control["Tu"], context)
if note:
say(" self-test {} [{}]: preprocessing failed".format(control["Name"], mode))
ok = False
continue
violations = find_violations(entries, control["Forbidden"], [])
at_depth_1 = [v for v in violations
if v["depth"] == 1 and v["path"] == control["ExpectDepth1"]]
if violations and at_depth_1:
trips += 1
say(" self-test {} [{}]: tripped, {} violation(s), {} at depth 1".format(
control["Name"], mode, len(violations), control["ExpectDepth1"]))
else:
ok = False
say(" self-test {} [{}]: DID NOT TRIP ({} violation(s), depth-1 hit: {})".format(
control["Name"], mode, len(violations), bool(at_depth_1)))
limit_result = self_test_text_limit(context)
if limit_result is False:
ok = False
if trips == 0:
error("negative control did not trip: the closure gate is not checking anything")
return False
say("self-test: {} negative-control trip(s), parser checks {}".format(
trips, "OK" if ok else "FAILED"))
return ok
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--mode", choices=["text", "clang", "both"], default="text",
help="literal #include walk, the preprocessor's -H transcript, or both")
parser.add_argument("--compiler", default=None,
help="preprocessor to use; default $CXX, then " + ", ".join(COMPILER_CANDIDATES))
parser.add_argument("--compile-commands", default=None,
help="take -D/-I/-isystem/-std from a compile_commands.json entry under MobileGL/")
parser.add_argument("--probe", action="append", default=None,
help="restrict to this probe (repeatable)")
parser.add_argument("--self-test", action="store_true",
help="run the negative controls and the parser checks (always on in CI)")
parser.add_argument("--require-all", action="store_true",
help="a SKIP (header not present yet) is a failure")
parser.add_argument("--json", default=None, help="write the machine-readable result here")
args = parser.parse_args()
names = [p["Name"] for p in PROBES]
for required in REQUIRED_PROBE_NAMES:
if required not in names:
error("probe `{}` is missing from the manifest in {}".format(
required, os.path.basename(__file__)))
return 1
modes = ["text", "clang"] if args.mode == "both" else [args.mode]
if "clang" in modes:
check_clang_prereqs()
tmpdir = tempfile.mkdtemp(prefix="mgl-include-closure-")
atexit.register(shutil.rmtree, tmpdir, True)
context = {
"tmpdir": tmpdir,
"counter": [0],
"cwd": REPO_ROOT,
"flags": list(DEFAULT_CLANG_FLAGS),
"compiler": None,
"compiler_label": "-",
}
if "clang" in modes:
compiler = pick_compiler(args.compiler)
context["compiler"] = compiler
context["compiler_label"] = compiler
if args.compile_commands:
flags, directory = flags_from_compile_commands(args.compile_commands)
context["flags"] = flags
context["cwd"] = directory
say("flags from {} ({} tokens), cwd={}".format(
args.compile_commands, len(flags), directory))
say("compiler: " + compiler)
selected = [p for p in PROBES if args.probe is None or p["Name"] in args.probe]
if args.probe:
unknown = sorted(set(args.probe) - set(names))
if unknown:
error("unknown probe(s): " + ", ".join(unknown))
return 1
results = []
problems = 0
skipped = 0
if args.require_all:
# The ratchet is about the two P0.5 headers existing, whether or not --probe
# narrowed this run to something else: a required header that is missing is a
# failure even when its probe was not selected.
selected_names = {probe["Name"] for probe in selected}
for probe in PROBES:
if probe["Name"] in REQUIRED_PROBE_NAMES and probe["Name"] not in selected_names and not os.path.isfile(os.path.join(REPO_ROOT, probe["Header"])):
problems += 1
say("{} is required under --require-all but its header does not exist: {}".format(
probe["Name"], probe["Header"]))
for probe in selected:
# D9: a header that does not exist yet is one SKIP line, counted once, and a
# failure only under --require-all (the ratchet the integrator flips).
if not os.path.isfile(os.path.join(REPO_ROOT, probe["Header"])):
say("{} SKIP header not present yet: {}".format(probe["Name"], probe["Header"]))
results.append({"probe": probe["Name"], "mode": "+".join(modes), "status": "SKIP",
"headers": 0, "forbidden": 0})
skipped += 1
if args.require_all:
problems += 1
say("{} SKIP is a failure under --require-all: {} does not exist".format(
probe["Name"], probe["Header"]))
continue
per_mode_status = {}
per_mode_violations = {}
for mode in modes:
status, violations = run_probe(probe, mode, context, results)
per_mode_status[mode] = status
per_mode_violations[mode] = violations
if any(s not in ("OK",) for s in per_mode_status.values()):
problems += 1
if len(modes) > 1:
sets = list(per_mode_violations.values())
if sets[0] != sets[1]:
problems += 1
say("{} MODE DISAGREEMENT - text and clang do not see the same violations".format(
probe["Name"]))
say(" text : " + (", ".join(sorted(per_mode_violations["text"])) or "(none)"))
say(" clang: " + (", ".join(sorted(per_mode_violations["clang"])) or "(none)"))
if "clang" in modes:
if not run_self_contained_check(probe, context):
problems += 1
self_test_ok = True
if args.self_test:
self_test_ok = self_test(modes, context)
if not self_test_ok:
problems += 1
say("{} probes, {} skipped, {} problem(s)".format(len(selected), skipped, problems))
if args.json:
with open(args.json, "w", encoding="utf-8", newline="\n") as handle:
json.dump({"modes": modes, "results": results, "skipped": skipped,
"problems": problems, "self_test": self_test_ok,
"require_all": args.require_all}, handle, indent=2)
handle.write("\n")
if problems:
error("include-closure gate failed: {} problem(s); see the chains above".format(problems))
return 1
return 0
if __name__ == "__main__":
sys.setrecursionlimit(10000)
sys.exit(main())
+547 -30
View File
@@ -8,11 +8,14 @@
# End of Source File Header
"""The seven MGPipe generators, G1..G7 (plan B section 4.1).
Reads the three hand-maintained sources of truth
Reads the four hand-maintained sources of truth
MobileGL/MG_Pipe/PipeCalls.def the call catalogue
MobileGL/MG_Pipe/PipeFields.def per-payload field lists for the verify comparator
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory
MobileGL/MG_Pipe/Coverage.def accessor -> call mapping for the read inventory,
and the sticky-field list
MobileGL/MG_Pipe/FillPoints.def verb -> class and class -> may-read field tables
(G5b), checked against MG_Backend::GLFunctionsTable
plus the vendored copy of the backend read inventory
@@ -24,9 +27,13 @@ from the catalogue (they all consume the same .def).
python3 scripts/gen_pipe.py # write the generated files, print the summary
python3 scripts/gen_pipe.py --check # fail if regenerating would change anything
python3 scripts/gen_pipe.py --self-test # the negative controls: each gate below must trip
Both modes refuse a catalogue whose call payload has no field list in PipeFields.def: a
payload the G4 comparator cannot see is a payload MOBILEGL_PIPE_VERIFY is blind to.
payload the G4 comparator cannot see is a payload MOBILEGL_PIPE_VERIFY is blind to. Both
also refuse a field list that does not name every data member of its struct (or names one
that is not a member), and a backend accessor read through MGB_CTX-> / pGLContext-> that
has no Coverage.def row (P1 brief D8, D12).
"""
import argparse
@@ -38,6 +45,7 @@ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
INVENTORY = os.path.join(REPO_ROOT, "scripts", "data", "backend_read_inventory.md")
FUNCTION_TABLE_HEADER = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend", "BackendObject.h")
GENERATED_BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
// Copyright (c) 2025-2026 MobileGL-Dev
@@ -160,15 +168,23 @@ def parse_calls():
return calls
# The member types the G4 comparator falls back to memcmp for (see gen_verify): the
# MG_State / MG_Backend value structs and MGHostSpan. They are not call payloads and get
# field lists of their own in P0.5. Nothing else may be missing from PipeFields.def.
MEMCMP_FALLBACK_TYPES = {
"RenderStateParameters",
"PixelStoreParameters",
"DynamicBackendParameters",
"MGHostSpan",
}
# Types the G4 comparator may fall back to memcmp for: NONE since P1. The value structs and
# MGHostSpan have field lists of their own, and the fallback branch of MGPipeFieldEqual is a
# static_assert, so a future struct without a field list is a compile error rather than a
# padding false positive. Kept as a (deliberately empty) set so the check below keeps its
# shape.
MEMCMP_FALLBACK_TYPES = set()
# Where the structs named in PipeFields.def are declared: the payload header, the value
# header, the host-span header and - for DynamicBackendParameters, the caps block - the
# backend object header.
FIELD_LIST_STRUCT_HEADERS = [
os.path.join(PIPE_DIR, "MGPipeTypes.h"),
os.path.join(PIPE_DIR, "MGPipeValueTypes.h"),
os.path.join(PIPE_DIR, "MGPipeHostSpan.h"),
FUNCTION_TABLE_HEADER,
]
BACKEND_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Backend")
def parse_verify_payloads():
@@ -183,6 +199,253 @@ def parse_verify_payloads():
return payloads
FIELD_LIST_RE = re.compile(r"#define MGP_FIELDS_(\w+)\(F\)")
def parse_field_lists(text=None):
"""PipeFields.def -> {payload: [field, ...]} for every MGP_FIELDS_<Payload>(F) macro; the
macro ends at the first line without a continuation backslash."""
if text is None:
text = read(os.path.join(PIPE_DIR, "PipeFields.def"))
lists = {}
lines = text.splitlines()
i = 0
while i < len(lines):
match = FIELD_LIST_RE.match(lines[i])
if not match:
i += 1
continue
name = match.group(1)
body = []
while i < len(lines):
body.append(lines[i])
if not lines[i].rstrip().endswith("\\"):
break
i += 1
i += 1
fields = re.findall(r"\bF\((\w+)\)", "\n".join(body))
if name in lists:
sys.exit("PipeFields.def: MGP_FIELDS_%s is defined twice" % name)
lists[name] = fields
return lists
def mask_comments_and_strings(text):
"""Replace comment and string-literal bodies with spaces, keeping every offset and
newline, so the regexes below cannot match inside a comment or a literal
(gen_pipe_dirty_surface.py's shape)."""
out = list(text)
i = 0
n = len(text)
while i < n:
c = text[i]
if c == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] != "\n":
out[i] = " "
i += 1
elif c == "/" and i + 1 < n and text[i + 1] == "*":
out[i] = out[i + 1] = " "
i += 2
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
if text[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = " "
if i + 1 < n:
out[i + 1] = " "
i += 2
elif c in "\"'":
quote = c
i += 1
while i < n and text[i] != quote:
if text[i] == "\\":
out[i] = " "
i += 1
if i < n and text[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = " "
i += 1
else:
i += 1
return "".join(out)
PADDING_MEMBER_RE = re.compile(r"^Pad\d*$")
NESTED_TYPE_RE = re.compile(r"^\s*(?:struct|class|union|enum)\b")
FUNCTION_HEAD_RE = re.compile(r"\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:=\s*(?:default|delete|0)\s*)?$")
NON_MEMBER_RE = re.compile(r"^\s*(?:static|using|typedef|friend|template|explicit|virtual|operator)\b")
def find_struct_body(masked, name):
"""The text between the braces of `struct <name> {` (an optional base list allowed), or
None. Comments and strings must already be masked."""
match = re.search(r"\bstruct\s+%s\s*(?::[^{;]*)?\{" % re.escape(name), masked)
if not match:
return None
i = match.end()
depth = 1
start = i
while i < len(masked) and depth:
if masked[i] == "{":
depth += 1
elif masked[i] == "}":
depth -= 1
i += 1
return masked[start:i - 1]
def matching_brace(text, open_index):
depth = 0
j = open_index
while j < len(text):
if text[j] == "{":
depth += 1
elif text[j] == "}":
depth -= 1
if depth == 0:
return j
j += 1
return len(text) - 1
def strip_balanced(text, open_char, close_char):
out = []
depth = 0
for c in text:
if c == open_char:
depth += 1
elif c == close_char:
depth -= 1
elif depth == 0:
out.append(c)
return "".join(out)
def member_names(statement):
"""The data-member names declared by one struct-body statement, or [] for anything
that is not a data member (a function, a static, a using, an access label...)."""
statement = re.sub(r"^\s*(?:public|private|protected)\s*:", "", statement).strip()
if not statement or NON_MEMBER_RE.match(statement):
return []
# The declarator part is what precedes the default initializer.
left = re.split(r"=|\{\.\.\.\}", statement, maxsplit=1)[0]
if "(" in left:
return [] # a function declaration
left = strip_balanced(left, "<", ">")
left = strip_balanced(left, "[", "]")
names = []
for k, chunk in enumerate(left.split(",")):
tokens = re.findall(r"[A-Za-z_]\w*", chunk)
if k == 0 and len(tokens) < 2:
return [] # no type: not a declaration
if not tokens:
return []
names.append(tokens[-1])
return [n for n in names if not PADDING_MEMBER_RE.match(n)]
def struct_data_members(body):
"""Direct data members of a struct body, in declaration order: statics, member
functions, nested types and Pad-named members excluded."""
members = []
statement = []
i = 0
n = len(body)
while i < n:
c = body[i]
if c == "{":
head = "".join(statement)
close = matching_brace(body, i)
if NESTED_TYPE_RE.match(head.strip()) or FUNCTION_HEAD_RE.search(head.rstrip()):
# A nested type or a member-function body: not a data member. Swallow the
# nested type's trailing semicolon too.
statement = []
i = close + 1
if NESTED_TYPE_RE.match(head.strip()):
while i < n and body[i] in " \t\n":
i += 1
if i < n and body[i] == ";":
i += 1
continue
statement.append("{...}") # a brace default initializer
i = close + 1
continue
if c == ";":
members.extend(member_names("".join(statement)))
statement = []
i += 1
continue
statement.append(c)
i += 1
return members
def check_field_lists_cover_struct_members(field_lists, payloads, header_texts=None):
"""Every payload in MGP_VERIFY_PAYLOAD_LIST: its MGP_FIELDS_ list must name every direct
data member of `struct <Payload> {` (Pad-named members are padding and excluded) and
nothing that is not a member. A member without an F(...) is a field MOBILEGL_PIPE_VERIFY
is blind to; an F(...) that is not a member is a list that stopped describing its
struct. Runs in both modes, --check included, so it is part of pipe-gates."""
if header_texts is None:
header_texts = [read(path) for path in FIELD_LIST_STRUCT_HEADERS]
masked = [mask_comments_and_strings(t) for t in header_texts]
problems = []
for payload in payloads:
body = None
for m in masked:
body = find_struct_body(m, payload)
if body is not None:
break
if body is None:
problems.append("%s: struct not found in %s" % (payload, ", ".join(os.path.basename(p) for p in FIELD_LIST_STRUCT_HEADERS)))
continue
members = struct_data_members(body)
listed = field_lists.get(payload, [])
missing = [m for m in members if m not in listed]
extra = [f for f in listed if f not in members]
if missing:
problems.append("%s: member(s) with no F(...) in PipeFields.def: %s" % (payload, ", ".join(missing)))
if extra:
problems.append("%s: F(...) name(s) that are not members: %s" % (payload, ", ".join(extra)))
if not members:
problems.append("%s: no data members parsed" % payload)
if problems:
sys.exit("PipeFields.def does not cover its structs:\n " + "\n ".join(problems))
ACCESSOR_READ_RE = re.compile(r"\b(?:MGB_CTX|pGLContext)\s*->\s*(\w+)")
def scan_live_accessors(accessors, backend_dir=None, verbose=True):
"""Every accessor a backend reads through MGB_CTX-> or pGLContext-> (comments and
strings masked) must have a Coverage.def row - a read without a row is a PipeInputs
field that does not exist. Rows no backend reads are printed, not refused (the dead
GetBoundTransformFeedbackName row is deliberate). Returns the set of names read."""
if backend_dir is None:
backend_dir = BACKEND_DIR
known = set(name for name, _ in accessors)
read_names = {}
for root, _, files in os.walk(backend_dir):
for name in sorted(files):
if not name.endswith((".cpp", ".h")):
continue
path = os.path.join(root, name)
masked = mask_comments_and_strings(read(path))
for match in ACCESSOR_READ_RE.finditer(masked):
read_names.setdefault(match.group(1), set()).add(os.path.relpath(path, REPO_ROOT))
unknown = sorted(n for n in read_names if n not in known)
if unknown:
sys.exit("Coverage.def: accessor(s) read by a backend with no row: %s"
% ", ".join("%s (%s)" % (n, ", ".join(sorted(read_names[n]))) for n in unknown))
unread = sorted(known - set(read_names))
if verbose and unread:
print("gen_pipe: %d accessor row(s) no backend reads: %s" % (len(unread), ", ".join(unread)))
return set(read_names)
def check_call_payloads_have_field_lists(calls, payloads):
"""Every payload PipeCalls.def names must have a G4 field list, or the verify comparator
is silently blind to that call. Runs in both modes, --check included."""
@@ -208,7 +471,18 @@ def parse_coverage():
sys.exit("Coverage.def: MGP_COVERAGE_DELTA_LIST is missing")
for kind, call in re.findall(r"X\(([^,]+),\s*(\w+)\)", block.group(1)):
deltas.append((kind.strip(), call))
return accessors, deltas
sticky = []
block = re.search(r"#define MGP_COVERAGE_STICKY_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_STICKY_LIST is missing")
accessor_names = set(name for name, _ in accessors)
for name, reason in re.findall(r"X\((\w+)\s*,\s*\"([^\"]*)\"\)", block.group(1)):
if name not in accessor_names:
sys.exit("Coverage.def: sticky field %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST" % name)
if name in dict(sticky):
sys.exit("Coverage.def: sticky field %s is listed twice" % name)
sticky.append((name, reason))
return accessors, deltas, sticky
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
@@ -387,6 +661,23 @@ def gen_verify(payloads):
""")
out.append("template <class T>")
out.append("struct MGPipeHasFieldVerifier : std::false_type {};\n")
out.append("""// 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]);
""")
for payload in payloads:
out.append("inline Bool MGPipeVerify(const %s& a, const %s& b, const char** outField);"
% (payload, payload))
@@ -400,6 +691,8 @@ 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>) {
@@ -407,15 +700,23 @@ inline Bool MGPipeFieldEqual(const T& a, const T& b) {
} else if constexpr (requires(const T& x, const T& y) { x == y; }) {
return a == b;
} else {
// MEMCMP FALLBACK. Only reached by the payload members that are still MG_State /
// MG_Backend value structs (RenderStateParameters, PixelStoreParameters,
// DynamicBackendParameters) and by MGHostSpan. Those are exactly the types P0.5
// moves into MGPipeValueTypes.h, at which point they get field lists of their own
// and this branch stops being reachable from any payload.
return std::memcmp(&a, &b, sizeof(T)) == 0;
// 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) {
@@ -443,8 +744,9 @@ inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
return "\n".join(out) + "\n"
def gen_filled(accessors, calls):
def gen_filled(accessors, calls, sticky):
call_names = set(c.Name for c in calls)
sticky_map = dict(sticky)
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
"Coverage.def and PipeCalls.def")]
out.append("""// One field id per GLContext accessor the backends actually read (plan B section 6.2:
@@ -457,8 +759,8 @@ def gen_filled(accessors, calls):
// field stamps it with that serial, and reading a non-sticky field whose stamp is older is
// Fatal{UnmigratedPipeInput} (section 6.2.2).
//
// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs
// itself lands in P1.
// PipeInputs itself is MG_Backend/MGPipe/PipeInputs.h (P1); the verb enum and the
// per-class fill masks are G5b, generated/PipeFillPoints.inc.
""")
out.append("enum class MGPipeInputField : Uint16 {")
for name, _ in accessors:
@@ -474,13 +776,17 @@ def gen_filled(accessors, calls):
out.append(" \"%s\"," % name)
out.append("};")
out.append("")
out.append("// Fields whose value is valid ACROSS verbs. Every entry is false in P0 and each")
out.append("// true has to be argued for in P1 when the fillers land: a sticky field is a field")
out.append("// the poison cannot protect.")
out.append("// Fields whose value is valid ACROSS verbs: a sticky field is a field the poison")
out.append("// cannot protect, so every true is argued for in Coverage.def's")
out.append("// MGP_COVERAGE_STICKY_LIST (the seven forwarded, argument-keyed accessors).")
out.append("inline constexpr Bool kMGPipeInputFieldSticky[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
if name in sticky_map:
out.append(" true, // %s: %s" % (name, sticky_map[name]))
else:
out.append(" false, // %s" % name)
out.append("};")
out.append("inline constexpr SizeT kMGPipeInputStickyFieldCount = %d;" % len(sticky))
out.append("")
out.append("// Which call is expected to have filled a field by the time a verb reads it. Names")
out.append("// come from Coverage.def, so this table and the coverage table cannot disagree.")
@@ -501,14 +807,166 @@ def gen_filled(accessors, calls):
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);
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
}""")
return "\n".join(out) + "\n"
FUNCTION_POINTER_MEMBER_RE = re.compile(r"\(\s*\*\s*(\w+)\s*\)\s*\(")
def parse_function_table():
"""The function-pointer members of MG_Backend::GLFunctionsTable, in declaration order.
Data members (PrefersCpuXfbPrimitiveAccounting) are not verbs and are skipped; comments
are masked line by line."""
text = read(FUNCTION_TABLE_HEADER)
start = text.find("struct GLFunctionsTable {")
if start < 0:
sys.exit("%s: struct GLFunctionsTable is missing" % FUNCTION_TABLE_HEADER)
members = []
for line in text[start:].splitlines()[1:]:
if re.match(r"^\s*};", line):
break
code = line.split("//", 1)[0]
for name in FUNCTION_POINTER_MEMBER_RE.findall(code):
members.append(name)
if not members:
sys.exit("%s: GLFunctionsTable has no function-pointer members" % FUNCTION_TABLE_HEADER)
return members
def parse_fill_points(accessors, table_members=None, text=None):
"""FillPoints.def -> (verbs, classes, fields): verbs is [(verb, class)] in file order,
classes is [class], fields is {class: [field]}. Refuses a verb set that is not exactly
GLFunctionsTable's function-pointer members in declaration order, a verb in two
classes, a class with no verbs, a field that is not an accessor, a duplicate
(class, field) row, and a class row that names no class."""
if text is None:
text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
if table_members is None:
table_members = parse_function_table()
def block(macro):
match = re.search(r"#define %s\(X\)(.*?)\n\n" % macro, text, re.S)
if not match:
sys.exit("FillPoints.def: %s is missing (or not followed by a blank line)" % macro)
return match.group(1)
classes = re.findall(r"X\((\w+)\)", block("MGP_FILL_CLASS_LIST"))
if len(classes) != len(set(classes)):
sys.exit("FillPoints.def: a class is listed twice in MGP_FILL_CLASS_LIST")
verbs = re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_VERB_LIST"))
seen = set()
for verb, cls in verbs:
if verb in seen:
sys.exit("FillPoints.def: verb %s is in two classes" % verb)
seen.add(verb)
if cls not in classes:
sys.exit("FillPoints.def: verb %s names unknown class %s" % (verb, cls))
verb_names = [verb for verb, _ in verbs]
missing = [m for m in table_members if m not in seen]
if missing:
sys.exit("FillPoints.def: GLFunctionsTable member(s) without a verb row: %s" % ", ".join(missing))
extra = [v for v in verb_names if v not in table_members]
if extra:
sys.exit("FillPoints.def: verb(s) that are not GLFunctionsTable members: %s" % ", ".join(extra))
if verb_names != table_members:
sys.exit("FillPoints.def: verb rows are not in GLFunctionsTable declaration order "
"(first difference at %s)" % next(a for a, b in zip(verb_names, table_members) if a != b))
for cls in classes:
if not any(c == cls for _, c in verbs):
sys.exit("FillPoints.def: class %s has no verbs" % cls)
accessor_names = set(name for name, _ in accessors)
fields = {cls: [] for cls in classes}
for cls, field in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block("MGP_FILL_FIELD_LIST")):
if cls not in fields:
sys.exit("FillPoints.def: field row names unknown class %s" % cls)
if field not in accessor_names:
sys.exit("FillPoints.def: %s is not an accessor in Coverage.def" % field)
if field in fields[cls]:
sys.exit("FillPoints.def: duplicate row (%s, %s)" % (cls, field))
fields[cls].append(field)
return verbs, classes, fields
def gen_fill_points(accessors, sticky, verbs, classes, fields):
field_index = {name: i for i, (name, _) in enumerate(accessors)}
# Two words minimum (the P1 contract shape, headroom for the 64th field); grows on demand.
words = max(2, (len(accessors) + 63) // 64)
sticky_names = [name for name, _ in sticky]
out = [banner("PipeFillPoints.inc", "G5b: the verb enum, the verb classes and their may-read field masks.",
"FillPoints.def, Coverage.def and MG_Backend/BackendObject.h")]
out.append("""// 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.
""")
out.append("enum class MGPipeVerb : Uint8 {")
for verb, _ in verbs:
out.append(" %s," % verb)
out.append(" kVerbCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbCount = static_cast<SizeT>(MGPipeVerb::kVerbCount);")
out.append("static_assert(kMGPipeVerbCount == %d, \"the GLFunctionsTable verb set moved\");" % len(verbs))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbNames[kMGPipeVerbCount] = {")
for verb, _ in verbs:
out.append(" \"%s\"," % verb)
out.append("};")
out.append("")
out.append("enum class MGPipeVerbClass : Uint8 {")
for cls in classes:
out.append(" %s," % cls)
out.append(" kClassCount,")
out.append("};")
out.append("")
out.append("inline constexpr SizeT kMGPipeVerbClassCount = static_cast<SizeT>(MGPipeVerbClass::kClassCount);")
out.append("static_assert(kMGPipeVerbClassCount == %d, \"the verb class set moved\");" % len(classes))
out.append("")
out.append("inline constexpr const char* kMGPipeVerbClassNames[kMGPipeVerbClassCount] = {")
for cls in classes:
out.append(" \"%s\"," % cls)
out.append("};")
out.append("")
out.append("inline constexpr MGPipeVerbClass kMGPipeVerbClass[kMGPipeVerbCount] = {")
for verb, cls in verbs:
out.append(" MGPipeVerbClass::%s, // %s" % (cls, verb))
out.append("};")
out.append("")
out.append("// One bit per MGPipeInputField. The %d sticky fields are OR'ed into every class." % len(sticky))
out.append("struct MGPipeFieldMask {")
out.append(" Uint64 Words[%d];" % words)
out.append("};")
out.append("")
out.append("inline constexpr Bool MGPipeFieldMaskHas(const MGPipeFieldMask& mask, MGPipeInputField field) {")
out.append(" const SizeT index = static_cast<SizeT>(field);")
out.append(" return (mask.Words[index / 64] >> (index % 64)) & 1u;")
out.append("}")
out.append("")
out.append("inline constexpr MGPipeFieldMask kMGPipeClassFieldMask[kMGPipeVerbClassCount] = {")
for cls in classes:
bits = [0] * words
names = fields[cls] + [n for n in sticky_names if n not in fields[cls]]
for name in names:
index = field_index[name]
bits[index // 64] |= 1 << (index % 64)
out.append(" // %s: %d fields (%d own + %d sticky)" % (cls, len(names), len(fields[cls]), len(names) - len(fields[cls])))
out.append(" {{%s}}," % ", ".join("0x%016xull" % b for b in bits))
out.append("};")
out.append("")
out.append("static_assert(kMGPipeInputFieldCount <= %d * 64, \"MGPipeFieldMask needs another word\");" % words)
return "\n".join(out) + "\n"
def gen_coverage(accessors, deltas, rows, calls):
call_names = set(c.Name for c in calls)
pseudo = {"kClientResolved", "kReverseChannel", "kStructuralHandle"}
@@ -625,16 +1083,71 @@ def write(path, text, check, changed):
handle.write(text)
def expect_trip(name, fn):
"""Runs one negative control; a gate that lets it through is the failure."""
try:
fn()
except SystemExit as trip:
print("gen_pipe: self-test %s: tripped as expected (%s)" % (name, str(trip).splitlines()[0][:100]))
return 1
print("gen_pipe: self-test %s: DID NOT TRIP" % name, file=sys.stderr)
return 0
def self_test(accessors):
"""The negative controls (check_include_closure.py's shape): each gate must go red for
its reason, and zero trips is itself an error."""
canned_struct = "struct Canned {\n Uint32 A;\n Uint32 B, C;\n Uint8 Pad0[3];\n void F() { return; }\n};\n"
controls = [
("struct member without F(...)",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])),
("F(...) that is not a member",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])),
("payload with no struct",
lambda: check_field_lists_cover_struct_members({"Nowhere": ["A"]}, ["Nowhere"], [canned_struct])),
]
fill_text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
verb_row = re.compile(r"X\(\s*DrawArrays\s*,\s*kDraw\s*\)")
field_row = re.compile(r"X\(\s*kDraw\s*,\s*GetBoundVertexArray\s*\)")
if not verb_row.search(fill_text) or not field_row.search(fill_text):
sys.exit("gen_pipe: self-test: FillPoints.def lost the rows the controls edit")
controls.append(("verb missing from FillPoints.def", lambda: parse_fill_points(
accessors, text=verb_row.sub("", fill_text, count=1))))
controls.append(("verb that is not a GLFunctionsTable member", lambda: parse_fill_points(
accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1))))
controls.append(("field row naming a non-accessor", lambda: parse_fill_points(
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
trips = 0
for name, fn in controls:
trips += expect_trip(name, fn)
# The positive control: the canned struct's exact list passes, and the parser sees the
# padding member as padding and the function as not a member.
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])
if trips == 0:
sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything")
if trips != len(controls):
sys.exit("gen_pipe: self-test: %d of %d negative controls did not trip" % (len(controls) - trips, len(controls)))
print("gen_pipe: self-test: %d negative-control trip(s), positive control OK" % trips)
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="do not write; exit 1 if regenerating would change anything")
parser.add_argument("--self-test", action="store_true",
help="run the negative controls (each gate must trip) and exit")
args = parser.parse_args()
calls = parse_calls()
payloads = parse_verify_payloads()
check_call_payloads_have_field_lists(calls, payloads)
accessors, deltas = parse_coverage()
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
accessors, deltas, sticky = parse_coverage()
if args.self_test:
return self_test(accessors)
scan_live_accessors(accessors)
verbs, classes, fields = parse_fill_points(accessors)
rows = parse_inventory()
if not os.path.isdir(GENERATED_DIR):
@@ -646,13 +1159,17 @@ def main():
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"), gen_wire(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
gen_fill_points(accessors, sticky, verbs, classes, fields), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeSpanTable.inc"), gen_span_table(), args.check, changed)
screen = sum(1 for c in calls if c.IsScreen)
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields "
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors)))
"(%d sticky), %d verbs, %d classes"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
len(verbs), len(classes)))
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
"%d structural handle, %d UNMAPPED"
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],
+498
View File
@@ -0,0 +1,498 @@
#!/usr/bin/env python3
# MobileGL - scripts/symbol_report.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Per-symbol nm/.text attribution between two libMobileGL.so builds.
P0.5's acceptance gate says every `nm --defined-only -S` delta has to be explainable per
symbol (ROADMAP.md:16). The awkward part is that de-nesting a type renames every mangled
name that mentions it - `ProgramObject::LinkArtifacts` -> `LinkArtifacts` shows up inside
every `std::vector<...>` instantiation too - so a raw nm diff of a pure move looks
catastrophic. `--strip-scope` folds those into a "renamed-only" bucket: same normalised
demangled name, byte-identical size. What is left over is the report's real content.
python3 scripts/symbol_report.py --before old.so --after new.so
python3 scripts/symbol_report.py --before old.so --after new.so \\
--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::' \\
--only-names 'TypeFacts,ResourceReflection,XfbVarying,LinkArtifacts,SpirvArtifacts' \\
--markdown ~/w7/p05-symbol-report.md
GUARD RAILS - the comparison is meaningless unless both .so files were built with:
* the same CMAKE_BUILD_TYPE (the visibility presets differ between configurations,
CMakeLists.txt:578-598, so a Debug/Release pair "adds" thousands of symbols),
* MOBILEGL_ENABLE_LTO OFF on both sides (CMakeLists.txt:108,137-146 - LTO merges and
renames at will and nothing here is attributable afterwards),
* the same compiler and standard library.
The header of every report prints both paths and their byte sizes so a mismatched pair is
visible in the output rather than only in the reader's assumptions.
This tool is informational by default (ARCHITECTURE.md:507, job name `monolith-symbol-report`
ARCHITECTURE.md:568): with no gate flag it prints its report and exits 0, whatever it found.
Three flags turn it into a hard gate, and P1's G1 uses the first two - the strangler's pull build
has to stay byte-identical, and G1 spells that out as `added == removed == resized == renamed == 0`
with a `.text` delta of zero:
python3 scripts/symbol_report.py --before base.so --after head.so \
--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0
* `--fail-on-symbol-set-change` covers all FOUR buckets, not just added/removed: a resize at zero
net delta and a rename at zero net delta are both source changes, and both are what a guard
rewrite produces. It always compares at threshold 0, whatever `--threshold` is set to.
* `--fail-on-added-bytes 0` means byte-identical, so a SHRUNK .text fails it too; a positive
budget keeps the one-sided "must not grow by more than N" meaning. `--fail-on-text-delta` is
the explicit spelling of the zero case for a run that wants no byte budget at all.
A gate that fires still writes its Markdown and JSON first: the report IS the diagnosis, and a
CI job that failed before uploading its artifact is a job nobody can act on.
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
PREFIX = "symbol-report: "
# `nm --defined-only -S` prints "<addr> <size> <type> <name>", and "<addr> <type> <name>"
# for a defined symbol whose size the object file does not carry (absolute symbols,
# assembler labels).
NM_SIZED_RE = re.compile(r"^([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(\S)\s+(.+)$")
NM_UNSIZED_RE = re.compile(r"^([0-9a-fA-F]+)\s+(\S)\s+(.+)$")
# `size --format=sysv` prints "section size addr" rows plus a Total row.
SIZE_ROW_RE = re.compile(r"^(\S+)\s+(\d+)(?:\s+(\d+))?\s*$")
INTERESTING_SECTIONS = (".text", ".data", ".bss", ".rodata", "Total")
def say(message):
print(PREFIX + message)
def parse_nm(text):
"""nm --defined-only -S transcript -> {mangled: (type, size_or_None)}."""
symbols = {}
for line in text.splitlines():
line = line.rstrip()
if not line:
continue
match = NM_SIZED_RE.match(line)
if match:
symbols[match.group(4)] = (match.group(3), int(match.group(2), 16))
continue
match = NM_UNSIZED_RE.match(line)
if match:
symbols[match.group(3)] = (match.group(2), None)
return symbols
def parse_size(text):
"""size --format=sysv transcript -> {section: bytes}."""
sections = {}
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("section"):
continue
match = SIZE_ROW_RE.match(stripped)
if not match:
continue
sections[match.group(1)] = int(match.group(2))
return sections
def demangle(names, cxxfilt):
"""Batch-demangle through c++filt; a plain identifier passes through unchanged."""
ordered = list(names)
if not ordered:
return {}
if not cxxfilt or not (shutil.which(cxxfilt) or os.path.isfile(cxxfilt)):
return {name: name for name in ordered}
completed = subprocess.run([cxxfilt], input="\n".join(ordered) + "\n",
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
lines = completed.stdout.splitlines()
if len(lines) != len(ordered):
return {name: name for name in ordered}
return dict(zip(ordered, lines))
def parent_scope(prefix):
"""`A::B::C::` -> `A::B::` (drop the last named component, keep the enclosing scope).
A de-nesting moves `A::B::C::X` to `A::B::X`, so folding the two spellings together
means deleting the `C::` component, NOT the whole prefix - deleting the whole prefix
would turn the before side into a bare `X` that no after-side name matches.
"""
body = prefix[:-2] if prefix.endswith("::") else prefix
cut = body.rfind("::")
return body[:cut + 2] if cut >= 0 else ""
def normalise(name, strip_scopes, rename_map):
"""Textual folding of the demangled name: this is what makes a de-nesting a rename."""
for scope in strip_scopes:
name = name.replace(scope, parent_scope(scope))
for old, new in rename_map:
name = name.replace(old, new)
return name
def build_side(path, nm_tool, size_tool, cxxfilt, strip_scopes, rename_map, canned=None):
if canned is None:
nm_text = subprocess.run([nm_tool, "--defined-only", "-S", path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, check=True).stdout
size_text = subprocess.run([size_tool, "--format=sysv", path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, check=True).stdout
else:
nm_text, size_text = canned
raw = parse_nm(nm_text)
demangled = demangle(raw.keys(), cxxfilt)
table = {}
for mangled, (kind, size) in raw.items():
key = normalise(demangled.get(mangled, mangled), strip_scopes, rename_map)
table.setdefault(key, []).append({"mangled": mangled, "type": kind,
"size": size or 0,
"demangled": demangled.get(mangled, mangled)})
folded = {}
for key, entries in table.items():
folded[key] = {
"size": sum(e["size"] for e in entries),
"count": len(entries),
"mangled": sorted(e["mangled"] for e in entries),
"type": entries[0]["type"],
}
return folded, parse_size(size_text), len(raw)
def bucket(before, after, only_names, threshold):
def wanted(name):
return not only_names or any(n in name for n in only_names)
removed, added, resized, renamed, unchanged = [], [], [], [], 0
for name in sorted(set(before) | set(after)):
b = before.get(name)
a = after.get(name)
if b is None:
if wanted(name):
added.append({"name": name, "delta": a["size"], "before": 0, "after": a["size"]})
continue
if a is None:
if wanted(name):
removed.append({"name": name, "delta": -b["size"], "before": b["size"], "after": 0})
continue
if a["size"] != b["size"] and abs(a["size"] - b["size"]) > threshold:
if wanted(name):
resized.append({"name": name, "delta": a["size"] - b["size"],
"before": b["size"], "after": a["size"]})
continue
if a["mangled"] != b["mangled"]:
# same normalised name, byte-identical size: the pure-move signature
if wanted(name):
renamed.append({"name": name, "delta": 0,
"before": b["mangled"][0], "after": a["mangled"][0]})
continue
unchanged += 1
for group in (removed, added, resized):
group.sort(key=lambda row: (-abs(row["delta"]), row["name"]))
renamed.sort(key=lambda row: row["name"])
return removed, added, resized, renamed, unchanged
def markdown_table(title, rows, columns):
lines = ["", "### {} ({})".format(title, len(rows)), ""]
if not rows:
lines.append("_none_")
lines.append("")
return lines
lines.append("| " + " | ".join(columns) + " |")
lines.append("|" + "|".join(["---"] * len(columns)) + "|")
for row in rows:
lines.append("| " + " | ".join(str(cell).replace("|", "\\|") for cell in row) + " |")
lines.append("")
return lines
CANNED_BEFORE = ("""0000000000001000 0000000000000010 T MobileGL::MG_State::GLState::ProgramObject::LinkArtifacts::Reset()
0000000000002000 0000000000000020 T MobileGL::MG_State::GLState::ProgramObject::Link()
0000000000003000 0000000000000030 T MobileGL::Gone()
0000000000004000 0000000000000040 T MobileGL::Grew()
0000000000005000 T MobileGL::NoSize()
""", """libBefore.so :
section size addr
.text 1000 100
.data 200 2000
.bss 300 3000
Total 1500
""")
CANNED_AFTER = ("""0000000000001000 0000000000000010 T MobileGL::MG_State::GLState::LinkArtifacts::Reset()
0000000000002000 0000000000000020 T MobileGL::MG_State::GLState::ProgramObject::Link()
0000000000004000 0000000000000050 T MobileGL::Grew()
0000000000006000 0000000000000060 T MobileGL::BrandNew()
0000000000005000 T MobileGL::NoSize()
""", """libAfter.so :
section size addr
.text 1100 100
.data 200 2000
.bss 300 3000
Total 1600
""")
def gate_failures(added, removed, resized, renamed, text_delta, fail_on_added_bytes,
fail_on_symbol_set_change, fail_on_text_delta=False):
"""The reasons this run should fail, in the order they are reported. Empty means green.
Pure, and takes the buckets rather than the file paths, so the self-test can drive it from the
canned transcripts: a gate whose only test is a real build is a gate nobody re-tests.
P1's G1 is spelled `added == removed == resized == renamed == 0` and `.text` delta 0, so all
FOUR buckets are part of --fail-on-symbol-set-change and a budget of 0 bytes means zero
movement in EITHER direction. A shrunk .text and a resize with zero net delta are exactly the
shapes a guard rewrite produces, and both used to walk straight through this function.
"""
reasons = []
if fail_on_symbol_set_change and (added or removed or resized or renamed):
reasons.append(
"--fail-on-symbol-set-change: {} symbol(s) added, {} removed, {} resized, {} renamed. "
"None of that is a property of the build machine, so any change here is a source "
"change - name each one in the commit message or fix it.".format(
len(added), len(removed), len(resized), len(renamed)))
if fail_on_text_delta and text_delta != 0:
reasons.append(
"--fail-on-text-delta: .text moved by {:+d} bytes.".format(text_delta))
if fail_on_added_bytes is not None:
# A budget of zero is not "must not grow", it is "must not move": the pull build of a
# strangler that got smaller is just as much a code change as one that got bigger, and G1
# names the delta, not its sign. A positive budget keeps the older, one-sided meaning.
if fail_on_added_bytes == 0 and text_delta != 0 and not fail_on_text_delta:
reasons.append(
"--fail-on-added-bytes 0: .text moved by {:+d} bytes (a budget of 0 means the "
"section must be byte-identical, in either direction).".format(text_delta))
elif fail_on_added_bytes > 0 and text_delta > fail_on_added_bytes:
reasons.append(
"--fail-on-added-bytes {}: .text grew by {} bytes.".format(
fail_on_added_bytes, text_delta))
return reasons
def self_test():
strip = ["MobileGL::MG_State::GLState::ProgramObject::"]
before, before_sections, before_count = build_side(None, None, None, None, strip, [],
canned=CANNED_BEFORE)
after, after_sections, after_count = build_side(None, None, None, None, strip, [],
canned=CANNED_AFTER)
removed, added, resized, renamed, unchanged = bucket(before, after, [], 0)
problems = []
if before_count != 5 or after_count != 5:
problems.append("nm parse counts: {} / {} (expected 5 / 5)".format(before_count, after_count))
if [r["name"] for r in removed] != ["MobileGL::Gone()"]:
problems.append("removed bucket: {}".format([r["name"] for r in removed]))
if [r["name"] for r in added] != ["MobileGL::BrandNew()"]:
problems.append("added bucket: {}".format([r["name"] for r in added]))
if [(r["name"], r["delta"]) for r in resized] != [("MobileGL::Grew()", 0x10)]:
problems.append("resized bucket: {}".format([(r["name"], r["delta"]) for r in resized]))
# `ProgramObject::LinkArtifacts::Reset` -> `LinkArtifacts::Reset` folds to the same
# normalised name at the same size: renamed-only, not added+removed.
if [r["name"] for r in renamed] != ["MobileGL::MG_State::GLState::LinkArtifacts::Reset()"]:
problems.append("renamed bucket: {}".format([r["name"] for r in renamed]))
if unchanged != 2: # ProgramObject::Link() and NoSize()
problems.append("unchanged count: {} (expected 2)".format(unchanged))
if before_sections.get(".text") != 1000 or after_sections.get("Total") != 1600:
problems.append("size --format=sysv parse: {} / {}".format(before_sections, after_sections))
# The three gates, driven from the same canned transcripts: the after side adds one symbol,
# removes one, resizes one, renames one and grows .text by 100, so each flag must fire, each
# must stay quiet when it is not asked for, and --fail-on-added-bytes must accept a delta it
# was told to tolerate.
text_delta = after_sections.get(".text", 0) - before_sections.get(".text", 0)
if gate_failures(added, removed, resized, renamed, text_delta, None, False):
problems.append("gates fired with no flag set")
if len(gate_failures(added, removed, resized, renamed, text_delta, None, True)) != 1:
problems.append("--fail-on-symbol-set-change did not fire on 1 added + 1 removed")
if len(gate_failures(added, removed, resized, renamed, text_delta, 0, False)) != 1:
problems.append("--fail-on-added-bytes 0 did not fire on a +100 .text delta")
if gate_failures(added, removed, resized, renamed, text_delta, 100, False):
problems.append("--fail-on-added-bytes 100 fired on a +100 .text delta")
if len(gate_failures([], [], [], [], text_delta, 0, True)) != 1:
problems.append("an unchanged symbol set still tripped --fail-on-symbol-set-change")
# The three shapes that used to walk through: a SHRUNK .text under a zero budget, a resize with
# no net delta, and a rename with no net delta. G1 forbids all three by name.
if not gate_failures([], [], [], [], -4096, 0, False):
problems.append("--fail-on-added-bytes 0 passed a .text that SHRANK by 4096 bytes")
if not gate_failures([], [], resized, [], 0, None, True):
problems.append("--fail-on-symbol-set-change passed a resized symbol at zero net delta")
if not gate_failures([], [], [], renamed, 0, None, True):
problems.append("--fail-on-symbol-set-change passed a renamed symbol at zero net delta")
if not gate_failures([], [], [], [], -4096, None, False, True):
problems.append("--fail-on-text-delta passed a .text that SHRANK by 4096 bytes")
if gate_failures([], [], [], [], 0, None, False, True):
problems.append("--fail-on-text-delta fired on a zero .text delta")
for problem in problems:
say("self-test: " + problem)
say("self-test: " + ("OK (2 canned transcripts, 5 buckets, 3 gates)" if not problems else "FAILED"))
return 0 if not problems else 1
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--before", help="the baseline libMobileGL.so")
parser.add_argument("--after", help="the libMobileGL.so under test")
parser.add_argument("--nm", default="nm")
parser.add_argument("--size", default="size")
parser.add_argument("--cxxfilt", default="c++filt")
parser.add_argument("--markdown", default=None, help="write the Markdown report here")
parser.add_argument("--json", default=None, help="write the machine-readable result here")
parser.add_argument("--strip-scope", action="append", default=[], metavar="PREFIX",
help="de-nest this scope in demangled names before comparing: PREFIX "
"'A::B::C::' rewrites every 'A::B::C::X' to 'A::B::X' (repeatable). "
"This is what folds a de-nesting into a rename instead of an "
"added+removed pair, including inside template arguments.")
parser.add_argument("--rename-map", action="append", default=[], metavar="OLD=NEW",
help="textual OLD=NEW substitution on demangled names (repeatable)")
parser.add_argument("--only-names", default=None,
help="comma-separated: list only symbols whose demangled text contains one")
parser.add_argument("--threshold", type=int, default=0,
help="ignore size deltas of at most this many bytes")
parser.add_argument("--fail-on-added-bytes", type=int, default=None,
help="exit non-zero when .text grew by more than this many bytes; 0 is "
"special and means the section must be BYTE-IDENTICAL, so a shrink "
"fails it too (that is what P1's G1 asks for)")
parser.add_argument("--fail-on-text-delta", action="store_true",
help="exit non-zero when .text moved at all, in either direction. The "
"explicit spelling of what --fail-on-added-bytes 0 also does.")
parser.add_argument("--fail-on-symbol-set-change", action="store_true",
help="exit non-zero when any defined symbol was added, removed, resized or "
"renamed (the four buckets of the report; --strip-scope decides which "
"of added+removed vs renamed a de-nesting lands in, it does not "
"excuse it). Compared at threshold 0 whatever --threshold says.")
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
return self_test()
if not args.before or not args.after:
parser.error("--before and --after are required (or use --self-test)")
if args.fail_on_added_bytes is not None and args.fail_on_added_bytes < 0:
parser.error("--fail-on-added-bytes takes a byte budget of 0 or more")
rename_map = []
for entry in args.rename_map:
if "=" not in entry:
parser.error("--rename-map expects OLD=NEW, got " + entry)
old, new = entry.split("=", 1)
rename_map.append((old, new))
only_names = [n.strip() for n in args.only_names.split(",")] if args.only_names else []
before, before_sections, before_count = build_side(
args.before, args.nm, args.size, args.cxxfilt, args.strip_scope, rename_map)
after, after_sections, after_count = build_side(
args.after, args.nm, args.size, args.cxxfilt, args.strip_scope, rename_map)
removed, added, resized, renamed, unchanged = bucket(before, after, only_names, args.threshold)
# --threshold is a REPORT control - "do not list resizes smaller than this" - and a gate that
# read the thresholded buckets would quietly weaken itself the day someone raised it. The gates
# always see the threshold-0 buckets.
if args.threshold:
gate_removed, gate_added, gate_resized, gate_renamed, _ = bucket(before, after, only_names, 0)
else:
gate_removed, gate_added, gate_resized, gate_renamed = removed, added, resized, renamed
before_text = before_sections.get(".text", 0)
after_text = after_sections.get(".text", 0)
delta = after_text - before_text
percent = (100.0 * delta / before_text) if before_text else 0.0
say("before: {} ({} bytes on disk)".format(args.before, os.path.getsize(args.before)))
say("after : {} ({} bytes on disk)".format(args.after, os.path.getsize(args.after)))
if args.strip_scope:
say("strip-scope: " + " ; ".join(args.strip_scope))
if rename_map:
say("rename-map: " + " ; ".join("{}={}".format(o, n) for o, n in rename_map))
say(".text {} -> {} ({:+d}, {:+.3f}%)".format(before_text, after_text, delta, percent))
for section in INTERESTING_SECTIONS:
if section == ".text":
continue
b = before_sections.get(section)
a = after_sections.get(section)
if b is None and a is None:
continue
say("{} {} -> {} ({:+d})".format(section, b or 0, a or 0, (a or 0) - (b or 0)))
say("{} -> {} defined symbols: {} added, {} removed, {} resized, {} renamed".format(
before_count, after_count, len(added), len(removed), len(resized), len(renamed)))
# The two counts differ by the folding: several mangled symbols can share one
# normalised demangled name (local aliases, `.cold` parts, identical-COMDAT clones).
say("{} -> {} normalised names, {} unchanged (name, size and mangling all identical)".format(
len(before), len(after), unchanged))
if only_names:
say("listing restricted to names containing: " + ", ".join(only_names))
gates = gate_failures(gate_added, gate_removed, gate_resized, gate_renamed, delta,
args.fail_on_added_bytes, args.fail_on_symbol_set_change,
args.fail_on_text_delta)
if (args.fail_on_added_bytes is None and not args.fail_on_symbol_set_change
and not args.fail_on_text_delta):
say("no gate flag: informational run")
elif args.threshold:
say("gates compare at threshold 0 ({} added, {} removed, {} resized, {} renamed there); "
"--threshold {} only shortens the report".format(
len(gate_added), len(gate_removed), len(gate_resized), len(gate_renamed),
args.threshold))
lines = ["# MobileGL symbol report", "",
"| side | path | file bytes | .text |",
"|---|---|---|---|",
"| before | `{}` | {} | {} |".format(args.before, os.path.getsize(args.before), before_text),
"| after | `{}` | {} | {} |".format(args.after, os.path.getsize(args.after), after_text),
"",
"`.text` {} -> {} ({:+d}, {:+.3f}%). {} -> {} defined symbols: "
"{} added, {} removed, {} resized, {} renamed, {} unchanged.".format(
before_text, after_text, delta, percent, before_count, after_count,
len(added), len(removed), len(resized), len(renamed), unchanged)]
lines += markdown_table("Removed", [(r["name"], r["before"]) for r in removed],
["symbol", "bytes"])
lines += markdown_table("Added", [(r["name"], r["after"]) for r in added],
["symbol", "bytes"])
lines += markdown_table("Resized", [(r["name"], r["before"], r["after"], "{:+d}".format(r["delta"]))
for r in resized],
["symbol", "before", "after", "delta"])
lines += markdown_table("Renamed only (same size)",
[(r["name"], r["before"], r["after"]) for r in renamed],
["normalised symbol", "before mangling", "after mangling"])
report = "\n".join(lines) + "\n"
if args.markdown:
with open(args.markdown, "w", encoding="utf-8", newline="\n") as handle:
handle.write(report)
say("markdown written to " + args.markdown)
else:
print(report)
if args.json:
with open(args.json, "w", encoding="utf-8", newline="\n") as handle:
json.dump({"before": args.before, "after": args.after,
"sections_before": before_sections, "sections_after": after_sections,
"removed": removed, "added": added, "resized": resized,
"renamed": renamed, "unchanged": unchanged}, handle, indent=2)
handle.write("\n")
say("json written to " + args.json)
# After the report is written, never before: a gate that fired is exactly when someone needs
# the Markdown and the JSON.
for reason in gates:
say("FAIL " + reason)
return 1 if gates else 0
if __name__ == "__main__":
sys.exit(main())
+4 -2
View File
@@ -372,10 +372,12 @@ function(add_trace_replay_test CASE_NAME BACKEND)
-P ${MOBILEGL_TRACE_ROOT}/run_trace_case.cmake)
if(BACKEND STREQUAL "DirectGLES")
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330")
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
LABELS retrace)
else()
set_tests_properties(MobileGLTraceReplay.${CASE_NAME}.${BACKEND} PROPERTIES
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330")
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
LABELS retrace)
endif()
endfunction()
+62
View File
@@ -127,3 +127,65 @@ endif()
if(NOT replay_result EQUAL 0)
message(FATAL_ERROR "${TRACE_CASE_NAME} ${TRACE_BACKEND} trace replay failed with status ${replay_result}")
endif()
# --- MOBILEGL_PIPE_VERIFY: the third CI mode's own assertions (gates G3 and G8) --------------
#
# A retrace that exported MOBILEGL_PIPE_VERIFY=1 at a library which was never configured with
# -DMOBILEGL_PIPE_VERIFY=ON is a no-op that looks exactly like a clean pass: the variable steers
# nothing, the frames still match their goldens, and the case reports green having verified
# nothing at all. The mode therefore has to prove it ran, and the only channel a `cmake -P` script
# has for that is the library's own log.
#
# Three demands, all of them silent when MOBILEGL_PIPE_VERIFY is unset or "0", so an ordinary
# retrace is untouched:
# * mobilegl.log exists - the replay wrote one, so the library was loaded and logging;
# * it carries "MGPipe: verify armed" - the comparator armed in THIS process;
# * it carries none of the three MGPipe Fatals: Fatal{PipeVerifyDiffer (a push/pull divergence,
# the thing the mode exists to find), Fatal{UnmigratedPipeInput (a backend read of a field the
# verb's fill table does not list - fixed by adding the row to MG_Pipe/FillPoints.def, never by
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT
# or MOBILEGL_PIPE_POISON_OMIT). The third is in the regex on purpose even though D2 makes it
# abort the process: with MOBILEGL_PIPE_VERIFY_FATAL=0 the abort is exactly what does not
# happen, and a typo'd knob would otherwise leave the negative-control lane looking healthy.
# The Fatal check is not redundant with the replay's exit status: MOBILEGL_PIPE_VERIFY_FATAL=0 is
# the supported triage configuration, and there the divergence is logged and counted rather than
# aborted, so the run would otherwise finish 0 with its own report in the log.
if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "")
set(pipe_verify_case "${TRACE_CASE_NAME} ${TRACE_BACKEND}")
if("$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "0" OR "$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "false")
message(STATUS "MGPipe verify: MOBILEGL_PIPE_VERIFY=$ENV{MOBILEGL_PIPE_VERIFY}, no verify assertions for ${pipe_verify_case}")
elseif(NOT EXISTS "${mobilegl_log}")
message(FATAL_ERROR
"MOBILEGL_PIPE_VERIFY is set for ${pipe_verify_case} but the run wrote no ${mobilegl_log}, "
"so there is no evidence the comparator ever armed. A verify retrace with no library log "
"cannot be counted as a verify retrace.")
else()
file(READ "${mobilegl_log}" pipe_verify_log)
string(FIND "${pipe_verify_log}" "MGPipe: verify armed" pipe_verify_armed_at)
if(pipe_verify_armed_at EQUAL -1)
message(FATAL_ERROR
"MOBILEGL_PIPE_VERIFY is set for ${pipe_verify_case} and the library never reported "
"\"MGPipe: verify armed\". Either this libMobileGL.so was not built with "
"-DMOBILEGL_PIPE_VERIFY=ON - in which case the whole verify retrace lane is comparing "
"nothing - or the runtime knob never reached the process. Check that the VERIFY "
"runtime artifact is the one unpacked at ${MOBILEGL_LIBRARY}.")
endif()
file(STRINGS "${mobilegl_log}" pipe_verify_fatals
REGEX "Fatal\\{(PipeVerifyDiffer|UnmigratedPipeInput|PipeVerifyBadKnob)")
if(pipe_verify_fatals)
foreach(line IN LISTS pipe_verify_fatals)
message(STATUS "${line}")
endforeach()
list(LENGTH pipe_verify_fatals pipe_verify_fatal_count)
message(FATAL_ERROR
"${pipe_verify_case}: ${pipe_verify_fatal_count} MGPipe Fatal(s) under "
"MOBILEGL_PIPE_VERIFY. Fatal{PipeVerifyDiffer, \"<Field>@<Verb>\"} is a real push/pull "
"divergence and is recorded, not silenced; Fatal{UnmigratedPipeInput, \"<Field>@<Verb>\"} "
"is a missing row in MG_Pipe/FillPoints.def's class table - add it, regenerate, rerun "
"(never mark the field sticky); Fatal{PipeVerifyBadKnob, ...} is a misspelt "
"MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT - fix the spelling, the "
"vocabularies are kMGPipeInputFieldNames[] and kMGPipeVerbNames[].")
endif()
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
endif()
endif()
+8
View File
@@ -13,6 +13,7 @@
"cases": [
{
"name": "OpenRA",
"verify": true,
"trace_archive": "openra.tgz",
"trace_file": "openra.trace",
"golden": "openra.0000031249.png",
@@ -27,6 +28,7 @@
},
{
"name": "minecraft-1.21.4-startup",
"verify": true,
"trace_archive": "minecraft-1.21.4-startup.tgz",
"golden": "minecraft-1.21.4-startup.0000092195.png",
"target_call": 92195,
@@ -34,6 +36,7 @@
},
{
"name": "minecraft-1.21.4-main-menu",
"verify": true,
"trace_archive": "minecraft-1.21.4-main-menu.tgz",
"golden": "minecraft-1.21.4-main-menu.0000481787.png",
"alternate_golden": "minecraft-1.21.4-main-menu.0000481787-mali.png",
@@ -42,6 +45,7 @@
},
{
"name": "minecraft-1.21.11-main-menu",
"verify": true,
"trace_archive": "minecraft-1.21.11-main-menu.tgz",
"golden": "minecraft-1.21.11-main-menu.0000205347.png",
"target_call": 205347,
@@ -49,6 +53,7 @@
},
{
"name": "minecraft-1.17-main-menu-854",
"verify": true,
"trace_archive": "minecraft-1.17-main-menu-854.tgz",
"golden": "minecraft-1.17-main-menu-854.0000117757.png",
"target_call": 117757,
@@ -56,6 +61,7 @@
},
{
"name": "minecraft-1.21.4-in-world",
"verify": true,
"trace_archive": "minecraft-1.21.4-in-world.tgz",
"golden": "minecraft-1.21.4-in-world.0000280000.png",
"target_call": 280000
@@ -70,6 +76,7 @@
},
{
"name": "minecraft-1.21.4-fabric-sodium-in-world",
"verify": true,
"trace_archive": "minecraft-1.21.4-fabric-sodium-in-world.tgz",
"golden": "minecraft-1.21.4-fabric-sodium-in-world.0000923340.png",
"target_call": 923340,
@@ -277,6 +284,7 @@
},
{
"name": "improved-transparency-minecraft-26.3",
"verify": true,
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
"target_call": 2667619,
+27
View File
@@ -26,6 +26,16 @@ def load_trace_case_manifest(path=TRACE_CASES_JSON):
for key in ("trace_archive", "trace_file", "golden", "target_call", "width", "height"):
if key not in merged:
raise ValueError(f"{key} is required for {name}")
# "verify" opts a case into the MOBILEGL_PIPE_VERIFY retrace subset. It is checked here
# rather than where the matrix is built so that a typo is a loud manifest error in every
# consumer (the cmake emitter included) instead of a subset that is quietly one case short.
verify = merged.get("verify", False)
if not isinstance(verify, bool):
raise ValueError(f"verify must be true or false for {name}")
if verify and not merged.get("ci", True):
raise ValueError(
f"{name} is marked verify but excluded from CI, so the verify matrix would drop it"
)
cases.append(merged)
return {"defaults": defaults, "cases": cases}
@@ -72,6 +82,16 @@ def ci_trace_cases(cases):
return [case for case in cases if case.get("ci", True)]
def verify_trace_cases(cases):
"""The subset the third CI mode retraces.
The verify build compares two state models at every verb boundary and again at every accessor
read, which the design budgets at 5-10x, so the per-push lane runs a named subset and the full
79-case sweep happens at the phase exit and on workflow_dispatch.
"""
return [case for case in cases if case.get("verify", False)]
def ci_backends(case):
backends = case.get("ci_backends")
if backends is None:
@@ -98,6 +118,10 @@ def github_test_matrix(cases):
}
def github_verify_matrix(cases):
return github_test_matrix(verify_trace_cases(cases))
def github_apk_matrix(cases):
backends = {
"DirectGLES": {"name": "DirectGLES", "gpu": "software"},
@@ -158,6 +182,7 @@ def parse_args():
choices=(
"names",
"github-test-matrix",
"github-verify-matrix",
"github-apk",
"github-apk-matrix",
"fixture-files",
@@ -177,6 +202,8 @@ def main():
print(json.dumps([case["name"] for case in cases], separators=(",", ":")))
elif args.format == "github-test-matrix":
print(json.dumps(github_test_matrix(cases), separators=(",", ":")))
elif args.format == "github-verify-matrix":
print(json.dumps(github_verify_matrix(cases), separators=(",", ":")))
elif args.format == "github-apk":
print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":")))
elif args.format == "github-apk-matrix":