[Docs] (docs/Disaggregated, MG_Impl/Pipe, MG_Remote/Client): shorten long comments and move their design arguments into docs

This commit is contained in:
2026-10-15 21:18:49 -04:00
parent 7ec1448f03
commit 37ecba200b
26 changed files with 704 additions and 2401 deletions
+7 -27
View File
@@ -4,33 +4,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// WHICH ELEMENTS OF A CLIENT-MEMORY ARRAY A DRAW ACTUALLY FETCHES.
//
// A client-memory vertex array has no extent the object could report, so "how much of it
// has to exist before this draw can run" is a question only the DRAW can answer: the
// index bytes it will read, the instance count and base instance it shifts by, and - for
// the indirect forms - the command block a shader may have written moments earlier.
//
// Two roles ask it and for the same reason. The wire arm asks it to snapshot the fetched
// elements into owned buffers before the draw crosses (MGPipeOwnedDrawInputs), and the
// monolith arm asks it to size the per-draw upload the backend has always done from the
// application's own pointer (DirectGLES' SyncClientSideAttributesForDraw*, Magma's
// UploadAndBindVertexBuffers). The question is identical, so the answer lives here once:
// two copies of this arithmetic would be two chances to disagree about which vertices a
// draw reads, and disagreeing is a wrong picture rather than an error.
//
// WHAT IT DELIBERATELY DOES NOT DO: dereference application memory. The plan names element
// INDICES; reading the bytes behind them is the caller's, because only the caller knows
// whether the elements are read from the client's own array or out of a store the server
// owns.
//
// COMPILED IN EVERY FLAVOR, and the guard it used to carry (`MOBILEGL_BUILD_DISAGGREGATED &&
// MOBILEGL_PIPE_PUSH`) was the same mistake the monolith arm's callers carried: the
// MONOLITH arm is not a split-arm fallback, it is the arm the pull build runs. An indexed or
// indirect draw over a client-memory array staged nothing there either, so the question has
// to be answerable - and answered - in the transport-free library too. The plan itself is
// pure arithmetic over the draw's own words; what changes per flavor is only who reads the
// bytes (the caller's reader), never the answer.
// Which elements of a client-memory vertex array a draw fetches. Only the draw can say: the
// index bytes it reads, its instance count and base instance, and for indirect forms the
// command block. The wire arm snapshots those elements into owned buffers
// (MGPipeOwnedDrawInputs); the monolith arm sizes the backend's per-draw upload from the
// application pointer. One copy of the arithmetic keeps the two from disagreeing about which
// vertices a draw reads. The plan names element indices only; the caller's reader dereferences
// memory. It is built in every configuration, since the monolith arm needs it too.
#pragma once
#include <MG_Pipe/MGPipeTypes.h>
+20 -112
View File
@@ -9,78 +9,14 @@
#pragma once
#include <Includes.h>
// P4a's PROGRAM-PIPELINE COMPOSITE, on the client side.
//
// GLContext::GetProgramForDraw() already flattens a bound pipeline into one hidden composite
// ProgramObject entirely in the frontend - it joins every graphics stage, computes the
// pipeline's draw-program signature, looks it up in the pipeline's own cache and, on a miss,
// attaches each stage's LINKED SNAPSHOT into a fresh ProgramObject and links it. All of that
// is frontend work and none of it moves. What this file adds is the one thing the wire needs:
// the composite gets ONE handle, out of the ShaderCso reserved high band, and
// create_shader_state goes out for it exactly as for an ordinary program. THE SERVER NEVER
// LEARNS IT IS A COMPOSITE and needs no "resolved draw program" hook at all.
//
// WHY A BAND RATHER THAN A FLAG ON THE HANDLE: a flag would have to be carried, honoured and
// masked off by every consumer of a ShaderCso handle, on both sides; a reserved slot range is
// a property of the allocator instead, so "an ordinary program can never be handed a composite
// slot" is true by construction. MGPipeSlotAllocator::Allocate refuses the band outright and
// AllocateComposite is the only door in.
//
// WHAT THIS FILE IS ACTUALLY FOR: the composite's slot has TWO INDEPENDENT RELEASE PATHS and
// either order has to free it exactly once.
// * the pipeline cache drops the composite when the draw-program signature moves. In the
// frontend that overwrite drops the last SharedPtr, so the composite's own destructor
// usually runs first; the resolver still speaks the release, because "usually" is not a
// contract and a client that only reacted to destructors would leak a slot the moment the
// frontend started holding a second reference.
// * the composite ProgramObject's own ~ProgramObject, which is an ordinary program's death
// path and takes the same helper.
// Both go through MGPipeEmitShaderCsoDestroyAndFree, and whichever runs second is a PROVEN
// no-op: MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and
// bumps no generation of its own, so a double release cannot skip a generation either.
//
// THE MEMO's KEY IS (CONTEXT ID, PIPELINE GL NAME) AND THE CONTEXT HALF IS NOT OPTIONAL.
// This resolver is a PROCESS singleton while a pipeline's GL name is per context: GLContext
// owns m_programPipelines AND its own name generator m_programPipelineNames (Core.h), so name
// N names two different ProgramPipelineObjects in two contexts, each with its own composite
// and its own handle. Keyed on the name alone, the first emission after a make-current found
// the OTHER context's entry, matched nothing - two composites are two ProgramObjects with two
// lifetime ids, so the handles differ even when the stage set and the signature are identical
// - and released it: a delete_shader_state and a cleared publication latch for a composite
// whose frontend ProgramObject is alive, its band slot handed back and re-issued at gen + 1,
// and the server rebuilding that program (glslang + SPIR-V + spirv-opt, the very cost the
// signature below exists to avoid) once per context switch.
//
// THE CONTEXT ID IS GLContext::GetTextureContextId() AND NOTHING ELSE - the tree's existing
// never-reused per-context id (TextureState::AllocateContextId; PipeInputs carries it as
// m_textureContextId at seven fill points and the backends' own per-context memos key on it).
// Deliberately NOT the GLContext ADDRESS that gPipeInputs and MGPipeTracker::m_context
// compare, because Core.h states the reason that id exists at all: a context freed and remade
// lands on the old heap address, which would put this same defect back one context recreation
// later.
//
// WHAT RELEASES A DESTROYED CONTEXT's ENTRIES: nothing in this file, and that is the correct
// answer rather than an omission. Destroying a context drops m_programPipelines, which drops
// each ProgramPipelineObject, which drops the composite it cached; ~ProgramObject then runs
// MGPipeEmitShaderCsoDestroyAndFree - the composite's OWN release path, the second of the two
// above - and the slot goes back exactly once. The entries those composites leave behind can
// never be found again (no future Observe can carry a dead context id) and could not release
// anything if they were (the allocator erases the lifetime-id mapping on Free), so Reset()
// DROPS them instead of releasing them. That is also what bounds the vector; see Reset().
//
// THE SIGNATURE IS ComputeDrawProgramSignature(), the per-graphics-stage {lifetimeId,
// GetLinkVersion()} array - and DELIBERATELY NOT GetBackendStateVersion(), which is what made
// the SSO conformance loop rebuild the composite (glslang + SPIR-V + spirv-opt) on every draw,
// because a glUniform1i to a sampler moves it.
//
// HEADER-ONLY, for the ownership reason Tracker.h states: a new .cpp would need the root
// CMakeLists.txt, which is the contract package's.
//
// IT IS INCLUDED BY ProgramEmit.h AND NOT THE OTHER WAY ROUND, deliberately: the composite is
// a special case of the program family's own emission, so the family header depends on this
// one and this one depends on nothing of the family's. The reverse arrangement would make the
// resolver reachable only from a translation unit that had already decided to use it, i.e.
// dead in the build that matters and live only in the tests.
// Client side of the program-pipeline composite. GLContext::GetProgramForDraw() builds the
// composite in the frontend, and the program emitter gives it one ShaderCso handle from the
// allocator's reserved band (AllocateComposite), so the server never learns it is a composite.
// This resolver remembers each (context, pipeline)'s composite and releases the old slot when
// the draw-program signature moves; the composite's ~ProgramObject is the other release path,
// both go through MGPipeEmitShaderCsoDestroyAndFree, and the later one is a no-op. Entries are
// keyed on (GLContext::GetTextureContextId(), pipeline GL name); see ARCHITECTURE.md §5.6.1.
// Included by ProgramEmit.h; it depends on nothing of the program family.
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeMutation.h>
@@ -162,28 +98,12 @@ namespace MobileGL::MG_Pipe {
return handle;
}
// A make-current, and it RELEASES NOTHING. The entries name composites that belong to
// the frontend objects of the context being left, those objects outlive the switch, and
// releasing them would emit a delete for a live program.
//
// NOR IS ANY MEMO INVALIDATED, and that is what the context key bought. This used to
// clear a per-entry `Fresh` flag beside `Live`, because with a name-only key an entry
// could not say whether it described "my own pipeline before the switch" or "another
// context's pipeline of the same name" - and exactly one of those two properties could
// hold at a time. The key answers the question directly now, so the freshness flag and
// its one reader (a HandleFor() accessor that had no caller anywhere in the tree) are
// both gone rather than left as scaffolding: `Live`, the release obligation, is the
// entry's only state and nothing but ReleaseEntry may clear it.
//
// WHAT IS LEFT TO DO HERE IS RECLAMATION, and this is the one moment the client is told
// that a context boundary was crossed. An entry whose composite slot is no longer live
// has had its obligation discharged elsewhere - by that composite's own ~ProgramObject,
// which is precisely what happened to EVERY entry of a context that has just been
// destroyed - so it is DROPPED rather than released: a release would resolve nothing
// anyway (the allocator erases the lifetime-id mapping on Free) and no reader is left.
// Without this the vector would grow by one per (context, pipeline name) pair the
// process ever used, where the name-only key bounded it by the highest pipeline name;
// with it, it is bounded by the pairs whose composite slot is actually live.
// Called at a make-current. It releases nothing, since the entries' composites still
// belong to live frontend objects, and invalidates nothing, since entries are keyed by
// context. It only drops entries already released or whose composite slot is no longer
// live, freed by the composite's ~ProgramObject (every entry of a destroyed context ends
// this way); releasing those again would resolve nothing. This bounds the vector by the
// (context, pipeline) pairs whose composite is still live.
void Reset() {
SizeT kept = 0;
for (SizeT i = 0; i < m_entries.size(); ++i) {
@@ -208,24 +128,12 @@ namespace MobileGL::MG_Pipe {
private:
struct Entry {
// NO FRONTEND SharedPtr, and that is the exit-order rule rather than a style
// choice: a static that held one would put a frontend destructor on an exit
// handler's path into a torn-down pipe. A GL name, a signature of plain integers,
// a handle and a lifetime id are all this needs.
// KEYED ON (CONTEXT ID, GL NAME), and the name half is the GL name because a
// ProgramPipelineObject has no lifetime id - ComputeDrawProgramSignature reads the
// STAGE programs' ids and the pipeline itself carries none. The context half is
// GLContext::GetTextureContextId(); see the file header for why the name alone was
// wrong and why the context ADDRESS would be too.
//
// WITHIN ONE CONTEXT glGenProgramPipelines recycles names, so a deleted-and-
// recreated pipeline can still inherit its predecessor's entry; that is bounded and
// self-correcting rather than a hazard. The first Observe on the new object finds a
// signature and a handle that do not match and releases the old entry, and that
// release resolves NOTHING - the allocator erases the lifetime-id mapping on Free,
// so a stale CompositeLifetimeId emits no delete and frees no slot; all it costs is
// one redundant, idempotent death notice, which is the same shape the composite's
// own second release path already has.
// Holds no frontend SharedPtr: the resolver is a never-destroyed static, and a
// frontend destructor must not run at exit into a torn-down pipe. Keyed on
// (context id, pipeline GL name) because a ProgramPipelineObject has no lifetime
// id; see the file header. A name recycled within one context can inherit its
// predecessor's entry; the first Observe then releases it, a no-op because the old
// composite's ~ProgramObject already freed the slot and its lifetime-id mapping.
Uint64 ContextId = 0;
Uint PipelineName = 0;
DrawProgramSignature Signature{};
+8 -29
View File
@@ -9,35 +9,14 @@
#pragma once
#include <Includes.h>
// The render-state CSO cache (ARCHITECTURE.md 4.5.2 / 5.3, P2 brief D7).
//
// THE LOOKUP, and the first step is the whole point:
// 1. m_pipelineStateVersion (widened) did not move -> reuse the last handle. ZERO hashing,
// zero probing, and nothing is emitted unless m_version also moved. That is the steady
// state of every frame, and it is why the tracker asks the cache at all only when the
// dirty walk says the pipeline version moved.
// 2. moved -> hash the 396 pipeline bytes, probe, and on a hit CONFIRM WITH A MEMCMP
// before reusing the handle. ARCHITECTURE.md 4.1 says content addressing on an
// xxHash; a bare 64-bit equality would let a collision alias two different render
// states onto one CSO, which is silent wrong pixels with no gate that can see it.
// Hashed constant-state caches confirm a hit with a full-key memcmp for the same reason.
// The memcmp only ever runs on a pipeline-version change, i.e. never in the steady state.
// 3. miss -> mint a slot, emit create_render_state with every pipeline chunk, then bind.
//
// CAPACITY 64 (ROADMAP.md P2). 64 x (8 + 8 + 396 + 8) = about 26 KB per context. ROADMAP.md
// open question 4 says 64 is provisional and the counters retune it at P13; this ships 64
// and publishes the mint / bind / evict counters that retune reads.
//
// THE NEGATIVE CONTROL. kMGPipeBehaviourNoCsoContentAddressing (bit 63 of the runtime
// MOBILEGL_PIPE_PUSH bitmask) turns off the PROBE and the handle reuse, not the records:
// every pipeline-version change then mints a fresh CSO, binds it and evicts, which is
// precisely "whole-block content addressing" and reproduces the regression
// RenderState.h records. It is what separates "push is slower" from "the CSO design is
// slower", and CsoContentAddressingScenario (package E) is the always-on ctest that stops
// the switch from rotting.
//
// Header-only for the same ownership reason as Tracker.h: the root CMakeLists.txt that
// would name a new .cpp is package A's and is frozen behind the p2/contract tag.
// The render-state CSO cache (ARCHITECTURE.md §2.3, §5.3). EmitRenderState calls Acquire only
// when the pipeline-state version moved, so the steady state does no hashing. Acquire hashes the
// 396 pipeline bytes, probes, and confirms a hit with a memcmp: a bare 64-bit match could alias
// two render states onto one CSO, which is silently wrong pixels. A miss mints a slot and emits
// create_render_state with every pipeline chunk. 64 entries, LRU, about 26 KB per context; the
// counters give the hit rate. kMGPipeBehaviourNoCsoContentAddressing (bit 63 of
// MOBILEGL_PIPE_PUSH) disables reuse as a negative control, which CsoContentAddressingScenario
// keeps exercised.
#include <Config.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
+15 -50
View File
@@ -9,22 +9,9 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's framebuffer family: set_framebuffer_state, emitted at the validate
// point once per bound TARGET that moved, or once with Target = Both when the two bindings
// name the same object.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT, and the
// split is the whole reason it exists this early. MG_Impl/Pipe/PipeFill.cpp is the contract
// package's for the entire phase - it carries Coverage.def's enum-coupled block, the validate
// point and the death helpers - so the emitter package must not edit it. What it edits instead
// is this header: the emitter's BODY, and the value of kMGPipeWiredFramebufferSubsystem below.
// That is what makes "no file is touched twice by two packages" structural rather than a
// convention, and it is what the bb2a236d semantic-merge trap taught (two branches green
// separately, the integrated tree not compiling).
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state: the root
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
// Client side of the framebuffer family: set_framebuffer_state, emitted at the validate point
// once per bound target that moved, or once with Target = Both when both bindings name the
// same object, and before a DSA call that names a framebuffer (EmitFramebufferByName).
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/TextureEmit.h>
@@ -179,24 +166,13 @@ namespace MobileGL::MG_Pipe {
}
// ---------------------------------------------------------------------------------
// D-C4: ContentHash, and the one input it must not swallow
// ContentHash
// ---------------------------------------------------------------------------------
//
// XXH64 over the WHOLE record with ContentHash itself zeroed, computed field-wise into a
// zero-initialised staging copy so that no padding byte can enter the hash. Two jobs: the
// server's render-pass memo key, and this client's emission suppressor.
//
// IT MUST COVER Fbo. A recycled framebuffer handle whose successor happens to carry an
// identical attachment set would otherwise be suppressed against its predecessor; Fbo
// carries Gen, so it cannot be.
//
// IT MUST COVER DrawBuffers[8], and this is the trap worth naming. The backend derives the
// fragColor BROADCAST COUNT from the draw-buffer array, and it does that at the verb, from
// the framebuffer state it then holds, precisely so a program can relink inside the same
// draw. A hash that did not cover the array would let a suppressed set_framebuffer_state
// mean "the draw buffers did not move" when they had, and the shader would be specialised
// for the previous output shape. With the array in the hash, a suppression provably means
// the array did not move, which provably means the broadcast count did not move.
// XXH64 over the whole record with ContentHash zeroed, copied field by field into a zeroed
// staging record so padding never enters it. It keys the server's render-pass memo and this
// client's suppressor, so it must cover Fbo (its Gen separates a recycled handle with the
// same attachments) and DrawBuffers (the backend derives the fragColor broadcast count from
// them at the verb, so a suppressed record must mean that count did not move).
inline void MGPipeCopySurfaceForHash(MGPSurface& dst, const MGPSurface& src) {
dst.Res = src.Res;
dst.InternalFormat = src.InternalFormat;
@@ -319,23 +295,12 @@ namespace MobileGL::MG_Pipe {
return bytes;
}
// ID-19(c): EVERY DSA ENTRY POINT THAT HANDS A FRAMEBUFFER TO THE SERVER BY NAME IS
// PRECEDED BY A RECORD FOR IT, and that is the phase's main correction rather than a
// nicety. With only the two BOUND-target records, glClearNamedFramebufferfv(fbo) on an
// unbound fbo made the backend mint a fresh driver framebuffer with NO ATTACHMENTS,
// find no record for it, decline, and issue the clear against it anyway -
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
// correctly (esprytobj C-1).
//
// THE TARGET IS Named ONLY WHEN THE OBJECT IS BOUND TO NEITHER BINDING. A record always
// writes FramebufferRecords[Fbo.Slot]; Draw/Read/Both ADDITIONALLY set the bound
// handle(s). So handing a currently-bound framebuffer a Named record would overwrite
// the bound record's Target with one that says "no binding" while BoundFramebuffer
// still names it, and the server would read a record whose Target contradicts the
// binding it is resolved through. Re-asserting the binding the object already has is
// free (the content hash suppresses it) and keeps the two consistent.
//
// Returns the bytes that went on the wire.
// Emits a record for `fbo` before a DSA call hands it to the server by name; without
// it, a named clear of an unbound framebuffer would find no record and run against an
// empty driver framebuffer. Target is Named only when `fbo` is bound to neither binding:
// every record writes FramebufferRecords[Fbo.Slot], and a Named record for a bound
// object would leave a Target that contradicts BoundFramebuffer. Re-asserting the
// existing binding is suppressed by the content hash. Returns the bytes sent.
Uint64 EmitFramebufferByName(const FramebufferObject& fbo) {
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
MGPipeFramebufferTarget target = MGPipeFramebufferTarget::Named;
+15 -49
View File
@@ -9,29 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of set_shader_images, the third of P4a's kVarTail unit sets. It rides
// SamplerEmit.h's subsystem bit (kMGPipeWiredSamplerSubsystem): one family, one A/B.
//
// TWO INVARIANTS THAT MUST SURVIVE INTO THE BODY, and they are the kind an optimisation
// deletes:
// 1. THE HIGH-WATER-ZERO EARLY-OUT. An image high-water mark of 0 emits nothing, BEFORE any
// hash - that is what makes every Minecraft draw pay one integer test for a feature it
// does not use.
// 2. THE SWEEP'S GATE IS KEYED ON FRONTEND GENERATIONS AND DELIBERATELY NOT ON A BACKEND
// RE-MINT COUNTER. A texture bound ONLY to an image unit is re-minted INSIDE the sweep,
// so a server-side epoch would be bumped after the gate had already declined. The
// client's bit-14 shutter is Mix(Mix(textureContent, textureParams), programImageUnitVersion)
// - all three FRONTEND counters - so the property is preserved by construction, and it is
// written here because it is invisible from the shutter itself.
//
// The record carries the APPLICATION's format and access; the bind-format recast (a GL_RG32F
// bind is INVALID_VALUE on 19 of 26 non-core formats on Adreno) and the buffer-texture split
// view stay SERVER-side and unchanged. ContentHash therefore has to cover InternalFormat and
// Access as well as the binding, because the format the shader was built against is live
// glBindImageTexture state and the format-less image bake keys on it.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
// Client side of set_shader_images, under SamplerEmit.h's subsystem bit (one family, one A/B).
// Two invariants an optimisation must keep. An image window of 0 emits nothing before any hash,
// so a draw that uses no images pays one integer test. The shutter gating the sweep mixes only
// frontend counters (texture content and params, program image units, texture binds), never a
// backend re-mint counter: a texture bound only to an image unit is re-minted inside the sweep,
// after the gate has decided. The record carries the application's format and access; the
// bind-format recast and buffer-texture split view stay on the server, so the content hash
// covers InternalFormat and Access too.
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -118,32 +103,13 @@ namespace MobileGL::MG_Pipe {
public:
using GLContext = MG_State::GLState::GLContext;
// set_shader_images. Start is 0 and Count is the image-unit window described below.
//
// WHERE THE HIGH-WATER MARK COMES FROM, because this is the one place a reader will look
// for it. DirectGLES keeps g_imageUnitHighWaterMark, but that is written from inside its
// own per-unit sync and lives on the far side of the boundary.
//
// AND IT IS STILL NOT TextureState's MARK, EVEN THOUGH ONE NOW EXISTS. P5d round 3
// (package C) added TextureState::NoteImageUnitTouched - a push-build-only image-unit
// high-water mark, fed by glBindImageTexture - for the split client's per-draw
// GPU-write sweep. That mark answers "which units could hold a binding"; the window
// below answers the strictly narrower "which units could a SHADER READ", which is what
// makes the zero early-out fire for an application that binds an image no shader names.
// Swapping one for the other would widen this emitter for no record's benefit, and the
// paragraph that used to stand here - "adding a counter to TextureState would resize the
// pull build's object, which G1 forbids outright" - is answered by that mark being
// compiled only into push builds, where the object is already a different size.
//
// So the window is derived instead, from the one thing that decides whether an image
// unit can matter at all: the highest image unit the CURRENT PROGRAM names, memoised
// per program state in SamplerEmit.h's shared inversion, UNIONED with a sticky mark of
// every unit this emitter has already described. A program with no image uniforms
// gives MaxImageUnit == -1 and, with nothing sticky yet, a window of 0 - which is the
// zero early-out, taken BEFORE any hash and before any 192-entry walk, exactly as
// property 1 requires. The mark is sticky so that a program which stops naming a unit
// does not silently stop describing it: the window only grows, and shrinking it is how
// a stale binding would become invisible to the server.
// set_shader_images. Start is 0; Count is the image-unit window: the highest image unit
// the current draw program names (memoised in SamplerEmit.h's program inversion),
// unioned with a sticky mark of every unit already described. TextureState's
// touched-unit mark is not used: it counts units holding a binding, not units a shader
// reads, and would defeat the zero early-out for an application that binds an image no
// shader names. The window only grows, so a unit a later program stops naming is still
// described and a stale binding stays visible to the server.
Uint64 EmitShaderImages(GLContext& ctx) {
const auto& program = ctx.GetProgramForDraw();
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(program.get());
File diff suppressed because it is too large Load Diff
+23 -75
View File
@@ -47,86 +47,34 @@ namespace MobileGL::MG_Pipe {
// stop where it says it stops (MG_Test/ScopedPipeVerb.h).
void MGPipeLeaveVerb();
// PipeFill.cpp. DOES THIS BUILD, ON THIS BACKEND, AT THIS MASK, EMIT FOR THIS P4a FAMILY?
// (ID-39, widened by S-3 / ID-41.) The four conjuncts are the operator's per-subsystem bit
// in MOBILEGL_PIPE_PUSH, the family's own kMGPipeWired*Subsystem constant (`wired`, which
// the caller passes because it lives in the family's emit header and this header may not
// include one), and - for the four families P4a migrates - a backend having registered
// MGPipeResourceOps (the same per-backend signal `MGPipeResourceSubsystemEnabled()` has
// applied to P3a's buffers since the phase began) and every D-K2 dependency bit of the
// family being set in the same mask.
//
// THE LAST TWO CONJUNCTS ARE THE ONES THIS DECLARATION EXISTS FOR, and they are the same
// defect twice. Magma (DirectVulkan) registers no table and has no P4a twins; at a mask like
// 0x7ff Espryt REFUSES the texture family server-side because D-K2's fourth row says bit 10
// requires bit 11. In both cases the client emitted anyway, the applier accepted, the
// emitters cleared their per-level dirty flags on that acceptance, and the legacy upload
// path that still owed those texels found nothing to upload (66 DirectVulkan cases at ID-39,
// 47 DirectGLES cases at ID-41). With them the four families emit NOTHING in that state and
// the legacy pull path runs exactly as it does on a pull build.
//
// D-K2's ROWS ARE IN MG_Pipe/SubsystemDeps.def, ONCE (P3b/P4b R-5), and this comment no
// longer restates them - restating them here is half of how the rule ended up with six
// statements, two of which were wrong. PipeFill.cpp's kMGPipeP4aFamilyDependencies is still
// the client's own copy and is still the bit-for-bit mirror of the four
// `Resolve<Family>SubsystemArm()` refusals in DirectGLES/Managers.cpp; switching it to read
// the .def is the integrator's one-line change (PipeFill.cpp is the contract package's file
// for the whole phase, so the emitter packages do not edit it).
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp drives BOTH readers at every interesting
// mask and compares them against the .def, so the two cannot drift in the meantime.
//
// It is exported for the unit gate and for no other caller: the gate itself is
// FamilyIsLive() inside PipeFill.cpp, every birth hook and every `wants()` row resolves
// through it, and this returns that same expression rather than a second copy of it.
// PipeFill.cpp. Whether this build, backend and MOBILEGL_PIPE_PUSH mask emit for `subsystem`:
// the FamilyIsLive() expression every birth hook and `wants()` row uses, exported only for
// unit tests. It needs the subsystem bit, the family's kMGPipeWired*Subsystem constant
// (`wired`; this header cannot include the emit headers), a backend consumer for the family
// and its dependency bits from MG_Pipe/SubsystemDeps.def. Without the last two the texture
// emitters clear dirty levels on acceptance that nothing then uploads.
// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp checks this and Espryt's
// Resolve<Family>SubsystemArm() refusals against the .def.
Bool MGPipeP4aFamilyEmits(Uint64 subsystem, Uint64 wired);
// PipeFill.cpp. DOES AN EMITTED CALL SUPPLY THIS FIELD at the environment named, i.e. would
// the validate point's residual fill SKIP it? This is step 4's own predicate, which P5d
// round 3 (package C) turned from a per-field-per-verb conjunction into a memo keyed on the
// three arguments plus the P4a consumer signal it reads for itself - the profile had it at
// 63 CapsMirror reads per verb for one answer.
//
// IT IS EXPORTED FOR THE UNIT GATE AND FOR NO OTHER CALLER, for MGPipeP4aFamilyEmits'
// reason and one of its own: which fields the fill copies has NO other observable, because
// in a push build every emission this predicate asks about is routed straight into the
// applier, which writes the same storage the fill would have written. So the only way to
// state "the memo's key is complete" - the one thing a memo can get wrong that the
// expression it replaced could not - is to ask it directly. A call re-keys the memo, which
// is exactly what the case is for.
//
// AND A STALE ANSWER IS SILENT, WHICH IS WHY THAT CASE IS THE ONLY GUARD. It is tempting to
// say a wrongly-skipped field aborts as Fatal{UnmigratedPipeInput}; it does not. The walk
// stamps FilledGen from the verb serial whether or not it copied (PipeFill.cpp, step 4), so
// a field the fill skips reads FRESH with the PREVIOUS verb's value - a stale binding slot
// rendered without a word, caught only by the verify lane's comparison. Poison catches an
// UNSTAMPED read, not a stamped-but-uncopied one.
// PipeFill.cpp. Whether an emitted call supplies `field` in this environment, i.e. whether
// the residual fill (step 4) skips it. Memoized on the last three arguments plus what it reads
// itself (the consumer signal, the caps-mirror generation); uncached it cost 63 caps-mirror
// reads per verb. Exported only for unit tests, the sole check that the memo key is complete,
// since emissions write the same storage the fill would. A wrong answer is silent: step 4
// stamps FilledGen whether or not it copied, so a skipped field reads as fresh with the
// previous verb's value. Poison only catches unstamped reads; the verify comparison is the
// other net.
Bool MGPipeResidualFillSuppliesField(MGPipeInputField field, Uint64 pushMask, Bool applierDerives,
Bool contextValuesWireLive);
// PipeFill.cpp. P3a D-H2.1: the DRAW's raw vertex-fetch base instance, which
// set_vertex_buffers now carries as an explicit field.
//
// It replaces an ambient process global the backend read at VAO sync time, which is a
// shape that cannot cross a pushed boundary. The client sends the raw value and never a
// pre-shifted offset: whether to emulate the fetch shift or let GL_EXT_base_instance do
// it is the SERVER's decision. It is also an input to set_vertex_buffers' content hash
// and to the tracker's bit-9 shutter, so a draw whose only change is its base instance
// still reaches the emitter and still goes out.
//
// DO NOT CALL IT DIRECTLY FROM A GL ENTRY POINT - use MGP_SET_BASE_INSTANCE below. This
// whole declaration block is inside #if MOBILEGL_PIPE_PUSH, so a bare call would not even
// compile in a pull build, and the three call sites are in a file that is compiled in
// both. The macro is the same shape MGP_FILL already has, for the same reason.
//
// The validate point consumes and clears it - on both of its exits - and MGPipeLeaveVerb
// clears it too, so a plain draw that follows a base-instanced one sees 0 again. The
// tracker's Reset() deliberately does NOT clear it (Tracker.h): a make-current happens
// BETWEEN the setter and the fill that reads it.
//
// The three GL entry points that make this call (ID-10's grant) are
// MG_Impl/GLImpl/Drawing/GL_Drawing.cpp's DrawElementsInstancedBaseVertexBaseInstance,
// DrawElementsInstancedBaseInstance and DrawArraysInstancedBaseInstance - one line each,
// immediately above the MGP_FILL, carrying the RAW baseinstance argument.
// PipeFill.cpp. The draw's raw vertex-fetch base instance for the next set_vertex_buffers,
// which carries it explicitly; the server decides whether to emulate the fetch shift or use
// GL_EXT_base_instance. It also feeds that record's content hash and the tracker's bit-9
// shutter, so a draw that changes only its base instance still goes out. GL entry points
// call it through MGP_SET_BASE_INSTANCE, one line above MGP_FILL in GL_Drawing.cpp's three
// *BaseInstance draws. The validate point clears it on both exits; the tracker's Reset()
// must not, because Update() resets on a context switch, after the setter and before use.
void MGPipeSetPendingBaseInstance(Uint32 baseInstance);
// What the next set_vertex_buffers will carry. The unit gate reads it to pin that a
// make-current between the setter and the fill does not eat it
+24 -64
View File
@@ -9,32 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's program family: create/bind/delete_shader_state,
// set_draw_program, set_dispatch_program and set_global_constants.
//
// WHERE create_shader_state IS EMITTED FROM, and why it is not the tracker's business: the
// tracker's bit-6 shutter reads GetCurrentProgram() and DELIBERATELY NOT GetProgramForDraw(),
// because the tracker must not force a compile just to answer "did the shader move". So the
// tracker keeps its shutter and the EMITTER joins - from the same GetProgramForDraw() /
// GetProgramForDispatch() call the verb is about to make anyway, so no join happens that would
// not have happened. Emitting from the compile pool's terminal continuation is a real
// asynchronous win and is a LATER phase's: in monolith the applier is one function call away,
// so it is unmeasurable here.
//
// WHAT THE SERVER STILL SPECIALISES, so nobody reads create_shader_state as self-contained
// and produces a per-draw rebuild: the draw-FBO clamp masks, the fragColor broadcast count,
// the storage-block binding signature, the atomic-counter set, the live image formats and the
// patch parameters are all inputs a backend program depends on BEYOND the artefacts. This call
// publishes the ARTEFACTS; the server specialises at the verb from the state it holds. The
// clause count does not shrink - its inputs move.
//
// THE ARTEFACTS DO NOT TRAVEL IN MONOLITH. All seven of MGPProgramDesc's blob refs are
// declared with Size 0 and the LinkArtifacts / SpirvArtifacts ride beside the record through
// MGPipeApplyCreateShaderState's companion pointers, so the codec is never called on the hot
// path; the verify build is where it is exercised.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
// Client side of the program family: create/bind/delete_shader_state, set_draw_program,
// set_dispatch_program and set_global_constants. create_shader_state is emitted from the
// GetProgramForDraw() / GetProgramForDispatch() join the verb makes anyway; the tracker's
// shutter reads GetCurrentProgram() so asking "did the shader move" never forces a compile.
// The record carries the artifacts only; the server specialises at the verb for the draw-FBO
// clamp masks, fragColor broadcast, storage-block bindings, atomic counters, image formats and
// patch parameters (ARCHITECTURE.md §7). In monolith the artifacts ride beside the record as
// companion pointers with Size 0 blob refs, so the codec runs only in the verify build.
#include <MG_Impl/Pipe/CompositeResolver.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -72,24 +54,14 @@ namespace MobileGL::MG_Pipe {
return version != kMGPipeGlobalConstantsNeverUploaded;
}
// A program's identity for the wire, out of the SNAPSHOT the last link consumed and never
// out of the live attach list: glAttachShader and glCompileShader take effect only at the
// NEXT link and neither moves m_linkVersion, so a stage mask built from GetAttachedShaders
// would describe a program that does not exist yet. GetLinkedShaderStages() is also what
// indexes GetGeneratedSpirv(), so the two halves of this descriptor are guaranteed to agree
// by construction rather than by care.
// P5e (pg). ONE ENTRY OF THE STORAGE-OVERRIDE SIGNATURE, and it is a VERBATIM copy of the
// mix in `MG_Backend/DirectGLES/Managers.cpp`'s ComputeShaderStorageBlockBindingSignature -
// the function whose answer this record replaces on the handle arm. The two halves cannot
// share one definition today because MG_Backend includes nothing from MG_Impl (checked at
// this head), so they are twins with a named pointer at each other and a unit case in
// ProgramEmitTest that pins the number. Order-independent by construction: the source is an
// UnorderedMap, the combine is commutative, and the entry mixes name and binding so two
// entries cannot trade halves and cancel out.
//
// OVER THE VALUES, NOT OVER A CHANGE COUNTER, which is the property the pipeline composite's
// uniform mirror depends on: it re-sets every override every draw, and re-setting a block to
// the binding it already carries must produce the same signature and force no rebuild.
// One entry of the storage-override signature (MGPProgramBindings::Signature), the one
// number the server stamps on a program twin and compares on the draw path (DirectGLES'
// ComputeShaderStorageBlockBindingSignature takes it verbatim). The entry mixes name with
// binding so two entries cannot trade halves; entries are summed, so the UnorderedMap's order
// cannot matter. Over values, not a change counter: re-setting a block to its current binding
// (the pipeline composite's uniform mirror does so every draw) forces no rebuild.
// MGPipeStageMaskOf below reads the last link's stages, not the attach list: attach and
// compile take effect only at the next link, and the same stages index GetGeneratedSpirv().
inline Uint64 MGPipeStorageOverrideSignatureEntry(const String& blockName, Int binding) {
Uint64 entry = std::hash<String>{}(blockName);
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
@@ -281,26 +253,14 @@ namespace MobileGL::MG_Pipe {
return bytes + sizeof(MGPGlobalConstants) + size;
}
// ---- P5e (pg): set_program_bindings, opcode 80 -----------------------------------
//
// THE THREE POST-LINK MUTABLE REFLECTION FIELDS, and why an archive alone is not an
// answer. `uniformBlockBinding[i]`, `uniformSamplerOrImageUnitIndex[loc]` and the
// name-keyed `shaderStorageBlockBinding` map are all MEMBERS OF LinkArtifacts - so the
// archive create_shader_state carries does hold them - but glUniformBlockBinding,
// glUniform1i on a sampler uniform and glShaderStorageBlockBinding all move them AFTER
// the link that produced that archive, without relinking. A server answering a draw
// from the archive alone would bind the uniform blocks the program was LINKED with
// rather than the ones it is BOUND with. This record is the delta carrier; the applier
// overlays it on the archive, and a re-issued create drops it because the indices no
// longer mean anything.
//
// LATCHED ON THE TWO COUNTERS THAT MOVE WHEN ANY OF THE THREE DOES, plus the handle:
// m_backendStateVersion (glUniformBlockBinding and glUniform1i both bump it) and
// m_blockBindingVersion (glUniformBlockBinding and glShaderStorageBlockBinding both
// bump it, and the storage setter deliberately bumps ONLY it). Neither setter moves a
// counter on an unchanged value - both have an equality bail-out - so an application
// that re-sets the same bindings every frame emits nothing, which is what the pipeline
// composite's uniform mirror needs (it replays every override per draw).
// ---- set_program_bindings ----
// glUniformBlockBinding, glUniform1i on a sampler uniform and glShaderStorageBlockBinding
// change LinkArtifacts' block bindings, sampler/image unit indices and storage-block map
// after the link that produced the archive, without relinking. This record carries those
// values; the applier overlays it on the archive, and a re-issued create drops it.
// Latched on the handle plus m_backendStateVersion and m_blockBindingVersion, which
// together move on any of the three setters but not on an unchanged value, so re-setting
// the same bindings every draw (the pipeline composite's uniform mirror) emits nothing.
Uint64 EmitProgramBindings(const ProgramObject& program, MGPipeHandle cso) {
if (MGPipeHandleIsNull(cso)) return 0;
const Uint32 backendStateVersion = program.GetBackendStateVersion();
+37 -126
View File
@@ -9,35 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P3a's resource family (brief D-A, D-B, D-C, D-D).
//
// WHERE IT RUNS, and it is the ONE exception to push-at-validate (ARCHITECTURE.md 5.1):
// the seven BufferBackendOps hooks already dispatch at the GL call that causes them, so
// their pipe calls are emitted from the same BufferObject dispatchers - not from
// MGPipeValidateForVerb. Nothing about buffers moves to validate time in P3a.
//
// WHAT LIVES HERE
// * the sticky BindMask, one constexpr BufferTarget -> bit table with a static_assert
// that it covers every enumerator, so a new target cannot be silently unmapped;
// * the lifetimeId -> {slot, gen} mint (through MGPipeSlots(), the one allocator) and
// the slot -> BufferObject* INVERSE the reverse channel resolves a writeback through;
// * the nine MGPipeEmitResource* bodies, declared in MG_Pipe/PipeMutation.h so that
// MG_State sees a declaration and never this file (the same layering PipeMutation.h
// already has for MGP_NOTE_MUTATION: declare in MG_Pipe, define in MG_Impl);
// * the MGPSubData range splitter, because one record's box caps the destination at a
// 2^31-1 offset and a 2^32-1 size;
// * the map-persistent-roundtrips counting site.
//
// HEADER-ONLY, for the ownership reason Tracker.h states in full: the root CMakeLists.txt
// that would name a new .cpp belongs to the contract package and is frozen behind the tag.
// MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
//
// NO TIMER, and no per-call record copy on a HOT path. The two observables a unit case
// needs - the last emitted descriptor and the per-call counts - are written only by
// resource_create and resource_respecify, which run once per glBufferData rather than per
// upload; resource_subdata, the hot one, is observed through the pure builders below
// instead (MGPipeBuildSubDataRecord / MGPipeForEachSubDataRecordRange), which is also what
// lets a test drive the splitter at both of its bounds without a 4 GiB buffer.
// Client side of the buffer resource family. Its records go out at the GL call that causes the
// change, from the BufferObject dispatchers that already run the BufferBackendOps hooks, not at
// the validate point (ARCHITECTURE.md §5.1). This file holds the sticky BindMask table, the pure
// record builders and the MGPSubData range splitter, the lifetime id -> {slot, gen} mint, the
// slot -> BufferObject* inverse that writebacks resolve through, and the reverse-channel
// consumers. The MGPipeEmitResource* bodies are in PipeFill.cpp and declared in
// MG_Pipe/PipeMutation.h, so MG_State never includes this file. resource_subdata, the hot path,
// records no test observables; tests drive MGPipeBuildSubDataRecord and the splitter directly.
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
@@ -195,24 +174,13 @@ namespace MobileGL::MG_Pipe {
return desc;
}
// The buffer half of MGPSubData: the destination range rides in the box's first
// coordinate and first extent, and MGPipeSetSubDataBufferRange is the ONLY spelling of
// that convention. Returns false, with the record untouched, when the range does not fit
// one record - which is where MGPipeForEachSubDataRecordRange comes in.
//
// `sourceIsVerbatimLevelShadow` is the record's own question - "are these bytes an
// untransformed level shadow?" - and it is a PARAMETER because the answer differs by
// caller: resource_subdata hands over the client's own shadow at an offset into it and
// says yes; buffer_subdata_resident hands over the application's staging store, or the
// locally expanded pattern FillSubData built, and both say no. Nothing reads it on the
// buffer path today, which is exactly why it must not be a hard-coded 1 that becomes
// wrong the moment something does.
//
// Blob is FILLED, exactly: Seg is kMGHostSpanSegNone (monolith - the bytes travel beside
// the record through the entry point's companion pointer) and Size is the piece's own
// byte length, which is what the applier's ONE Blob rule holds a non-zero declaration to
// (PipeApply.cpp's SubDataBoxFault: != 0 && != MGPipeSubDataBufferSize is refused).
// Leaving it 0 would be legal too; declaring it correctly is the stronger of the two.
// The buffer half of MGPSubData: the destination range rides in the box's first coordinate
// and extent, written only through MGPipeSetSubDataBufferRange. Returns false when the range
// does not fit one record (see MGPipeForEachSubDataRecordRange). The verbatim-shadow flag is
// a parameter because callers differ: resource_subdata passes the client's own shadow (true),
// buffer_subdata_resident a staging store or a FillSubData pattern (false). Blob.Seg is
// kMGHostSpanSegNone (the bytes travel through the entry point's companion pointer) and
// Blob.Size is the piece's byte length, the only non-zero size SubDataBoxFault accepts.
inline Bool MGPipeBuildSubDataRecord(MGPipeHandle res, Uint64 offset, Uint64 size, MGPSubData& out,
Bool sourceIsVerbatimLevelShadow) {
out = MGPSubData{};
@@ -372,37 +340,14 @@ namespace MobileGL::MG_Pipe {
m_bySlot[slot].BindMask |= static_cast<Uint16>(MGPipeBindMaskForBufferTarget(target));
}
// Accumulates into the sticky mask every target `buffer` is bound to RIGHT NOW, and
// returns the accumulated value.
//
// [DEVIATION, recorded in client-v2.md] D-A3 asks for the OR at every glBindBuffer /
// glBindBufferBase / glBindBufferRange / VAO element-slot bind, and C.1 points at
// MG_State/GLState/BufferState/BufferState.{h,cpp} for it - a file this package DOES
// own. The brief is wrong about where the entry points are: BufferState only VENDS
// BindingSlot<BufferObject>& / BindingSlotRange1D&, and the .Bind() calls are
// MG_Impl/GLImpl/Buffer/GL_Buffer.cpp's (BindBuffer_State, BindBufferBase_State,
// BindBufferRange_State), which C.5 assigns to no package. So the mask is accumulated
// by SAMPLING the frontend's live binding state instead - here, at every create and
// respecify, which is where the value is PUBLISHED - and ORed into a per-slot sticky
// field that is never cleared.
//
// WHAT SAMPLING ALONE CANNOT SEE is not "a bind after the last respecify" (which the
// specified design misses too) but a TRANSIENT bind: bind an EBO, draw, unbind, then
// define it through DSA - the respecify's sample sees no binding at all, and the DSA
// idiom makes that the common case rather than a corner (TryAdoptLargeStorage's own
// comment names glNamedBufferSubData as what MC 26.3 streams with). That hole is
// closed for the two bits anything keys on by NoteBoundAs above, called from
// EmitVertexBuffers / EmitIndexBuffer at every draw. What is left unpublished is a
// buffer that is bound, never drawn from, and never re-specified afterwards; the
// remaining fix is one line in each of GL_Buffer.cpp's three *_State binders, for the
// seven bits nothing keys on yet, and it stays handed to whoever owns that file.
//
// The scan is skipped unless a binding-slot version moved since the last one, which
// is one Uint16 load per global target and none per binding point. It is NOT called
// from the content emitters, deliberately: it walks the whole context's binding state
// and writes the tracker, and one of those emitters (resource_subdata) is on the path
// D-A2 preserves as reachable off the render thread. Extra sampling could only widen
// a sticky union, but not at the price of a context-wide read from the wrong thread.
// ORs into the sticky mask every target `buffer` is bound to right now and returns the
// result. The bind entry points (GL_Buffer.cpp's *_State binders) do not record targets,
// so the mask is sampled here, at create and respecify where it is published. Sampling
// misses a transient bind (bind, draw, unbind, then define through DSA); NoteBoundAs at
// every draw covers the vertex and element-array bits, but other bits of a buffer never
// drawn from or respecified after its bind stay unpublished. The scan runs only when
// BindEpoch moved, and never from the content emitters: it reads the whole context, and
// resource_subdata can run off the render thread.
Uint16 RefreshBindMask(GLContext& ctx, const BufferObject& buffer, MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (slot >= m_bySlot.size()) return 0;
@@ -461,34 +406,13 @@ namespace MobileGL::MG_Pipe {
void NoteDestroy() { ++m_destroys; }
void NoteMapPersistent() { ++m_mapPersistents; }
// A unit fixture's per-case reset, and the library never calls it. THE RULE, stated
// rather than left as an absence, because "nothing resets this" is not a reason:
//
// A buffer handle and the applier record it names are SHARE-GROUP OBJECT STATE.
// A GL object lives in a share group, not in a context, so a make-current changes
// neither. The applier's MGPipeApplierReset() is a make-current and deliberately
// keeps its Resources / VertexElementsCsos (PipeApply.h says so beside them); the
// ONLY things that drop a record are the object's own death signal -
// resource_destroy, which ~BufferObject raises through
// MGPipeEmitResourceDestroyAndFree, and delete_vertex_elements - and
// MGPipeApplierReleaseObjectRecords(), which is the SERVED CONTEXT's teardown and
// is deliberately wired to nothing in the monolith (there is one applier behind
// every context, so calling it on one context's destruction would drop every other
// context's records).
//
// So this tracker needs no re-publication path on a fresh context and must not have
// one: re-emitting resource_create for a record the applier still holds would move
// its Serial for nothing. What the client owes instead is the destroy - which
// ~BufferObject already emits, in the fixed emit-then-free order (D-L) - and that is
// the whole of the client's side of the record lifecycle.
//
// The vertex-input emitter's latches are the OTHER half and are genuinely per
// context: MGPipeVertexInputEmitter::Reset() is called from the FreshlyPrimed arm
// because the applier's vertex-input WORKING state (the bound handle, the window, the
// fetch shift) IS cleared there. Its vertex-elements RECORDS are not, which is why
// the emitter's Reset drops the "already published" latches but no create is lost:
// the latch is what says "re-publish", and re-publishing an unchanged configuration
// is a bounded over-fire, not a dropped write.
// Per-case reset for unit fixtures; the library never calls it. Buffer handles and the
// applier records they name are share-group state: MGPipeApplierReset (a make-current)
// keeps them, and only the object's own death (resource_destroy from ~BufferObject,
// delete_vertex_elements) or MGPipeApplierReleaseObjectRecords drops one. So this tracker
// has no re-publication path on a fresh context; a second resource_create would move the
// record's Serial for nothing. Per-context latches live in the emitters instead, such as
// MGPipeVertexInputEmitter::Reset(), which the fresh-context path calls.
void ResetForTest() {
m_bySlot.clear();
m_bindEpoch = 0;
@@ -569,26 +493,13 @@ namespace MobileGL::MG_Pipe {
// D-D: the client's half of the reverse channel
// ---------------------------------------------------------------------------------
// The backend produced the bytes of a readback and hands them back through the channel.
// The client resolves the handle to its own object and writes the shadow; the epoch bump
// stays SERVER-side and happens AFTER this returns, never before (ARCHITECTURE.md 7.4:
// the reverse channel needs the same ordering guarantee as the forward one).
//
// THE BLOBREF'S THREE ARMS (CONTRACT-P5C §4.3, replacing the P3a monolith guard that
// rejected every Seg != kMGHostSpanSegNone and would have dropped every writeback EVENT
// on arrival):
// Seg == kSegEvent -> the wire shape (CONTRACT-P5C §1's reverse-channel row):
// Offset is the byte offset of the inline payload inside
// SEG_EVENT, resolved through the client session's OWN
// SegmentTable, bounds-checked against the announced size;
// Seg == kMGHostSpanSegNone -> monolith only: Offset IS the backend's mapped address
// (MGPipeTypes.h says so in as many words). With an active
// transport this is rule B in the reverse direction and is
// Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"};
// anything else -> the same Fatal.
//
// `producedAtSeq` is the event's ProducedAtSeq (the record the server read the bytes back
// at); 0 for the monolith callback, whose answer is synchronous with its request.
// Writes the bytes of a backend readback into the client's shadow of the buffer `res` names.
// The server bumps its epoch only after this returns (ARCHITECTURE.md §8.2). With
// Seg == kSegEvent, `bytes` is an offset into SEG_EVENT, resolved and bounds-checked through
// the client session; with Seg == kMGHostSpanSegNone it is a backend address, legal only in
// the monolith. Anything else, or a host address under an active transport, is
// Fatal{ProtocolCorruption} (a build without a transport drops it with an error).
// `producedAtSeq` is the record the server read the bytes back at, 0 for the monolith.
inline void MGPipeClientOnBufferWritebackAt(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes,
Uint64 producedAtSeq) {
auto* buffer = MGPipeResourceTrackerInstance().Resolve(res);
+57 -220
View File
@@ -9,34 +9,13 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's sampler family: the content-addressed sampler CSO cache, the
// identity-addressed sampler view per texture object, and the two unit sets
// set_sampler_views and bind_sampler_states. The third unit set, set_shader_images, is
// ImageEmit.h's - the same subsystem bit, a different resolution.
//
// TWO THINGS THIS FILE OWNS THAT ARE EASY TO GET WRONG, both stated where the body will go:
// * SamplerParameters is 100 bytes with THREE BYTES OF TRAILING PADDING, so the CSO cache
// hashes and memcmp-confirms over a ZERO-INITIALISED canonical copy built field by field,
// never over the object's own bytes. Without that the 256-entry cache's hit rate is zero
// and nobody notices, because the pixels are right.
// * every emission goes through a VERSION-FIRST SKIP before it hashes anything: the sampler
// view latches (params version, shape version) per handle, and the two sets latch their
// SetHashSuppressor slots. A 192-entry walk per verb without a latch is not affordable.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full. kMGPipeWiredSamplerSubsystem below covers this file AND
// ImageEmit.h: the three unit sets, the sampler CSO and the sampler view are ONE family and
// one subsystem bit, because an operator switching samplers off has to get the whole family's
// legacy arm rather than two thirds of it.
//
// WHAT THIS FILE IS THE CLIENT HALF OF, named so a reader can check it against the oracle:
// DirectGLES' ResolveAndBindUnitTextures (the per-unit sampler-view resolution),
// BindCurrentUnitSamplers (the per-unit sampler-object walk) and the program pass' sampler
// override. TWO BACKEND POST-PROCESSINGS DELIBERATELY STAY ON THE SERVER and act on the
// RESOLVED set: Espryt's raw-depth-fetch sampler substitution and Magma's feedback-loop
// detection. Neither is reproduced here and neither may be.
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
// Client side of the sampler family: the content-addressed sampler CSO cache, one sampler view
// per texture object (addressed by identity), and the unit sets set_sampler_views and
// bind_sampler_states. set_shader_images is ImageEmit.h's, under the same subsystem bit.
// It is the client half of DirectGLES' ResolveAndBindUnitTextures, BindCurrentUnitSamplers
// and the program pass' sampler override (ARCHITECTURE.md §5.5). Espryt's raw-depth-fetch
// sampler substitution and Magma's feedback-loop detection act on the resolved set and stay on
// the server. Every emitter checks version latches before hashing anything.
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -59,60 +38,23 @@
namespace MobileGL::MG_Pipe {
// WIRED. The sampler CSO, the sampler view and all three unit sets - set_shader_images
// included, whose emitter lives in ImageEmit.h - have bodies, so this file's family
// contributes its bit to kMGPipeWiredSubsystems. ONE bit for the whole family, because an
// operator switching samplers off has to get the whole family's legacy arm rather than two
// thirds of it.
//
// WHAT THE BIT DOES, and it really is a gate - c0b changed this and the note that used to
// say the opposite was true only against the contract commit. The validate point's
// `wants()` (PipeFill.cpp) now asks FOUR things: the bit maps to a subsystem, the
// operator's MOBILEGL_PIPE_PUSH mask carries it, `kMGPipeWiredSubsystems` carries it -
// i.e. this constant is non-zero - and the dirty bit fired. The birth hooks' own gate
// (`FamilyIsLive`) is the same pair one level in, so the client paths and the validate
// point cannot disagree. So an emitter with a body and this constant still 0 is called by
// nothing and emits nothing, and flipping the constant is what switches the family on.
//
// IT IS ALSO A COMPILE-TIME CONTRACT. While it is non-zero, PipeFill.cpp's `if constexpr`
// seam instantiates the forward to this family's entry points - EmitSamplerCso and
// EmitSamplerView below - so a missing or misspelled one is a build error in this file's
// own commit rather than a surprise at the merge.
//
// The A/B that switches this family off at RUNTIME is still the mask; this constant is
// what a build declares it emits for, and it feeds EmittedCallSuppliesTheWholeField's
// guard. ONE bit for the whole family, for the reason above.
// The subsystem bit this build emits for, covering the sampler CSO, the sampler view and all
// three unit sets (set_shader_images in ImageEmit.h included): one bit, so switching samplers
// off returns the whole family to the legacy path. PipeFill.cpp ORs it into
// kMGPipeWiredSubsystems, which the validate point's `wants()` tests; the birth hooks'
// FamilyIsLive tests this constant. While it is non-zero the ForwardWhenWired seam calls
// EmitSamplerCso / EmitSamplerView, so a misspelled entry point fails to build. The runtime
// switch is the MOBILEGL_PIPE_PUSH mask. Set it together with kMGPipeWiredTextureSubsystem.
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = kMGPipeSubsystemSamplers;
// ---------------------------------------------------------------------------------
// D-F1: the canonical SamplerParameters copy, and why it is not a memcpy
// ---------------------------------------------------------------------------------
//
// sizeof(SamplerParameters) == 100 and its members occupy 97 of those bytes: six 4-byte
// enums, four Floats, two more 4-byte enums, three 16-byte colour vectors and the one-byte
// borderColorForm. Bytes 97, 98 and 99 are PADDING and no writer ever touches them.
//
// A cache that hashed or memcmp'd the object's own bytes would therefore read
// uninitialised memory. In practice SamplerObject value-initialises its member and never
// rewrites it, so in practice those bytes are stable - and "in practice" is not a
// contract. The whole point of a content-addressed cache is that a false MISS mints a
// fresh CSO per call: a 256-entry cache with a hit rate of zero, on a path nobody looks
// at, because the pixels are right either way.
//
// So every hash and every confirm runs over a copy that is memset to zero FIRST and then
// assigned field by field, which makes the padding deterministically zero on both sides of
// the comparison. The field list is MGP_FIELDS_SamplerParameters', in its order, and the
// verify build compares the same sixteen fields one at a time - without that
// PipeFields.def row the blob would be compared as bytes and G4 would be a coin flip.
//
// ONE COPY PER MINT ATTEMPT, never per draw: the version-first skip in the two emitters
// below decides whether to come here at all.
// THE PRIMITIVE TAKES AN OUT-PARAMETER, and that is not a style preference either. A
// returned SamplerParameters is copied, and a copy of a trivially copyable type leaves the
// padding UNSPECIFIED - so a canonicaliser that returned by value would hand its caller a
// value whose three trailing bytes are whatever the copy left there, which is the very
// thing this function exists to make deterministic. Every producer of canonical bytes in
// this file, and every consumer that stores them, goes through this and through memcpy.
// ---- Canonical SamplerParameters copy ----
// SamplerParameters is 100 bytes with 3 bytes of trailing padding no writer touches, so the
// CSO cache never hashes or compares an object's own bytes: a padding mismatch is a silent
// false miss (correct pixels, a fresh CSO per call). The copy is memset to zero, then
// assigned field by field in MGP_FIELDS_SamplerParameters' order. It is an out-parameter
// because returning by value copies and leaves the padding unspecified; canonical bytes are
// stored only with memcpy. It runs once per mint attempt; the emitters' version checks keep
// it off the per-draw path.
inline void MGPipeCanonicaliseSamplerParameters(const SamplerParameters& src, SamplerParameters& canon) {
static_assert(std::is_trivially_copyable_v<SamplerParameters>,
"the canonical copy is memset and then assigned field by field");
@@ -151,59 +93,13 @@ namespace MobileGL::MG_Pipe {
return MGPipeHashSamplerParameters(canon);
}
// ---------------------------------------------------------------------------------
// D-F1: the content-addressed sampler CSO cache, capacity 256
// ---------------------------------------------------------------------------------
//
// Unlike P3a's vertex-elements CSO, content addressing is RIGHT here: a SamplerObject is a
// pure 100-byte value with no driver-side per-object binding state, two identical samplers
// can share one CSO with no extra work on either side, and Espryt's BackendSamplerObject
// is a driver name plus a parameter shadow - nothing a second frontend object would have
// to re-establish.
//
// THE LOOKUP IS CsoCache.h's, one for one: hash, probe, and CONFIRM WITH A MEMCMP before
// reusing a handle, because a bare 64-bit equality would alias two different sampler
// states onto one CSO and that is silent wrong filtering with no gate that can see it.
//
// WHAT KEEPS A LIVE HANDLE OUT OF THE LRU's REACH IS A REFERENCE COUNT, not the capacity
// (ID-17). The capacity argument - "one pass acquires at most kMGPipeMaxTextureUnits == 192
// CSOs and touches every one of them, so LRU can only evict an entry from an EARLIER pass"
// - closes the INTRA-PASS case only, and the case that matters is the other one: a
// MGPTextureParams record published in an earlier pass names its texture's BuiltinSampler
// for as long as that texture's parameters do not move, the applier deliberately does not
// resolve that handle, and an eviction is not a parameter change, so nothing re-emits and
// nobody refuses. With more than 256 distinct live sampler values - a CTS sampler sweep, a
// scene with many filter/wrap/border combinations - the rarely-touched built-in samplers
// are the FIRST victims.
//
// So every Acquire takes a REFERENCE and Release drops one; LRU considers only entries
// whose count is 0; and when every entry is pinned the cache MINTS BEYOND CAPACITY and
// counts it (Counters::OverCapacityMints). Growing is the safe direction - a slot too many
// costs memory, a handle pulled out from under a live record costs correctness - and the
// count is a recorded number rather than a gate, so a workload that pins more than 256
// values is visible instead of being wrong.
//
// WHO HOLDS A REFERENCE: package B's EmitTextureParams for a texture's built-in sampler
// (released when the texture dies or when a parameter change makes it re-acquire), and
// this file's own bind_sampler_states for every CSO a unit is currently bound to. Anything
// that acquires a handle it will still name after it returns must hold one.
//
// THE CAPACITY ITSELF IS STILL A SIZING BOUND worth asserting: a cache that could not hold
// one whole emission pass would over-capacity-mint on every single pass, which is a
// silently unbounded cache rather than a 256-entry one.
//
// THE SLOT IS ALLOCATED WITHOUT A LIFETIME ID, deliberately: a content-addressed CSO
// belongs to a VALUE and not to a frontend object, so ~SamplerObject must not free it -
// another live SamplerObject may hold the same value. MGPipeEmitSamplerCsoDestroyAndFree
// resolves nothing for such an id and correctly frees nothing; the only death path for
// these slots is the LRU eviction below, which is client-side and therefore
// backend-neutral on day one.
//
// PACKAGE D MUST BE TOLD, and it is the one consequence of that choice that reaches the
// server: NotifyStateObjectDestroyed(SamplerCso, lifetimeId) arrives from ~SamplerObject
// with NO HANDLE BEHIND IT, every time. A backend must NOT key a sampler twin on a
// SamplerObject's lifetime id. The twin's life is create_sampler_state -> the LRU's
// delete_sampler_state and nothing else.
// ---- Content-addressed sampler CSO cache ----
// A SamplerObject is a pure value with no per-object driver binding state, so equal samplers
// share one CSO. Lookup is CsoCache.h's: hash, probe, then memcmp before reusing a handle.
// Eviction safety comes from reference counts, not capacity: LRU evicts only unreferenced
// entries and the cache mints past capacity when all are pinned. Slots carry no lifetime id,
// so ~SamplerObject frees nothing and a backend must not key a sampler twin on a
// SamplerObject's lifetime id. Holders and the full rule: ARCHITECTURE.md §2.3.1.
inline constexpr SizeT kMGPipeSamplerCsoCacheCapacity = 256;
static_assert(kMGPipeSamplerCsoCacheCapacity > kMGPipeMaxTextureUnits,
"a cache that cannot hold one whole emission pass would mint over capacity on "
@@ -273,24 +169,13 @@ namespace MobileGL::MG_Pipe {
Uint64 GroupPublishes = 0;
};
// The handle for `params`' value, AND A REFERENCE ON IT. Mints and emits
// create_sampler_state on a miss and emits delete_sampler_state for whatever it evicts
// to make room. `payloadBytes` accumulates what went on the wire.
//
// EVERY Acquire TAKES A REFERENCE and every caller owes exactly one Release for it
// (ID-17). A caller that only wants a transient handle still has to release it, and a
// caller that keeps naming the handle in a published record must hold its reference for
// as long as that record stands - that is what stops the LRU pulling the handle out
// from under it, because an eviction is not a parameter change and nothing re-emits.
//
// THIS IS ALSO PACKAGE B's SEAM. MGPTextureParams::BuiltinSampler names the CSO that
// carries the SamplerParameters of the SamplerObject every ITextureObject owns, and a
// null handle there is Fatal{ProtocolCorruption} rather than "no sampler". TextureEmit.h
// acquires it from here - taking a reference it holds until the texture dies or a
// parameter change makes it re-acquire, at which point it releases the previous handle -
// so the texture's built-in sampler and a glBindSampler'd sampler object with the same
// value share one CSO and one server-side twin, which is exactly the sharing that makes
// content addressing the right answer for this kind.
// The handle for `params`' value, with a reference on it. On a miss it mints and emits
// create_sampler_state, and emits delete_sampler_state for any entry it evicts;
// `payloadBytes` accumulates the wire bytes. Every Acquire owes exactly one Release, and
// a caller that names the handle in a published record holds it for as long as the
// record stands. TextureEmit.h acquires MGPTextureParams::BuiltinSampler here (a null
// handle there is Fatal{ProtocolCorruption}), so a texture's built-in sampler and a
// bound sampler object with the same value share one CSO and one server twin.
MGPipeHandle Acquire(const SamplerParameters& params, Uint64& payloadBytes) {
SamplerParameters canon;
MGPipeCanonicaliseSamplerParameters(params, canon);
@@ -805,22 +690,13 @@ namespace MobileGL::MG_Pipe {
using ITextureObject = MG_State::GLState::ITextureObject;
using SamplerObject = MG_State::GLState::SamplerObject;
// set_sampler_views: the PROGRAM-RESOLVED set only, one entry per unit, no stage
// dimension. Start is 0 and Count is GetMaxTouchedTextureUnit() + 1 clamped to the
// wire bound - the high-water mark is directly the count argument and is not
// re-derived.
//
// WHAT A UNIT'S ENTRY MEANS, and all three cases are legal rather than holes:
// the program resolves no target here -> View and Texture both null
// it resolves one, nothing is bound -> View and Texture both null
// it resolves one and a texture is bound-> Texture is that object's handle and View
// is its sampler-view CSO
// An UNDEFINED DEFAULT texture (name 0 with no image) and a texture that
// SAMPLES AS INCOMPLETE are both dropped to null here, which is the resolution
// DirectGLES performs by leaving the native target unbound: an incomplete texture
// samples (0,0,0,1) and the driver cannot work that out for itself, because the
// backend storage is immutable and never saw the level the application redefined at
// the wrong size.
// set_sampler_views: the program-resolved set, one entry per unit, no stage dimension.
// Start is 0; Count is GetMaxTouchedTextureUnit() + 1, clamped to the wire bound. An
// entry is null when the program samples nothing at the unit, nothing is bound, the
// bound texture is the undefined default texture, or it samples as incomplete;
// otherwise it names the texture and its sampler-view CSO. Nulling an incomplete
// texture matches DirectGLES leaving the native target unbound: it must sample
// (0,0,0,1), and the backend's immutable storage never saw the redefined level.
Uint64 EmitSamplerViews(GLContext& ctx) {
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
const Uint32 count =
@@ -886,38 +762,14 @@ namespace MobileGL::MG_Pipe {
return bytes + sizeof(MGPSamplerViews) + static_cast<Uint64>(count) * sizeof(MGPBoundView);
}
// bind_sampler_states: the unit's sampler CSO, or the null handle when the unit has no
// sampler object - the texture's built-in sampler then applies, exactly as today.
//
// NOT program-resolved, and that asymmetry with the view set above is deliberate: a
// sampler object is bound to a UNIT and applies to whatever the unit holds, so its set
// is the plain per-unit walk BindCurrentUnitSamplers already does. A redundant re-bind
// of the same sampler object emits nothing at all, which is the whole point of the
// suppressor slot: GetTextureBindGeneration() bumps on a redundant re-bind - 26.2
// rebinds the same sampler at every texture-unit switch - so without it this would be
// a several-hundred-byte variable-length record per batch.
// A BOUND SAMPLER STATE HOLDS A REFERENCE ON ITS CSO FOR AS LONG AS IT IS BOUND
// (ID-17). The set the applier holds is "the last set as received" and outlives the
// pass that sent it, so a handle in it must not become the LRU's victim; the reference
// is what makes that true. The reconciliation below walks the unit array once: Acquire
// has already taken a reference for every unit in the new set, so a unit whose handle
// did not move gives that duplicate back, a unit whose handle moved gives back the one
// it used to hold, and a unit that fell outside the window gives back its own.
//
// ITS COST IS NOT "ONE EXTRA ARRAY WALK" and the ID-19 budget entry says so. Each
// Release of a NON-NULL handle is itself a linear scan of the whole cache, sitting
// beside an Acquire probe that is another, so a pass in which K units hold sampler CSOs
// costs 2 * K * O(cacheSize) handle comparisons - up to ~98 000 at K == 192 with a full
// cache, and unbounded above that because the over-capacity path lets the cache grow
// past 256. In Minecraft-shaped workloads K is typically 0: no unit has a SamplerObject
// bound, so Acquire is never called and Release(null) returns immediately. The workload
// that has both a large K and a large cache is a CTS sampler sweep. c0d moved the
// firing rate underneath this as well - bit 13's shutter now mixes
// GetTextureBindGeneration(), so this runs on EVERY texture bind at any unit and not
// only on a parameter change, and the set-hash suppressor below stops the record going
// out but not these two scans, which happen first. The per-unit (sampler lifetime id,
// ctx.GetSamplingResolutionGeneration()) latch recorded under ID-19 as C-m2 removes
// BOTH scans, not one.
// bind_sampler_states: each unit's sampler CSO, or null when the unit has no sampler
// object and the texture's built-in sampler applies. Not program-resolved: a sampler
// object applies to whatever its unit holds, as in BindCurrentUnitSamplers. A bound
// handle keeps a cache reference while bound, because the applier's set outlives this
// pass. Cost: Acquire and each non-null Release scan the whole cache, 2 * K * cache size
// per pass for K units with sampler objects (usually 0 in Minecraft, large in CTS sampler
// sweeps), on every texture bind; the set-hash suppressor skips only the record. A
// per-unit (sampler lifetime id, sampling resolution generation) latch would avoid both.
Uint64 EmitSamplerStates(GLContext& ctx) {
const Int maxTouched = ctx.GetMaxTouchedTextureUnit();
const Uint32 count =
@@ -1028,27 +880,12 @@ namespace MobileGL::MG_Pipe {
return handle;
}
// ---- THE TWO CONTRACT ENTRY POINTS THIS FAMILY OWES (contract-v2 §3.4) ----
//
// PipeFill.cpp's birth hooks forward here through an `if constexpr` seam keyed on
// kMGPipeWiredSamplerSubsystem, so while that constant is non-zero these two must
// exist and be spelled exactly like this or the build fails in this file's own commit.
// The hook has already applied BOTH gates (the operator's mask and the wired constant);
// what belongs here is the family's HANDLE RULE and its publication, which the contract
// deliberately does not pick for us because the three families disagree about identity.
//
// MINTING A SAMPLER CSO AT CONSTRUCTION TIME CONTENT-ADDRESSES THE DEFAULT PARAMETERS,
// and that is correct rather than wasteful: the cache dedupes, so a thousand
// freshly-created SamplerObjects share ONE create_sampler_state, and the moment the
// application sets a parameter the next acquire content-addresses the new value and
// this object simply stops naming the default entry. It is one create_sampler_state per
// distinct default-valued sampler, not one per object.
//
// THE REFERENCE THIS TAKES IS DROPPED IMMEDIATELY. A birth is not a standing record: no
// MGPTextureParams and no bind_sampler_states names this handle yet, so pinning the
// entry here would keep the default-parameter value un-evictable for the life of the
// process for no reader's benefit. Whoever later names the handle in a record takes its
// own reference through its own Acquire.
// ---- Birth hooks, forwarded from PipeFill.cpp ----
// ForwardWhenWired calls these after FamilyIsLive passes, so the names must match.
// EmitSamplerCso content-addresses the new object's default parameters, so all
// default-valued samplers share one create_sampler_state. Its reference is released at
// once: a birth names the handle in no record, and pinning it would keep the default
// value unevictable; whoever later names the handle takes its own reference.
void EmitSamplerCso(SamplerObject& sampler) {
Uint64 bytes = 0;
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
+7 -28
View File
@@ -9,34 +9,13 @@
#pragma once
#include <Includes.h>
// Coalescing rule 4 (ARCHITECTURE.md 5.4, P2 brief D11): every kVarTail set_* hashes the
// RESOLVED set on the client and does not emit when the hash has not moved.
//
// This is the carrier for the ~175 lines of debounce that move off the backends in P3b and
// P4b - Espryt's UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and
// Magma's equivalents all answer "is this set the same set as last time", and every one of
// them answers it against a shape the backend rediscovered. P2 lands the MECHANISM and ONE
// real consumer (SetVertexAttribDefaults) so the shape is pinned by a test rather than by a
// plan; the other six slots exist, are unit-tested, and are wired by the phase that moves
// the set they name. P3a wires the second, SetVertexBuffers. P4a wires SetSamplerViews,
// BindSamplerStates and SetShaderImages, and APPENDS an eighth slot, SetFramebufferState -
// which leaves only SetShaderBuffers and SetStreamOutputTargets unwired, both P4b's.
//
// A WIRED SLOT PUTS A REQUIREMENT ON ITS HASH, and SetVertexBuffers is where that first
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
// baseInstance is DRAW state and moves without the buffer set moving, so a hash over the
// entries alone would suppress a record whose one changed field is the fetch shift and the
// server would keep the previous one. MG_Impl/Pipe/VertexInputEmit.h's
// MGPipeVertexBufferSetContentHash mixes Start, Count and BaseInstance in for exactly that
// reason, and VertexInputEmit's base-instance pair is the test that says so.
//
// A hash of 0 is reserved for "never emitted", so the first emission always goes out; a
// computed 0 is remapped to 1, which costs one collision in 2^64 an extra emission and
// never a missed one.
//
// Header-only for the same ownership reason as Tracker.h and CsoCache.h: the root
// CMakeLists.txt that would name a new .cpp belongs to package A and is frozen behind the
// p2/contract tag.
// Set-hash suppression (ARCHITECTURE.md §5.4): every kVarTail set_* hashes the resolved set on
// the client and is not emitted while the hash is unchanged, in place of the backends' own
// "same set as last time" debounce. A slot's hash must cover every input its record carries,
// not only the set: MGPipeVertexBufferSetContentHash mixes in BaseInstance, which is draw state
// that moves without the buffer set, or a changed fetch shift would be suppressed. A latched 0
// means "never emitted", so the first emission always goes out; a computed 0 is latched as 1,
// costing at worst one extra emission, never a missed one.
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
+14 -55
View File
@@ -9,45 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of set_shader_buffers - the INDEXED BUFFER BINDING POINTS (P5e package sb,
// docs/archive/contracts/CONTRACT-P5E.md §5.6, rulings 10 and 11). The call has been in the catalogue since
// P4a with no emitter, no route and no applier; this file is the emitter half.
//
// ONE RECORD PER CLASS, THREE CLASSES. MGPShaderBuffers::Class names which binding-point array
// the record describes - Uniform=0, ShaderStorage=1, AtomicCounter=2 - so the three are three
// emissions through one route and one applier, not one record with three tails. That is also
// why the suppressor has THREE SLOTS (ruling 11): a single slot would let a uniform set cancel
// the previous storage set, and the shader-storage bindings would be suppressed as "unchanged"
// by a record that described something else entirely.
//
// XFB IS NOT ONE OF THEM (§5.7). The transform-feedback capture points are span-scoped state
// latched at glBeginTransformFeedback, and set_stream_output_targets carries a Generation this
// payload has no field for - so the catalogue's split between the two rows stays, XFB keeps its
// lockstep escalation, and dirty bit 17 fires for a family that emits nothing this phase.
//
// TWO INVARIANTS THAT MUST SURVIVE INTO THE BODY:
// 1. THE HIGH-WATER-ZERO EARLY-OUT. A class whose touched high-water mark is 0 emits nothing,
// BEFORE any hash and before any walk. Minecraft's touched SSBO and atomic-counter counts
// are both 0, so the whole family costs one integer read per class per validate point on
// the workload this phase exists for.
// 2. A BASE BINDING TRAVELS AS kMGPipeWholeBuffer AND NEVER AS A RESOLVED EXTENT (§5.6,
// table 0). glBindBufferBase does not freeze anything: GL resolves the range against the
// object's size at every USE, and BindingSlotRange1D::GetRange() reproduces that by asking
// the bound object. Resolving it HERE would freeze the size as of the emission, and under
// run-ahead a glBufferData issued between the emission and the apply would then bind the
// OLD extent - silently, with the right handle. So Offset/Size are copied only for an
// EXPLICIT range (glBindBufferRange), and a base binding says "whole buffer" and lets the
// server re-resolve against its own descriptor.
//
// THE WINDOW IS THE TOUCHED HIGH-WATER MARK (ruling 10), the same value the backend's own walk
// has used since P2 and the same value MGPContextValues already carries. A program-derived
// window would be narrower for the uniform class - Minecraft reaches UBOs through the program's
// block bindings, so a program binding block 0 at GL point 83 pays a 84-entry record - but that
// is a NARROWING and this package is a correctness landing; BRIEF-P5E §5 lists it as trailing.
//
// HEADER-ONLY, and the wired constant below is what switches the family on, for the ownership
// reason PipeFill.cpp states in full: that file belongs to the contract package for the whole
// phase, so the bit that turns an emitter on is a constant in the emitter's OWN header.
// Client side of set_shader_buffers, the indexed buffer binding points. One record per class
// (MGPShaderBuffers::Class: Uniform, ShaderStorage, AtomicCounter) through one route and applier,
// with one suppressor slot per class so one class's record cannot mask another's. Transform
// feedback capture points are not in this family; they ride the begin_stream_output record.
// Invariant 1: a class whose touched high-water mark is 0 emits nothing, before any hash or
// walk; that mark is also the record's window. Invariant 2: a base binding travels as
// kMGPipeWholeBuffer, never a resolved extent: GL resolves it at each use, so the server resolves
// it against its own descriptor, and a glBufferData between emission and apply binds the new size.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -63,22 +32,12 @@
namespace MobileGL::MG_Pipe {
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the per-family
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives this
// emitter its body and no file is touched twice.
//
// IT MUST MOVE WITH MG_Backend/Init.cpp's CONSUMER BIT (ID-106). c0e deliberately withheld
// bit 13 from ConsumedSubsystemsFor because no emitter existed and withholding is the safe
// direction; the client's R-8 liveness gate (PipeFill.cpp's P5eFamilyIsLive) asks the server
// for THIS bit in particular, so a build that set the constant here and left the server's
// mask alone would emit nothing at all and look exactly like a build that had not landed.
//
// TURNING IT ON DOES NOT RETIRE A PULL. GetBufferBindingPoint is the family's Coverage.def
// emitted row and PipeFill.cpp's EmittedCallSuppliesTheWholeField answers FALSE for it: the
// field is four raw bases into the frontend's binding-point table and the record carries
// resolved {handle, offset, size} triples, so the residual fill keeps writing the mirror and
// the verify comparator keeps proving it. What retires the pull is the four Espryt consumers
// reading the applier's window instead, which is the other half of this package.
// The subsystem bit this build emits for; PipeFill.cpp ORs it into kMGPipeWiredSubsystems.
// It must change together with MG_Backend/Init.cpp's consumer bit 13: P5eFamilyIsLive asks
// the server for that bit, so setting only this constant emits nothing. Emitting does not
// retire a pull: GetBufferBindingPoint holds raw frontend table bases while the record
// carries resolved {handle, offset, size} ranges, so EmittedCallSuppliesTheWholeField
// answers false and the residual fill keeps writing that mirror.
inline constexpr Uint64 kMGPipeWiredBufferBindingSubsystem = kMGPipeSubsystemBufferBindings;
// The BufferTarget each wire class names. One table, because the class is a wire value and
+21 -56
View File
@@ -11,29 +11,13 @@
#include <MG_Pipe/MGPipeHandles.h>
// The CLIENT's slot allocator: the thing that mints every MGPipeHandle in the system
// (ARCHITECTURE.md 4.2 - no create_* call in the catalogue returns a server-cast handle,
// which is what lets the whole catalogue be remoted with zero creation round trips).
//
// Per kind: a free list plus a high-water mark, so slots stay DENSE and the server's object
// table is an array rather than a hash map. It has nothing to do with MG_State's
// IndexGenerator - that container's LIFO GL-name reuse is the very problem {slot, gen}
// exists to close, and the whole point of the identity is that an ABA on the GL name, on
// the heap address or on the lifetime id cannot reproduce a handle.
//
// Gen increments ONLY when a slot is reused, never on a respecify: a glBufferData on a live
// buffer keeps the same {slot, gen}, because the object is the same object. Two generations
// exist in the design and they are strictly separate - this is the client's answer to "is
// this still the same GL object"; MGGen is the server's epoch for "did I recast my driver
// object", and no MGPipe call may require the client to know it.
//
// The lifetimeId -> slot map is what keeps a GL NAME out of every key (ARCHITECTURE.md 4.2):
// the frontend object's lifetime id is the client's own identity for it, so the backend key
// is the handle and the frontend key is the lifetime id, and neither is a recyclable name.
//
// Lives in MG_Impl (the client side, unrestricted) and is compiled only under
// MOBILEGL_PIPE_PUSH. It is in the P2 CONTRACT commit rather than in a Track H package
// because both Track H slices - Espryt 0b and Magma subsystem 4 - key off it.
// The client's slot allocator: it mints every MGPipeHandle, so no create_* call needs a round
// trip (ARCHITECTURE.md §2.1). A free list plus a high-water mark per kind keeps slots dense,
// so the server's object table is an array. Unrelated to MG_State's IndexGenerator, whose LIFO
// GL-name reuse is the ABA {slot, gen} exists to prevent. Gen increments only when a slot is
// reused, never on a respecify; it is separate from the server's MGGen epoch, which no MGPipe
// call may require the client to know (§2.2). The lifetimeId -> slot map keeps GL names out of
// every key: the frontend key is the lifetime id, the backend key is the handle.
namespace MobileGL::MG_Pipe {
class MGPipeSlotAllocator {
@@ -48,23 +32,14 @@ namespace MobileGL::MG_Pipe {
// Allocate and remember `lifetimeId` as this handle's frontend identity.
MGPipeHandle AllocateFor(MGPipeKind kind, Uint64 lifetimeId);
// P4a, D-H7: THE ONE ENTRY POINT INTO THE ShaderCso COMPOSITE BAND, and the only one
// there will ever be. Allocate() above refuses that band on purpose, so a program
// pipeline's flattened composite - minted client-side from the stage programs bound to
// the pipeline object, and indistinguishable from an ordinary program to the server -
// needs a door of its own rather than a flag on the handle. The kind is implied: only
// ShaderCso has a band.
//
// It behaves exactly like AllocateFor in every other respect (free list first, then
// the band's own high-water mark; Gen moves only on reuse; the lifetimeId -> slot map
// is written) and it carries the band's own exhaustion assert, so exhausting the
// composite space is a NAMED Fatal rather than silent slot theft from ordinary
// programs. Returns kMGPipeNullHandle when the band is full.
//
// Freed through the ordinary Free(MGPipeKind::ShaderCso, handle): a composite's slot
// has two independent release paths - the pipeline cache's LRU eviction and the
// composite ProgramObject's own destructor - and Free refusing a slot that is not live
// at that generation is what makes the second one a proven no-op.
// The only way into the ShaderCso composite band, which Allocate() refuses: a program
// pipeline's flattened composite is minted client-side and looks like an ordinary
// program to the server, so it gets its own entry point rather than a flag on the
// handle. Otherwise it behaves like AllocateFor (free list first, Gen moves only on
// reuse, the lifetimeId map is written). Returns kMGPipeNullHandle, after a named
// assert, when the band is full. Freed through Free(ShaderCso, handle): of its two
// release paths (the pipeline cache's LRU and ~ProgramObject), the second finds the
// slot no longer live at that generation and is a no-op.
MGPipeHandle AllocateComposite(Uint64 lifetimeId);
// The handle a lifetime id was allocated for, or kMGPipeNullHandle. A recycled heap
// address does NOT reproduce a mapping: MG_State hands out a fresh lifetime id per
@@ -89,22 +64,12 @@ namespace MobileGL::MG_Pipe {
// otherwise, live or not.
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
Uint64 LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const;
// One past the highest ORDINARY slot ever handed out of this kind. For every kind but
// ShaderCso that is the whole story; for ShaderCso the composite band is a second,
// separately dense space and CompositeHighWater() below answers it.
//
// THE TWO SPACES ARE REPORTED SEPARATELY, and that is the point rather than a detail.
// Folding the band into this number pins it at ~983k from the first composite mint
// onward, and every later assertion of the "the high-water mark did not move over N
// churn rounds" shape - the one that catches a dense table that never shrinks, which
// is the ~1.3 KB-per-record leak C-1 produced - becomes vacuously true for ordinary
// ShaderCso slots for the rest of the process. A leak case per space is two real
// assertions; one merged number is one real assertion and one that cannot go red.
//
// It is also NOT a table size for kind ShaderCso even now: the band is sparse against
// the ordinary space by design, so a consumer indexing by slot must test
// MGPipeIsCompositeShaderSlot(slot) first and keep the band in a table of its own,
// exactly as this allocator does.
// One past the highest ordinary slot ever handed out of this kind. For ShaderCso the
// composite band is a separate dense space, answered by CompositeHighWater(): folded
// in, this number would sit at ~983k after the first composite, and a leak test
// asserting it stays put over churn could no longer fail for ordinary slots. Nor is it
// a table size for ShaderCso: a consumer indexing by slot tests
// MGPipeIsCompositeShaderSlot(slot) first and keeps the band in a table of its own.
Uint32 HighWater(MGPipeKind kind) const;
// One past the highest COMPOSITE slot ever handed out, i.e.
// kMGPipeShaderCsoCompositeSlotBase + (band slots ever handed out), and exactly the
+61 -232
View File
@@ -9,49 +9,14 @@
#pragma once
#include <Includes.h>
// The CLIENT side of P4a's texture and renderbuffer family: resource_create from the object's
// Client side of the texture and renderbuffer family: resource_create from the object's
// constructor, resource_respecify from every storage-defining entry point, set_texture_params
// from the parameter mutators, and resource_subdata from the DRAIN LIST at the validate point.
//
// THE THREE OBJECT CALLS ARE NOT EMITTED FROM HERE'S CALLER, they are emitted from MG_State's
// own mutators - a constructor, a storage definition, a glTexParameter - exactly as P3a's
// buffer family is, because that is where the event happens. Only the sub-data drain runs at
// the validate point, which is the explicit exception ARCHITECTURE.md 5.1 makes for texture
// upload: walking every live texture per verb is the cost the drain list exists to avoid.
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full: PipeFill.cpp is the contract package's for the whole
// phase, so the emitter package edits this header and the value of
// kMGPipeWiredTextureSubsystem below, and never that file.
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
//
// ---------------------------------------------------------------------------------------
// HOW MG_State REACHES THIS FILE: IT DOES NOT, AND THAT IS THE POINT (c0b, ID-13).
//
// v1 of this package shipped a deviation - six free functions here, called from
// TextureObject.cpp and RenderbufferObject.cpp - because at the contract TAG
// MG_Pipe/PipeMutation.h declared only the six DEATH helpers and this package may not edit
// A's files. c0b landed the BIRTH half, so the deviation is retired rather than carried:
// MG_State now calls MGPipeMintTextureHandle / MGPipeEmitTextureResourceCreate /
// ...ResourceRespecify / MGPipeEmitTextureParams / MGPipeNoteTextureLevelDirty and the two
// renderbuffer twins, all DECLARED in MG_Pipe/PipeMutation.h and DEFINED in
// MG_Impl/Pipe/PipeFill.cpp, which forwards to the entry points below through
// ForwardWhenWired<kMGPipeWiredTextureSubsystem>. No MG_State translation unit includes this
// header any more, which is the property check_include_closure.py's mutation-header probe
// exists to keep.
//
// WHAT THIS FILE OWES THAT SEAM, and a mismatch is a compile error in this package's own
// commit rather than a surprise at the merge (that is what the wired constant buys):
//
// EmitResourceCreate(ITextureObject&) EmitResourceRespecify(ITextureObject&)
// EmitTextureParams(ITextureObject&) NoteLevelDirty(ITextureObject&, Uint32, Uint32)
// EmitRenderbufferCreate(RenderbufferObject&)
// EmitRenderbufferRespecify(RenderbufferObject&)
//
// and the PUBLICATION LATCH is A's too: MGPipeNoteHandlePublished is called where a create
// actually goes out and MGPipeHandleIsPublished is what the death helpers read, so this file
// keeps no Published flag of its own.
// from the parameter mutators, and resource_subdata from the drain list at the validate point.
// Like the buffer resource ops (ARCHITECTURE.md §5.1), the object calls go out from MG_State's
// mutators, where the event happens; texels accumulate and drain at the validate point (§6),
// from a list of dirty levels so no verb walks every live texture. MG_State never includes this
// header: it calls the hooks in MG_Pipe/PipeMutation.h, which PipeFill.cpp forwards here through
// ForwardWhenWired. The publication latch is PipeFill.cpp's; this file keeps no flag of its own.
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -79,30 +44,13 @@
namespace MobileGL::MG_Pipe {
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the four per-family
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives the
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
// silently drop a field on the floor before the call that carries it exists.
//
// IT WAS 0 IN v1, AND THE THING THAT BLOCKED IT IS NOW IN THE TREE. Unlike the other three
// P4a families this one gets no fresh apply entry points - the catalogue is closed and a
// texture rides P3a's OWN resource_create / resource_respecify / resource_subdata /
// resource_destroy rows - so on a base without the wire package's per-kind record vectors
// and its texture branch, two properties made a texture record actively harmful rather than
// merely ignored: MGPipeApplierState::Resources was ONE slot-indexed vector, so a texture
// create at slot 12 overwrote the BUFFER record at slot 12; and SubDataBoxFault validated
// every record as the buffer half of MGPSubData, so a texture sub-data record was
// Fatal{ProtocolCorruption} on `record.Level != 0` alone. Both are closed on this base
// (three per-kind vectors, ApplyTextureUpload, SubDataTextureFault, and C1's level-scoped
// PendingUploads clear), so the constant is its own bit and the family is live.
//
// THE FLIP IS THE WHOLE SWITCH AND NOTHING ELSE MOVES: PipeFill.cpp ORs this constant into
// kMGPipeWiredSubsystems, static_asserts it is 0-or-its-own-bit, gates every birth hook on
// FamilyIsLive(kMGPipeSubsystemTextureResources, this), and gates DrainTextureSubData on
// the same OR. D-K2's rows - including bit 9's dependency on this family - are in
// MG_Pipe/SubsystemDeps.def, once (P3b/P4b R-5), and are not restated here; what this
// comment still has to say is the consequence for INTEGRATION ORDER, which no table can
// carry: this package may never be integrated with only one of the two constants set.
// The subsystem bit this build emits for. PipeFill.cpp ORs it into kMGPipeWiredSubsystems,
// gates every texture birth hook on FamilyIsLive and DrainTextureSubData on the same mask;
// 0 compiles the family out. Textures share the resource_* rows with buffers, which is safe
// because the applier keeps one record vector per kind and checks texture sub-data on its own
// (SubDataTextureFault). The rows of MG_Pipe/SubsystemDeps.def are checked against
// MOBILEGL_PIPE_PUSH, not the wired constants, so this constant and the sampler one are set
// together, and the framebuffer constant is never set without this one.
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = kMGPipeSubsystemTextureResources;
static_assert(kMGPipeWiredTextureSubsystem == 0 ||
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources,
@@ -407,30 +355,14 @@ namespace MobileGL::MG_Pipe {
return region;
}
// ---------------------------------------------------------------------------------
// THE STAGE-CHUNK SLAB SPLIT (fix A2)
// ---------------------------------------------------------------------------------
//
// ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, a linear arena, so a level shadow larger
// than that arena is Fatal{RingOverrun, "SEG_STAGE"} at the encoder rather than a split
// (PipeWireCodec.cpp:856-864) - the same wall PipeFill.cpp's content walks are cut at. It is
// not hypothetical: measured on the CI traces, a 512x128x33 GL_RGBA32F level is 34,603,008
// bytes and a 192-cube GL_RGBA16 level is 56,623,104, both against the default 32 MiB
// segment, so those glTexImage3D calls aborted their replays at the emitter.
//
// THE SPLIT IS INTO WHOLE-WIDTH SLABS. A piece is a set of CONSECUTIVE ROWS OF THE LEVEL
// SHADOW - {x0, y0, z0, w, dy, dz} in level coordinates - because that is the one shape whose
// box names both its texels and its byte run: the run begins at
// z0 * sliceStride + y0 * rowStride + x0 * bpp and is exactly the box's own byte extent. So
// the run's placement is derivable from the strides and box the record carries, and the far
// side never has to know the level's format or pixel size. Whole slices are the coarsest cut
// and the two CI blobs take it; a slice bigger than the cap falls back to whole rows, and a
// ROW bigger than the cap to whole texels inside one row - still one box exactly matching its
// own run, which is the property everything downstream depends on.
//
// THE PIECES ARE CONTIGUOUS AND ASCENDING, and their union is the level exactly once: the
// server assembles one level image out of them (StagedTextureStore::AdoptRun), so a gap would
// lose texels and an overlap would place them twice.
// ---- Stage-chunk slab split (ARCHITECTURE.md §4) ----
// One record's blob is staged whole in SEG_STAGE, so a level larger than the chunk cap goes
// out as several records (a 512x128x33 GL_RGBA32F level is 33 MiB, over the whole default
// segment). A piece is whole slices, else whole rows of one slice, else texels of one row:
// the only shapes whose byte run starts at z0*sliceStride + y0*rowStride + x0*bpp and is
// exactly the box's extent, so the server places it without knowing the format. Pieces
// ascend, touch and cover the level exactly once; StagedTextureStore::AdoptRun assembles one
// level image from them, so a gap loses texels and an overlap places them twice.
struct MGPipeTextureSlab {
MGPBox Box{}; // level coordinates
Uint64 RunOffset = 0; // the run's first byte, as an offset into the level shadow
@@ -690,27 +622,14 @@ namespace MobileGL::MG_Pipe {
entry = Entry{};
}
// ---- the sticky bind mask (D-A4) ----
//
// ORed, never cleared, and emitted on BOTH resource_create and every
// resource_respecify, exactly as P3a's buffer mask is. The four bits nothing set before
// P4a get their producers here and in the framebuffer emitter: RENDER_TARGET and
// DEPTH_STENCIL from an attachment point (FramebufferEmit.h), SAMPLER from a resolved
// sampler view (SamplerEmit.h) and SHADER_IMAGE from glBindImageTexture's state setter
// and the resolved image unit (TextureState.h, ImageEmit.h) - the last two through the
// contract's MGPipeNoteTextureBoundAs door, since neither may include this header
// (final review M-A: before the fix round nothing produced them and the hint was dead).
// A MASK CHANGE AFTER THE ALLOCATION IS A METADATA RESPECIFY (ID-18 M4), and without it
// the sticky half of D-A4 is a no-op for exactly the textures it was written for. The
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
// no further respecify, that being what immutable means - so for the canonical order
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
// ImageBindableHint = 0 for ever and the re-mint PREVENTION never fired. So a mask that actually MOVES re-emits the stored descriptor with
// the new mask: every storage-defining field is byte-identical to what the applier
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
// change arriving between a glTexSubImage2D and the sync that consumes it cannot eat
// the texels.
// ---- Sticky bind mask ----
// ORed, never cleared, and carried on resource_create and every resource_respecify.
// Producers: FramebufferEmit.h (RENDER_TARGET, DEPTH_STENCIL), SamplerEmit.h (SAMPLER),
// ImageEmit.h and glBindImageTexture's setter in TextureState.h (SHADER_IMAGE); the
// last three use the MGPipeNote* hooks in PipeMutation.h, as they cannot include this
// header. An immutable texture is never respecified again, so a mask that moves later
// re-sends the stored descriptor with the new mask (RepublishMask), which the applier
// takes as a metadata update that drops no pending upload (ARCHITECTURE.md §6.1).
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_textures, handle);
@@ -770,34 +689,14 @@ namespace MobileGL::MG_Pipe {
PublishCreate(MGPipeKind::Texture, handle, entry, desc);
}
// resource_respecify, from every storage-defining entry point, WITH THE SCOPE OF THE
// STORAGE IT REPLACES (P4a final review C-1; the scopes are PipeMutation.h's).
//
// THE LEVEL IS PASSED, AND IT IS WIRE'S KEY. The applier keeps a pending-upload set per
// (uploadTarget, level) - the client's dirty flags, inverted - and a respecify drops the
// entries against the storage it REPLACES: with a null MGPRespecifiedLevel every entry,
// with a level exactly that one. v2 passed null at every call, so a level the applier
// had ACCEPTED at one verb (the client flag already clear, D-D5 step 1) and that the
// next verb's glTexImage2D(level 1) or glGenerateMipmap grow defined AROUND was dropped
// with nobody owing its texels: `L0; draw(other); L1; draw(T)` read a black level 0.
// The key is built from the SAME packed MGPSubData::Target the drain puts in that
// level's record (wire-v3 §5 item 5), so what this drops is what that emission made.
//
// THREE SCOPES, one call each for the first two and one call PER REMOVED LEVEL for the
// chain cut: the applier's key is one (uploadTarget, level), so "every level from N"
// is spelled as N.., each after the first landing on an unchanged descriptor - which
// the applier classifies as a metadata update that drops nothing but the level it
// names. That is the refinement wire's W11 clause takes this round.
//
// DEDUPED ON THE DESCRIPTOR ITSELF for the whole-resource form only: the entry points
// that reach it move the SHAPE and several of them do not move the descriptor at all
// (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field this
// record carries), and a byte compare of an 88-byte POD is cheaper than the emission
// it avoids. A PER-LEVEL form is never deduped: the level it redefines is not in the
// descriptor (a non-base level's extent moves no field), so an unchanged descriptor
// cannot say whether the applier still holds a box against the OLD level - and a box
// kept across a shrink is uploaded past the end of the new one. One applier call per
// level definition is the cost, and the sub-data that follows moves the serial anyway.
// resource_respecify, from every storage-defining entry point, with the scope of the
// storage it replaces (MGPipeTextureRespecifyScope). The applier drops pending uploads
// per (uploadTarget, level), only against the replaced storage (ARCHITECTURE.md §6.1);
// the key packs the same MGPSubData::Target the drain writes. A chain cut sends one call
// per removed level. A whole-resource call is deduped on the descriptor, since some
// entry points (glTexParameter BASE_LEVEL) move no descriptor field; a OneLevel call
// never is: a non-base level's extent moves no descriptor field, and a pending box kept
// across a shrink would upload past the end of the new level.
void EmitResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
Uint32 uploadTarget, Uint32 level) {
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
@@ -1131,32 +1030,14 @@ namespace MobileGL::MG_Pipe {
NoteRespecified(entry, desc, accepted);
}
// ---- the drain list (D-D4) ----
//
// Appended ONCE, on the first dirty mark of a level, and cleared at emission. Keyed on
// the STORAGE OWNER from day one and for free: TextureObjectView forwards
// MarkStorageDirty / MarkStorageDirtyRegion to the OWNER's methods after remapping the
// level and the region, so an upload through a view and an upload through the owner
// reach this function with the same object and the same owner-side coordinates.
//
// The per-slot key list is a short linear scan rather than a hash: a level count is
// ~15, the cap on the rect list behind it is 96, and this runs on the glTexSubImage
// path which has just memcpy'd texels.
// THE PARAMETER TYPES ARE THE CONTRACT'S (PipeMutation.h): Uint32 rather than
// MobileGL::TextureUploadTarget and Uint, because that declaration is the one door
// MG_State has into the client and it may not name a frontend enumeration.
//
// THERE IS NO CLEAN ARM, and that is a DECLARED DEVIATION rather than a dropped half.
// v1 carried a second entry point for MarkStorageDirty(..., false); the contract's hook
// has no `dirty` parameter, and asking A to widen it would put a second signature in
// MG_Pipe/PipeMutation.h for something the drain already collects. A level that goes
// clean stays on the list until the NEXT drain walks it, where
// `!mipmap->IsStorageDirty(...)` is the first test EmitOneLevel makes and returns
// "nothing owed", so the entry is dropped from both lists there. The cost is one
// IsStorageDirty call per cleaned level per drain, the list is bounded by the (texture,
// level) pairs dirtied since the last validate point, and a re-dirty before that drain
// is already covered by the entry still standing. What it must NOT be confused with is
// dropping the level's TEXELS: nothing here clears a dirty flag.
// ---- Drain list ----
// A (storage owner, upload target, level) is appended on its first dirty mark and
// removed when emitted. TextureObjectView forwards dirty marks to its owner in owner
// coordinates, so view and owner uploads share one entry. The per-slot key list is a
// linear scan (about 15 levels, on a path that has just copied texels). The hook takes
// Uint32s because PipeMutation.h may not name frontend enums. There is no "went clean"
// hook: a level cleaned before the drain is dropped there when EmitOneLevel finds it
// clean. Nothing here clears a dirty flag.
void NoteLevelDirty(ITextureObject& texture, Uint32 uploadTarget, Uint32 level) {
if (m_draining) return;
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
@@ -1499,29 +1380,14 @@ namespace MobileGL::MG_Pipe {
const MGPBox unionBox = MGPipeBoxOfDirtyRegion(mipmap->GetStorageDirtyRegion(uploadTarget, level));
if (unionBox.W == 0 || unionBox.H == 0 || unionBox.D == 0) return false;
// THE REGION LIST BEHIND THE BOX IS THE LEVEL'S EXACT DIRTY FOOTPRINT (P11 M2, dev
// 9524c688): disjoint boxes holding every texel written since the level was last
// clean and no other. The storage's rect list this used to carry only COVERS the
// writes - it merges touching rects into their bounding box, folds when full and is
// withheld (0) past 3/4 of the union box - and once the far side's image may hold GPU
// writes, whatever it uploads beyond the writes goes up over them. Both servers read
// the list as the texels to upload: Magma always, Espryt once its driver texture may
// hold GPU writes (and otherwise it re-applies the storage's box-or-rects choice
// itself, so the Mali job count is the server's call as before).
//
// 0 IS NOW THE WHOLE-LEVEL SPELLING - a footprint that is the whole level - with one
// exception (below). Every other write carries its boxes, a single one included, so the
// applier's accumulation (boxes union, lists concatenate, "0 regions" makes the entry
// box only) never widens two exact contributions into their union box. THE EXCEPTION:
// a footprint larger than one record may carry (kMGPipeMaxPendingUploadRegions) falls
// back to the storage's rect list - a COVER, stated once in the log - and when that list
// is withheld (0: its rects fill 3/4 of the union box) this 0 is a PARTIAL, INEXACT
// box. A reader that takes "box only" as "the client owns every texel of the box"
// (AdoptDriverLevelIntoShadow's covering-box rule) is exact everywhere but here.
//
// READ AS BOXES, ONCE, because both shapes below want them in that form: the
// whole-level record converts each one straight back through MGPipeBuildSubRegion,
// and a piece clips each one to its own slab.
// The region list is the level's exact dirty footprint: disjoint boxes holding
// every texel written since the level was last clean and no other, because a server
// image that may hold GPU writes must not be uploaded over outside them. 0 regions
// means a whole-level write, except when the footprint exceeds
// kMGPipeMaxPendingUploadRegions: the storage's covering rect list goes out instead,
// and when that list is withheld the box is inexact (ARCHITECTURE.md §6.1.1). Read
// as boxes once: the whole-level record converts each through MGPipeBuildSubRegion,
// and a piece clips each to its slab.
mipmap->GetStorageDirtyFootprint(uploadTarget, level, m_footprint);
m_rectBoxes.clear();
const Bool wholeLevelWrite =
@@ -1656,23 +1522,13 @@ namespace MobileGL::MG_Pipe {
accepted = emitRecord(shadow, static_cast<Uint64>(levelBytes));
}
// THE CLIENT CLEARS ITS OWN FLAG ONLY FOR A LEVEL THE APPLIER ACCEPTED (D-D5 step 1
// read literally; ID-18 M3). v1 cleared on DISPATCH - and, with the wired constant
// still 0, even on a call the `if constexpr` had discarded - so any refusal left the
// server with nothing and the client with a clean flag, and since MG_Impl contains
// no reader of a texture's dirty state the level simply stopped updating for the
// life of the texture. The two refusal paths are invisible from here without this
// answer: a dead or stale handle is a counted no-op and a corrupt record is a Fatal
// that deliberately moves no counter.
//
// A REFUSED LEVEL STAYS DIRTY AND STAYS ON THE DRAIN LIST, which is the safe
// direction and self-heals: the ordinary cause is a record the applier does not
// hold, and the next respecify's self-healing create gives it one. It is LOUD
// because a permanently refused level would otherwise re-emit once per verb for
// ever with nothing to show for it. WITH PIECES IN FLIGHT it is the same rule one
// level up: every piece is still emitted, so a refusal leaves the server with a
// partial image and the WHOLE level dirty, and the next drain re-sends every piece -
// an adoption of the same bytes twice is idempotent.
// The dirty flag is cleared only for a level the applier accepted (ARCHITECTURE.md
// §6): nothing else in MG_Impl reads a texture's dirty state, so clearing it on a
// refused record would stop the level updating for the texture's lifetime. A refused
// level stays dirty and on the drain list; the usual cause is a missing applier
// record, which the next respecify's self-healing create supplies. It is logged
// because a level refused for good re-emits every verb. A refused split level
// re-sends every piece; adopting the same bytes twice is idempotent.
if (dispatched && !accepted) {
++m_refusedSubDatas;
MGLOG_E_ONCE("MGPipe: resource_subdata for texture {slot=%u, gen=%u} level %u was refused; "
@@ -1725,31 +1581,4 @@ namespace MobileGL::MG_Pipe {
(MG_Config::Features.PipePush & kMGPipeSubsystemTextureResources) != 0;
}
// ---------------------------------------------------------------------------------
// WHAT USED TO BE HERE, AND WHY IT IS NOT (c0b, ID-13)
// ---------------------------------------------------------------------------------
//
// v1 carried eight free functions - MGPipeMintAndCreateTexture, MGPipeEmitTextureRespecify,
// MGPipeEmitTextureParams, MGPipeNoteTextureLevelDirty, MGPipeMintAndCreateRenderbuffer,
// MGPipeEmitRenderbufferRespecify and the two ...ResourceDestroy halves - because at the
// contract tag MG_Pipe/PipeMutation.h declared no texture birth hook and
// MG_Impl/Pipe/PipeFill.cpp's death helpers hard-coded `published = false`. Every one of
// them is now A's:
//
// * the four MINTS and the nine EMISSIONS are declared in PipeMutation.h and defined in
// PipeFill.cpp, which gates them on FamilyIsLive(bit, kMGPipeWiredTextureSubsystem) and
// forwards to this class through ForwardWhenWired. MGPipeEmitTextureParams in
// particular was a NAME COLLISION - the contract declares that exact signature - so
// keeping the inline definition here would not have compiled at all;
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
// resource_destroy themselves, so the destructors call ONE helper and not two - and
// since the final review's C-2 the helper then forwards to NoteTextureDied /
// NoteRenderbufferDied above, so no ITextureObject* survives its object in this table
// and no dead level survives on the drain list;
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
// PublishCreate above and read by those helpers.
//
// The self-healing create in EmitResourceRespecify stays this file's: c0b provides no such
// path and it is what repairs a texture born while the subsystem bit was clear.
} // namespace MobileGL::MG_Pipe
+64 -307
View File
@@ -9,54 +9,14 @@
#pragma once
#include <Includes.h>
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
// The frontend state tracker (ARCHITECTURE.md §5.2).
//
// WHERE IT RUNS. Not above MGP_FILL and not in the GL setter: MGPipeValidateForVerb, the
// one statement MGP_FILL already expands to before every gBackendFunctionsTable.GL call
// (PipeFill.h). Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), so a
// setter that pushed would push twice per batch for a state the batch may not even read;
// the validate point coalesces the whole bracket into the two draws that observe it
// (ARCHITECTURE.md 5.1).
//
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
// computed by comparing a shutter against what the tracker last pushed. P2 emitted for bits
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and P4a
// adds SEVEN: 6, 7 and 8 (the program family), 11 (the framebuffer) and 12, 13 and 14 (the
// three unit sets). Only bits 15, 16 and 17 - the const-buffer, shader-buffer and
// stream-output sets - are still computed, latched and counted without an emitter, so the
// per-bit fire rate is a measurement rather than a plan and their fields go through the
// residual fill until P4b.
//
// P4a NARROWS NOTHING AND WIDENS THREE THINGS, and every one of them was an UNDER-FIRE that
// only became reachable once the bit gained an emitter:
// (1) bit 11's shutter gains the READ framebuffer binding slot's version, because
// set_framebuffer_state is emitted per bound TARGET and a glBindFramebuffer(
// GL_READ_FRAMEBUFFER, ...) moved no shutter at all before;
// (2) bit 13's gains the TEXTURE BIND generation, because glBindSampler moves that one and
// not the sampling-resolution one, so bind_sampler_states could not see a sampler bind;
// (3) bits 6/7/8 - and with them bit 14's program half - read the EFFECTIVE program source
// instead of GetCurrentProgram() alone, which is null for the whole life of a bound
// separable program pipeline, so a re-composited pipeline reached no program emitter.
// Over-firing is free; all three of those were the other direction.
//
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
// dangerous direction precisely because the P1 verify comparator cannot see it for
// object-class state (it compares those by identity only). So each shutter below is
// deliberately coarser than the state it guards - five bits share one buffer aggregate,
// the framebuffer bit fires on any attachment write anywhere - and the narrowing is P3's
// work, paid for with the fire rates this file publishes.
//
// NO TIMER LIVES HERE. ROADMAP.md forbids committing hot-path instrumentation; the
// absolute ns/draw comes from DriverBench, which times whole frames from outside the
// library (P2 brief D17). The only counting is the per-bit fire tally, behind
// PipeStats::Enabled() like every other counting site in the tree.
//
// HEADER-ONLY, and that is an ownership decision rather than a design one: the P2 brief
// asks for Tracker.{h,cpp}, but the root CMakeLists.txt that would have to name a new .cpp
// belongs to package A and is frozen behind the p2/contract tag. Everything here is
// included by exactly one translation unit in the library (MG_Impl/Pipe/PipeFill.cpp) plus
// the unit tests, so inline costs nothing. Splitting it back out is one list(APPEND) line.
// Update() runs at the validate point, MGPipeValidateForVerb, which MGP_FILL expands to before
// every backend call (PipeFill.h), not in the GL setters: a glEnable/glDisable bracket around a
// batch then costs one push at the draws that observe it (ARCHITECTURE.md §5.1). It returns one
// MGPipeDirty bit per shutter that moved since the last push. A shutter may over-fire (one extra
// push) but must never under-fire (silently stale state, ARCHITECTURE.md §5.2.1), so each is
// coarser than the state it guards. Bit 17 is computed and counted but has no emitter.
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_State/GLState/Core.h>
@@ -239,27 +199,13 @@ namespace MobileGL::MG_Pipe {
}
}
// A COMPOSITE shutter, for the bits whose "did anything move" is more than one counter.
// It is a hash, so two different states can in principle collide and cost a MISSED fire.
// The five bits P2 emits for are never composed - they are widened counters and byte
// compares, neither of which can collide.
//
// P3a's three ARE composed, so the risk is now real rather than academic, and it is
// accepted with its size stated: two DIFFERENT states collide with probability ~2^-64 per
// pair, and the inputs are monotone lifetime ids, versions and generations - none of which
// an application can steer. The alternative, comparing the whole 32-attribute configuration
// byte for byte on every verb, is the per-draw cost the shutter exists to avoid. The
// narrowing that removes the composition for bit 10 - its own slot version plus the bound
// object's identity - is what this phase already did to the one shutter that was composed
// over an unrelated aggregate.
//
// That bound needs a mix that avalanches. The hash_combine step this used to be
// (acc ^= value + k + (acc << 6) + (acc >> 2)) does not: small neighbouring counters collide
// outright - (1, 64) and (2, 1) mix to the same value - and bit 12 missed a fire on exactly
// such a pair, so a draw sampled the previous set's texture
// (KHR-GL46.internalformat.copy_tex_image.rgb10_a2ui after a run of earlier cases). Each
// step here is a bijection of either argument with the other fixed, so a change to one
// input alone always moves the result, and the 64-bit finalizer spreads a change to both.
// The mix for composite shutters, built from several counters. It is a hash, so two states
// can collide and miss a fire; over lifetime ids, versions and generations an application
// cannot steer, that is about 2^-64 per pair, accepted instead of comparing the whole state
// on every verb. The mix must avalanche: a hash_combine-style step collides outright on small
// neighbouring counters ((1, 64) and (2, 1)), and a missed bit-12 fire then leaves a draw
// sampling the previous texture set. Each step is a bijection in either argument with the
// other fixed, so a change to one input alone always changes the result.
inline constexpr Uint64 MGPipeShutterAvalanche(Uint64 value) {
value ^= value >> 30;
value *= 0xbf58476d1ce4e5b9ull;
@@ -348,53 +294,14 @@ namespace MobileGL::MG_Pipe {
vao ? MGPipeMixShutter(vaoLifetime, vaoConfig) : 0;
now[Index(MGPipeDirty::NewVertexElements)] = vaoIdentity;
// Deliberately NOT GetProgramForDraw: that joins a pending link, and the tracker
// must not force a compile just to answer "did the shader move". These version
// counters are plain members and are exactly what the backends already read
// without joining (Core.cpp, the glUseProgram half of join site J1).
//
// BUT GetCurrentProgram() ALONE IS NOT THE PROGRAM SOURCE, AND AT P4a THAT IS AN
// UNDER-FIRE. Under GL_ARB_separate_shader_objects an application drives
// `glUseProgram(0); glBindProgramPipeline(P)`, and m_currentProgram is then null
// for the whole life of that pipeline (Core.cpp, GetProgramForDraw's second half):
// all three of these shutters read 0 == 0 forever, so after the first walk on a
// fresh context - the one !m_primed fires unconditionally - bits 6, 7 and 8 never
// fire again however the pipeline is restaged.
//
// WHILE NOTHING WAS EMITTED FOR THEM THAT WAS INVISIBLE, which is how it survived
// to P4a: GetProgramForDraw is emitted-and-still-pulled, the residual fill copies
// it at every verb, and DirtySurface.def rules BindProgramPipelineObject
// kPulledEveryVerb for exactly that reason - the backend still receives the right
// SharedPtr and nothing renders wrong. The moment P4a emits off these bits it
// stops being invisible: glUseProgramStages rebuilds the composite, EmitShaderState
// is never called again, so the new composite gets no ShaderCso handle and no
// create_shader_state while set_draw_program keeps naming the previous one - a
// program the handle protocol never announced, which is exactly the seam-defect
// class P3a spent a phase closing. And bit 8 never firing means
// set_global_constants is never sent for a pipeline draw at all, where the pull
// rescues nothing.
//
// SO THE SHUTTER READS THE EFFECTIVE SOURCE: the program in use when there is one,
// and the bound pipeline when there is not. What it reads OF that pipeline is the
// pair ComputeDrawProgramSignature() is built from - each stage program's lifetime
// id and LINK version - so bit 6 fires exactly when GetProgramForDraw would hand
// back a different composite, which is exactly when a new ShaderCso handle has to
// be minted. Those are the same non-artefact fields the plain-program arm above
// reads, and the ones Core.cpp calls out as not passing through ProgramObject's
// join gate, so the "must not force a compile" rule survives intact: no join, no
// flatten, no Link().
//
// THE PIPELINE NAME IS MIXED IN because two pipelines can carry the same stage set
// and each caches its OWN composite object, so the signature alone would let a
// glBindProgramPipeline between two such pipelines pass without a fire. What that
// does NOT close is a name RECYCLED (glDeleteProgramPipelines +
// glGenProgramPipelines) back onto the same stage programs at the same link
// versions with no other program-family change in between: a ProgramPipelineObject
// has no lifetime id and no wire object at all - DirtySurface.def says so where it
// rules MarkProgramPipelineForDeletion kUnpublishedDestroy - so there is nothing
// else here to mix it with. Recorded rather than quietly left: closing it needs a
// generation counter on the frontend object, which is an MG_State change and not
// this file's to make.
// Reads plain version counters, never GetProgramForDraw(): that joins a pending
// link, and the tracker must not force a compile. The source is the program in use
// or, when there is none (glUseProgram(0) with a separable pipeline bound), the bound
// pipeline's stage programs' {lifetime id, link version}, the pairs
// ComputeDrawProgramSignature() packs, so bit 6 fires exactly when the composite
// changes. The pipeline name is mixed in because two pipelines with the same stages
// cache separate composites. Known gap: a deleted pipeline name regenerated onto the
// same stages is not seen, since ProgramPipelineObject has no lifetime id to mix in.
const auto& program = ctx.GetCurrentProgram();
Uint64 shader = 0;
Uint64 bindings = 0;
@@ -453,23 +360,14 @@ namespace MobileGL::MG_Pipe {
"bits 7 and 8 read the four counters per graphics stage that "
"ComputeUniformMirrorVersions packs");
// Bit 6 is the pipeline's identity plus the composite cache key. Bits 7 and 8
// add the per-program state, which under a pipeline is written to the STAGE
// programs - glUniform* addresses the pipeline's active program,
// glProgramUniform* and the two block-binding calls address a named one - and
// only reaches the composite through RefreshCompositeUniforms. Bit 14's half
// takes the image-unit generation, which is its own counter for the reason
// ProgramObject gives (ES forbids glUniform1i on an image uniform, so Espryt
// BAKES the unit into the ESSL it generates and only a regeneration honours a
// change) and which D-G4 asks this shutter to keep reading as a FRONTEND
// counter rather than any server-side epoch.
//
// STAGELINKS IS MIXED INTO ALL THREE OF THE OTHERS, ON PURPOSE. A composite
// REBUILD hands back a brand-new ProgramObject with an empty default uniform
// block and no backend state at all - SetCachedDrawProgram clears the mirror
// versions with it - so a shutter watching only the per-stage state counters
// would let a rebuilt composite inherit the bindings, the constants and the
// image units of the one it replaced.
// Bit 6 is the pipeline identity plus the composite cache key. Bits 7 and 8
// add the stage programs' own state (glUniform* reaches the active program,
// glProgramUniform* and the block-binding calls a named one), which reaches
// the composite only through RefreshCompositeUniforms. Bit 14 reads the image
// unit version, a separate counter because Espryt bakes image units into the
// ESSL it generates (see ProgramObject). stageLinks is mixed into all of them:
// a rebuilt composite is a new ProgramObject with no uniforms or backend state
// and must not inherit the old one's bindings, constants or image units.
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
Uint64 stageState = 0;
Uint64 stageImages = 0;
@@ -501,76 +399,11 @@ namespace MobileGL::MG_Pipe {
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
now[Index(MGPipeDirty::NewGlobalConstants)] = constants;
// ===========================================================================
// THE RECORD-FIELD -> SETTER -> SHUTTER TABLE FOR THE SEVEN P4a BITS.
//
// THE RULE (P4a fable seam audit, section C.1): every field of every emitted
// record names the frontend setter that changes it, and that setter moves a
// counter the emitting bit's shutter reads - or the emission is unconditional at
// the setter (the resource_* family, set_texture_params). A record field whose
// setter moves no shutter input is a stale record with nothing to refuse: c0d
// (bit 13 without the bind generation), SD-0 (an image re-bind), F-1 (the
// program behind the view set), F-2 (the program behind the image window) and
// F-3 (an attached object's storage) were all this one class. DirtySurface.def
// cannot catch it - it maps MUTATORS to bits and cannot see that a DERIVED field
// depends on a mutator whose row is another family's - so the table lives here,
// beside the shutters, and a row is added whenever a record gains a field.
//
// bit 6 create/bind_shader_state, set_draw/dispatch_program (ProgramEmit.h)
// fields: Cso, StageMask, GlobalUboSize, the artefact blob refs, the two
// bound handles
// setters: glUseProgram (m_currentProgram), glLinkProgram (link version),
// glBindProgramPipeline / glUseProgramStages (pipeline name +
// per-stage {lifetime id, link version})
// shutter: lifetime id x link version, or stageLinks under a pipeline
// bit 7 the program's bindings (image units, block bindings, uniform write set)
// setters: glUniform1i on an opaque uniform (backend state version, image
// unit version), glUniformBlockBinding / glShaderStorageBlockBinding
// (block binding version), any glUniform* (uniform write set)
// shutter: the four per-program counters, x stageLinks under a pipeline
// bit 8 set_global_constants: ShaderCso, Version, the default-block image
// setters: any glUniform* on the default block (UBO content version),
// glUseProgram (lifetime id)
// shutter: lifetime id x UBO content version, or stageState
// bit 11 set_framebuffer_state: Fbo, Color[8]/Depth/Stencil/ReadSurface
// (Res, Kind, InternalFormat, TextureTarget, Layered, Level, Layer,
// UploadTarget), DrawBuffers[8], Width/Height/Layers/Samples/
// FixedSampleLocations, IsDefault, Complete, Target
// setters: glFramebufferTexture*/glFramebufferRenderbuffer, glDrawBuffer(s),
// glReadBuffer, glFramebufferParameteri (the attachment
// aggregate); glBindFramebuffer (the two binding slot versions);
// AND a storage redefinition of an ATTACHED texture or
// renderbuffer - glTexImage*/glTexStorage*/glTexBuffer/
// glTextureView/glRenderbufferStorage* - because InternalFormat,
// TextureTarget, the extent, Samples and Complete are INLINED at
// emission (D-C1): those bump the attachment aggregate from the
// object's PipePublishDescriptor (F-3)
// shutter: attachment aggregate x draw bind version x read bind version
// bit 12 set_sampler_views: per unit {View, Texture}
// setters: glBindTexture / glActiveTexture (bind generation), a texture's
// or a sampler object's parameters (SamplesAsIncompleteTexture -
// the params aggregate), an upload that defines a level (content
// aggregate), the default texture's image appearing (bind
// generation, TextureObject.cpp); AND the program in use -
// glUseProgram, a relink, glUniform1i on a sampler uniform (which
// unit a uniform's TYPE resolves) - F-1
// shutter: content x params x bind generation x opaqueUnits
// bit 13 bind_sampler_states: per unit the sampler CSO handle
// setters: glBindSampler (bind generation, c0d), glSamplerParameter* /
// glTexParameter* (params aggregate + sampling resolution),
// glDeleteSamplers (bind generation)
// shutter: params x sampling resolution x bind generation
// bit 14 set_shader_images: per unit {Res, InternalFormat, Layer, Level,
// Layered, Access} over the program's image-unit window
// setters: glBindImageTexture (bind generation, SD-0), a texture's
// content/params, glUniform1i on an image uniform (image unit
// version); AND the program in use - glUseProgram, a relink -
// F-2
// shutter: content x params x bind generation x programImages
// (lifetime id x link version x image unit version)
// bits 11-14 ALSO fire on the share group's twins of the aggregates they read, for
// a setter called through a sibling context (C8, after the dirty loop)
// ===========================================================================
// Every field of an emitted record must change only through setters that move an
// input of the emitting bit's shutter, or be emitted unconditionally at its setter
// (resource_*, set_texture_params). DirtySurface.def cannot check derived fields,
// so the record-field -> setter -> shutter table for bits 6-8 and 11-14 is kept by
// hand in ARCHITECTURE.md §5.2.2; add a row there when a record gains a field.
// ---- the object-class bits 9..17 ----
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
@@ -614,40 +447,14 @@ namespace MobileGL::MG_Pipe {
indexObject ? indexObject->GetLifetimeId() : 0);
}
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(vaoIdentity, indexShutter);
// Bit 11, WIDENED AT P4a AND THIS IS A REQUIREMENT RATHER THAN AN OPTION. The
// shutter observed the DRAW binding slot only, so glBindFramebuffer(
// GL_READ_FRAMEBUFFER, ...) moved nothing at all - which was harmless while
// nothing was emitted for the bit and is an UNDER-FIRE the moment P4a emits
// set_framebuffer_state per bound target (D-C2): the read record would never be
// sent and the server's ReadSurface would stay the previous framebuffer's. Over-
// firing costs one extra push; under-firing renders stale, and this file's own
// rule is that under-firing is the dangerous direction.
//
// A STORAGE REDEFINITION OF AN ATTACHED OBJECT MOVES THIS SHUTTER (P4a fable seam
// F-3), and the sentence that stood here - "a renderbuffer respecify is still
// invisible here, and deliberately so ... closed by emitting resource_respecify
// straight from the storage entry point" - was true of the RESOURCE record only.
// set_framebuffer_state inlines each attachment's InternalFormat, TextureTarget,
// extent, Samples and Complete (D-C1: "so the four cross-object masks fall out at
// push time with no lookup"), so `glTexImage2D(tex, RGB8); attach; draw;
// glTexImage2D(tex, RGBA8); draw` left the FRAMEBUFFER record saying RGB8 while the
// resource record said RGBA8, and the handle arm answered its alpha-widening,
// snorm-clamp and integer masks from the stale copy where the legacy arm re-read
// the frontend at the same re-sync - a proven arm divergence on a public-GL
// sequence. The fix is at the SETTER, not here: TextureObjectBase::PipePublish
// Descriptor and RenderbufferObject::PipePublishDescriptor - the one funnel every
// storage-defining entry point of either object takes, push-only - bump the
// attachment aggregate this shutter already reads. No counter is added to either
// object (G1), nothing widens this shutter onto the texture-content aggregate (which
// would fire the 304-byte record build on every glTexSubImage2D), and a storage
// definition of an UNATTACHED object over-fires it exactly once at load time.
//
// AND A TRAP THE NEXT NARROWING WOULD WALK INTO, recorded here because it is
// invisible from the shutter: FramebufferObject::SetDrawBuffer versions the VALUE
// being written rather than the index being written TO - it calls
// BumpAttachmentVersion(buffer). The object version and the aggregate still move,
// so THIS shutter is safe; a narrower one built on m_attachmentVersions would not
// be, and P4a must not build one.
// Bit 11 reads the draw and the read binding-slot versions, since the record is
// emitted per bound target. It inlines each attachment's format, target, extent,
// samples and completeness, so a storage redefinition of an attached object must fire
// it too: TextureObjectBase's and RenderbufferObject's PipePublishDescriptor bump the
// attachment aggregate (an unattached object over-fires once). The texture-content
// aggregate stays out; it would rebuild the record on every glTexSubImage*. Do not
// narrow this onto m_attachmentVersions: FramebufferObject::SetDrawBuffer bumps the
// version of the value written, not of the index.
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
MGPipeMixShutter(
ctx.GetAnyFramebufferAttachmentGeneration(),
@@ -665,67 +472,27 @@ namespace MobileGL::MG_Pipe {
now[Index(MGPipeDirty::NewSamplerViews)] = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), ctx.GetTextureBindGeneration()),
opaqueUnits);
// Bit 13, WIDENED AT P4a FOR BIT 11's REASON and found the same way. glBindSampler
// moves NEITHER half of what this used to read: GL_Sampler.cpp's BindSampler_State
// goes through NoteTextureUnitTouched and TextureUnit::SetSamplerObject, and both
// of those bump the TEXTURE BIND generation - bit 12's. The only two writers of
// BumpSamplingResolutionGeneration are PARAMETER changes (SamplerObject.cpp,
// TextureObject.cpp). So `glBindSampler(3, a); draw; glBindSampler(3, b); draw`
// fired bit 12 twice and bit 13 not once, and the server's BoundSamplerStates[3]
// went on naming a's CSO: wrong filtering, with nothing able to see it, because
// bind_sampler_states has no pulled twin to fall back on the way the view set does.
//
// MIXING THE GENERATION IN IS THE FIX RATHER THAN A SECOND GATE ON THE EMITTER,
// because that generation is what the unit SET is derived from: a sampler bind
// changes which sampler state applies at a unit, and a texture bind changes it too
// whenever the unit carries no sampler object and the texture's BUILT-IN sampler is
// what applies. Keeping it one shutter per bit is also what keeps the per-subsystem
// A/B and the per-bit fire tallies meaning what they say - a bit gated on another
// bit's shutter measures neither. The extra fires a plain texture bind now costs
// are swallowed by the emitter's own set-hash suppressor, which MGPipeTypes.h makes
// mandatory for every kVarTail set for this exact traffic.
// Bit 13 mixes in the texture bind generation because glBindSampler moves only that
// one (through NoteTextureUnitTouched and TextureUnit::SetSamplerObject); the
// sampling-resolution generation moves only on parameter changes. A texture bind can
// change the applied sampler too, when the unit has no sampler object. Mixing it in,
// rather than gating the emitter on bit 12, keeps the per-bit fire tallies meaningful;
// the extra fires are absorbed by the emitter's set-hash suppressor. A missed fire
// here means wrong filtering, since bind_sampler_states has no pulled fallback.
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
ctx.GetTextureBindGeneration());
now[Index(MGPipeDirty::NewShaderImages)] = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages),
ctx.GetTextureBindGeneration());
// ---- bits 15/16/17, REWRITTEN AT P5e (sb, docs/archive/contracts/CONTRACT-P5E.md §5.6) ----
//
// ALL THREE USED TO READ `buffers` - the buffer CONTENT aggregate - AND THAT WAS
// WRONG IN BOTH DIRECTIONS AT ONCE. Over: any glBufferSubData anywhere fired all
// three, which is the same width bit 10 was narrowed out of at P3a. Under, and this
// is the half that mattered: glBindBufferBase / glBindBufferRange mutate a binding
// point through a returned reference, which moves the slot's own Uint16 version and
// NOTHING the content aggregate reads - so
// `glBindBufferBase(UNIFORM,1,A); draw; glBindBufferBase(UNIFORM,1,B); draw` fired
// no bit at all. Harmless while nothing was emitted for these three; an
// under-fire the moment set_shader_buffers is, and under-firing renders stale,
// which this file's own rule calls the dangerous direction. Same defect class as
// P4a's glBindSampler hole (c0d), closed the same way: the shutter reads the
// generation the mutator actually moves (BufferState::NoteBindPointChanged).
//
// THE CONTENT AGGREGATE LEAVES ALL THREE and is not replaced by anything: whether
// the BYTES behind a bound buffer moved is the resource family's question, answered
// server-side by the resource record's own Serial, and the record these bits emit
// carries {handle, offset, size} - none of which a glBufferSubData changes. A base
// binding's extent is the one thing that could, and it does not travel resolved:
// Size is kMGPipeWholeBuffer and the server re-resolves it at use (§5.6).
//
// BIT 15 MIXES THE PROGRAM IDENTITY IN, for bit 12's and bit 14's reason one family
// over (fable seams F-1/F-2): the uniform window is resolved FOR THE PROGRAM IN USE
// - the UBO loop indexes it by the program's own block bindings - so a glUseProgram
// alone must re-open it. `shader` is that identity in both arms (lifetime id x link
// version, or stageLinks under a pipeline).
//
// BIT 16 IS TWO TARGETS, one bit: a storage-buffer bind and an atomic-counter bind
// are both "the shader's writable binding points moved", they are emitted together
// at the same validate point, and splitting them would buy one suppressed record on
// a workload that binds one without the other.
//
// BIT 17 KEEPS THE TRANSFORM-FEEDBACK GENERATION beside the new bind-point one:
// the capture points are span-scoped state latched at Begin, so when a span opens
// matters as much as what is bound. Nothing is emitted for it this phase (§5.7).
// Bits 15-17 read the buffer bind-point generations that glBindBufferBase/Range move
// (BufferState::NoteBindPointChanged), not the buffer content aggregate: the records
// carry {handle, offset, size}, a base binding's size travels as kMGPipeWholeBuffer
// and is resolved by the server at use, and content is the resource records' job.
// Bit 15 mixes in the program identity (`shader`) because the uniform window is
// indexed by the program's block bindings. Bit 16 covers storage and atomic-counter
// points, which are emitted together. Bit 17 adds the transform-feedback generation
// (capture points latch at Begin) and has no emitter.
now[Index(MGPipeDirty::NewConstBuffers)] = MGPipeMixShutter(
ctx.GetBufferBindPointGeneration(BufferTarget::Uniform), shader);
now[Index(MGPipeDirty::NewShaderBuffers)] = MGPipeMixShutter(
@@ -916,22 +683,12 @@ namespace MobileGL::MG_Pipe {
AttribDefaults& StagedAttribDefaults() { return m_stagedAttribs; }
const AttribDefaults& StagedAttribDefaults() const { return m_stagedAttribs; }
// ---- P3a D-H2: the draw's vertex-FETCH base instance ----
//
// It lives HERE rather than in a file static because bit 9's shutter has to see it:
// an ambient process global cannot cross a pushed boundary, and the value is now an
// explicit field of set_vertex_buffers and an input to its content hash, so a draw
// whose only change is its base instance has to reach the emitter. Set immediately
// before the fill at the three *BaseInstance draw entry points; CONSUMED and cleared
// by the validate point once it has been emitted, so a plain draw that follows one
// sees 0 again.
//
// THE CLEAR THAT ACTUALLY RUNS IN PRODUCTION IS THE VALIDATE POINT'S. MGPipeLeaveVerb
// clears it too, but no GL entry point calls MGPipeLeaveVerb - only MG_Test's
// ScopedPipeVerb and TrackerTest do - so the production guarantee is entirely
// PipeFill.cpp's, on BOTH of its exits: the end of step 3, and the no-live-context
// early return that skips step 3 altogether. Reset() deliberately does not clear it
// (see there): it is this call's argument, not a latch.
// The draw's vertex-fetch base instance, held here because bit 9's shutter mixes it in:
// it is a field of set_vertex_buffers and an input to its content hash. The three
// *BaseInstance draw entry points set it just before the fill. In production only the
// validate point clears it, on both exits of PipeFill.cpp (after emission and on the
// no-live-context early return); MGPipeLeaveVerb also clears it, but only tests call
// that. Reset() must not clear it: it is the current call's argument, not a latch.
void SetPendingBaseInstance(Uint32 baseInstance) { m_pendingBaseInstance = baseInstance; }
Uint32 PendingBaseInstance() const { return m_pendingBaseInstance; }
void ClearPendingBaseInstance() { m_pendingBaseInstance = 0; }
+5 -22
View File
@@ -78,28 +78,11 @@ namespace MobileGL::MG_Record {
void MarkWritableImageBufferTextures(MG_State::GLState::GLContext* context) {
if (!context) return;
// The backend keeps a bitset of writable image-buffer units
// (DirectGLES.cpp:2350-2352, maintained from its own SyncImageTextureBinding) and
// the client has no equivalent, so it sweeps. The sweep is NOT bounded by a device
// limit read: MaxImageUnits would be a backend read, and a stale or absent backend
// object would silently shorten the walk - which is the one direction this set may
// not fail in.
//
// IT IS BOUNDED BY THE FRONTEND's OWN IMAGE-UNIT HIGH-WATER MARK (P5d round 3,
// package C). The paragraph that stood here said the narrowing "belongs with P8's
// binding-walk migration"; the 2026-09-17 inproc profile moved it forward -
// MarkWritableImageBufferTextures was 2.15% self / 3.59% inclusive of the GL thread
// (GetImageTextureBinding 1.37 of it) at ~852 draws/frame, in a workload that never
// binds an image at all, purely because the walk was MAX_TEXTURE_IMAGE_UNITS (192)
// units wide unconditionally - 192 GetImageTextureBinding reads per draw for a
// context that has never bound one.
//
// AND THE MARK IS SAFE IN THE ONLY DIRECTION THAT MATTERS. It is written by
// glBindImageTexture itself (GL_Texture.cpp, TextureState::NoteImageUnitTouched) and
// it only ever grows - an unbind, a delete-unbind, a context that stops using an
// image unit all leave it where it was - so `unit <= mark` can only be WIDER than
// the set of units that hold a binding, never narrower. -1 means no image unit has
// ever been bound in this context, and then this is one compare.
// The client has no bitset of writable image-buffer units, so it sweeps them, bounded
// by the context's image-unit high-water mark rather than a backend limit, which a
// stale or absent backend object could shorten. glBindImageTexture sets the mark and
// it only grows (unbinding leaves it), so it never excludes a bound unit; -1 means no
// image unit was ever bound and the walk is one compare.
const Int highest = context->GetMaxTouchedImageUnit();
for (Int unit = 0; unit <= highest && unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
++unit) {
+7 -47
View File
@@ -6,53 +6,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// THE CLIENT-SIDE CONSERVATIVE GPU-WRITE SET (ARCHITECTURE.md §16 names this component;
// CONTRACT-P5.md section 3, table 2's first set). Owner: package b1.
//
// WHY IT HAS TO MOVE SIDES. `BufferObject::SyncGpuWrites` (BufferObject.cpp:369) runs
// synchronously, on the application's thread, the moment the application calls glMapBuffer or
// glGetBufferSubData. It has to answer "did the GPU write this buffer since I last read it?"
// - and the six backend sites that rows 0-3 below mirror are all on the server's side of a split
// (DirectGLES.cpp's SyncBufferBindingPointsByRecord, SyncAtomicCounterBuffers and
// MarkWritableImageBufferTexturesGpuWritten; UniformManager.cpp's ResolveStorageTexelBufferDescriptor
// and ResolveStorageBufferDescriptor; WireDraw.inc's BeginWireXfbCaptureForDraw). Asking the server
// is a round trip the design forbids in steady state, so the
// client must build the set itself, from state it already owns: the SSBO and atomic-counter
// binding points, the image units whose Access is not GL_READ_ONLY, and the transform-feedback
// capture targets.
//
// CONSERVATIVE MEANS OVER-APPROXIMATE, AND THAT IS THE WHOLE DESIGN. The reverse channel's
// OnGpuWritten is a NARROWING channel (ResourceTracker.h:577-581): the client marks
// everything a shader COULD have written and the server only ever removes entries. So a row
// here that fires too often costs a readback; a row that fires too rarely reads a stale shadow
// and is silent. Every row below therefore mirrors its backend twin exactly, including the two
// places the twin is deliberately NARROW - a GL_READ_ONLY image binding is left alone, and the
// image walk runs from draw preparation rather than from glBindImageTexture's eager sync.
//
// NO NARROWING IN P5. ResourceTracker.h:587-592's `rangeCount == 1` assertion STAYS. Zero
// ranges will mean "a fully narrowed set - nothing is dirty" at P8/P9, and a package that
// reads ResourceTracker.h:577-581 alone will think it owns that already. It does not.
//
// THE TWO NEW PRODUCERS. Rows 4 and 5 have no backend twin: they are behaviour P5 ADDS
// (ARCHITECTURE.md §13.1), and both are strictly better than what monolith does.
// * glReadPixels into a pack PBO becomes fire-and-forget plus a client-side mark. Monolith
// maps the PBO and copies it into the shadow inside the call (DirectGLES.cpp:10983-10993),
// which is an unconditional stall on every glReadPixels whether or not anyone reads the
// shadow; marking instead defers the cost to the first read that actually wants it.
// * glEndTransformFeedback drops its unbounded ClientWaitSync (GL_Drawing.cpp:1367, timeout
// ~0ull) and marks the capture targets, for the same reason: the wait exists only so that
// a later MapBuffer sees real results, which is precisely what the flag is for.
//
// WHAT NO ROW COVERS, WRITTEN DOWN SO THE NEXT READER DOES NOT HAVE TO ASK. The set is built
// from the APPLICATION's bindings, so it says nothing about a backend's own scratch buffers -
// Espryt's converted-vertex-stream and primitive-restart substitution buffers, Magma's UBO
// ring. None of those has a MarkGpuWritten today either, so the client set is no NARROWER than
// monolith's and this is not a regression; it is a standing hole in both, and it stays one
// until the phase that migrates the backend's own allocations.
//
// GATED ON THE TRANSPORT, like everything else in this package: on the monolith path the six
// backend sites still run and a second marker would be new behaviour (D-J), and rows 4 and 5
// would remove a stall monolith is entitled to keep.
// Client-side conservative GPU-write set (ARCHITECTURE.md §13.1.2). glMapBuffer and
// glGetBufferSubData ask BufferObject::SyncGpuWrites, on the application thread, whether the GPU
// may have written a buffer; the backend's marking sites run on the server, so the client marks
// from its own bindings before each draw or dispatch record goes out. The set over-approximates:
// a missed mark reads a stale shadow silently, while an extra one costs a readback. Rows with a
// backend twin mirror it, including where the twin is deliberately narrow; the atomic-counter
// row is wider, in the safe direction. Backend-owned scratch buffers are not covered.
// P13 W5: moved from MG_Remote/Client/GpuWritePending.h - the record arm owns this set in a
// library without a transport too. The session's waits stay there.
@@ -52,22 +52,13 @@
"Pixel packing exceeds the destination or its addressable range."));
}
// P9 (W1, docs/archive/contracts/CONTRACT-P9.md §1): MAY A READ INTO THIS PACK BUFFER GO WITHOUT A
// REPLY? The answer is the buffer's handle when it may - the read then crosses as
// read_pixels_to_buffer / get_texture_image_to_buffer, lands server-side, and the buffer
// is marked GPU-written - and the null handle when the reply-slot form has to carry it.
// `end` is the last byte past the application's offset the read touches.
//
// Each refusal is a case the mark cannot keep its promise in, not a preference:
// - MOBILEGL_IPC_PBO_READBACK_SYNC=1, the A/B and red-once arm;
// - no readback route (BufferWritebackIsReachable: no size, or the resource family
// is not on the wire), so SyncGpuWrites would clear the mark with nothing fetched;
// - a MAPPED buffer. Only a persistent mapping can be live across a read, and its
// reader takes the bytes straight from the pointer through no call this side sees:
// no SyncGpuWrites would ever run for it, and the pixels would never arrive;
// - a range MGPSubData cannot name (a buffer offset stops at 2^31-1);
// - a buffer that never crossed: it has no handle for the server to write into, and
// the reply form's UploadSubData is what creates it.
// Returns the pack buffer's handle when a read into it may skip the reply: the read
// then crosses as read_pixels_to_buffer / get_texture_image_to_buffer, lands
// server-side, and the buffer is marked GPU-written. Returns null, selecting the
// reply-slot form, when that mark could not be honoured: MOBILEGL_IPC_PBO_READBACK_SYNC=1;
// no readback route (BufferWritebackIsReachable); a mapped buffer, whose persistent
// reader never goes through SyncGpuWrites; an `end` (last byte past the application
// offset) above MGPSubData's 2^31-1 offset limit; or a buffer with no handle yet.
MG_Pipe::MGPipeHandle PackBufferReadbackTarget(const SharedPtr<MG_State::GLState::BufferObject>& pbo,
Uint64 end) {
if (!pbo) return MG_Pipe::kMGPipeNullHandle;
+34 -111
View File
@@ -88,30 +88,12 @@ namespace MobileGL::MG_Record {
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
// ---- WHY THERE ARE TWO OF THESE KNOBS, measured rather than assumed -----------------
//
// MOBILEGL_IPC_E2_DROP_CLEAR came first and it does exactly what it says: every glClear
// stops at the client and no Clear record reaches the ring. It STILL COULD NOT TURN THE
// E2 RETRACE RED (joint-v1.md 3: SSIM 1.000000, mismatchPixels=0 with the knob armed and
// the arming WARN in the library's own log). That is not a broken knob, it is OpenRA:
// `apitrace dump` of openra.trace over the 31249 replayed calls counts 30 glClear, 30
// glXSwapBuffers and 788 glDrawArrays, and the final frame issues its clear at call
// 30197 and then covers the surface four times over with a terrain layer
// (`glDrawArrays(GL_TRIANGLES, first=56064, count=16128)` x4, under a scissor of
// -24,-24,688x528 over a 640x480 surface) before the snapshot at 31249. A frame that
// overdraws every pixel it clears has a picture that does not depend on the clear, so
// "drop the clear" is a control whose observable is invisible to THIS trace - R-16's
// exact defect, a gate that cannot go red for its own reason.
//
// MOBILEGL_IPC_E2_DROP_DRAW is the honest form of the same statement for a trace lane:
// drop every DrawVbo record and the picture can only be the clear colour. It is the
// control that makes "the wire carried the frame" falsifiable, because the thing it
// removes is the thing the golden is made of.
//
// DROP_CLEAR IS KEPT rather than retired: it drops a real record, it now publishes the
// count it dropped (below), and a scenario whose picture DOES depend on its clear -
// ClearThenReadPixelsScenario is the reduced path's target A - is where it is
// observable. What it is no longer allowed to be is E2's retrace control.
// Two negative controls. MOBILEGL_IPC_E2_DROP_DRAW drops every DrawVbo record, so a
// frame that really crosses the wire can show only the clear colour; it is the OpenRA
// retrace's control. MOBILEGL_IPC_E2_DROP_CLEAR cannot be: OpenRA's last frame clears,
// then covers the whole 640x480 surface four times before the snapshot, so dropping the
// clear leaves SSIM at 1.0 (measured). It stays for scenarios whose picture depends on
// the clear, such as ClearThenReadPixelsScenario.
void ArmControlKnobs() {
g_dropClearEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_CLEAR");
g_dropDrawEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_DRAW");
@@ -591,29 +573,14 @@ namespace MobileGL::MG_Record {
EmitDrawRecord("DrawArrays", info, &range, 1, nullptr, 0, nullptr);
}
// =============================================================================
// P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots
// (docs/archive/contracts/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL
// call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink
// reproduces the backend call the monolith makes.
// =============================================================================
//
// Every entry point below is: read the bindings, plan the head and the ranges (the pure
// functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb
// hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record;
// ID-18), honours the E2 draw-drop control for every draw record and not only the P5
// one, stages a client index array into SEG_STAGE when there is one, and emits.
//
// Client arrays and client indices become owned ordinary buffer resources.
// Remaining invalid/unrepresentable draw shapes are named before encoding:
// +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect`
// would be a host pointer, which rule B forbids on the wire.
// +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has
// already raised INVALID_OPERATION for it; stated so the emitter
// cannot send a null handle where the sink dereferences one).
// +INDEX_OFFSET an element-buffer byte offset that is not a whole number of
// indices (or past 2^32 of them): the record spells Start in
// indices, and rounding would draw from the wrong element.
// ---- Indexed, instanced, multi-draw and indirect draws ----
// All nineteen slots emit one draw_vbo record, the GL call verbatim beside the handles,
// which the server's sink turns into the backend call the monolith makes. Each entry
// point reads the bindings, plans the head and ranges, then calls EmitDrawRecord: the
// pre-verb hooks in EmitDrawArrays' order (push, mark walk, record), the drop-draw
// control, client arrays and indices as owned buffers. Refused by name: +CLIENT_COMMANDS
// (no indirect buffer), +UNBOUND_PARAMETER (no parameter buffer; the sink dereferences
// it) and +INDEX_OFFSET (a byte offset that is not whole indices; Start counts indices).
[[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) {
char name[96];
@@ -974,28 +941,11 @@ namespace MobileGL::MG_Record {
nullptr, 0, nullptr);
}
// How many bytes glReadPixels will pack for this rectangle, from the PACK half of the
// pixel-store state.
//
// ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT.
// The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips,
// alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and
// the real backend honours the live pack state, so a 4x3 RGBA8 read with
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote
// to byte 120. That is the shape of the joint inproc lane's two
// DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends.
//
// So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack
// state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT -
// which is the side that holds the application's pack state, and the only side that
// can - scatters those rows into the application's pointer. "OnReadPixels writes
// exactly DstSize bytes" still holds; DstSize is now a number both sides derive from
// the same three values instead of one side deriving it from state the other cannot
// see.
//
// IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split
// entries touch take the same rule with no special case, because the rule is about the
// LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to.
// Bytes per pixel of a glReadPixels format/type pair, for the tight extent. The pack
// state never crosses for a read: DstSize is the tight w * h * bytesPerPixel run the
// server reads with neutral pack state, and the client scatters it under the
// application's GL_PACK_* state (ARCHITECTURE.md §13.1.3). The rule is about layout,
// so depth and depth-stencil reads need no special case.
Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) {
const TextureInputFormat inputFormat =
MG_Util::ConvertGLEnumToTextureInputFormat(format);
@@ -1284,27 +1234,13 @@ namespace MobileGL::MG_Record {
});
}
// =============================================================================
// CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block
// (docs/archive/contracts/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows.
// =============================================================================
//
// RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record
// carries the GL arguments verbatim - the enums as tokens, the GL names the backend
// keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's
// ServerVerbSink reproduces the backend call the monolith makes. The backend keeps
// reading the frontend state it reads today through the BARRIER-PULLED fields of its
// verb class, which the record's own verb stamp is what makes legal. So a migration is
// one emitter here plus one sink body there, and nothing in the backend moves.
//
// THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks
// below dispatch on the GL NAME - that is the whole point of carrying it - so a handle
// is carried for P7's sake only. FindByLifetimeId answers the handle the resource
// subsystem has already published for this object and kMGPipeNullHandle when it has
// published none; Acquire would MINT one here instead, at a call site that emits no
// create record, and the server would then be handed an identity it has never seen.
// A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a
// true statement, rather than a slot nobody allocated.
// ---- Image bind, compute, barriers, copy-image, storage-block binding ----
// Each verb crosses as the call: the record carries the GL arguments verbatim (enum
// tokens, GL names) beside the objects' handles, and the server's verb sink makes the
// backend call the monolith makes. Handles are looked up with FindByLifetimeId, never
// minted: this call site emits no create record, so a minted handle would name an
// object the server has never seen. A null Res/Src/Dst/ShaderCso means none is
// published yet.
MG_Pipe::MGPipeHandle PublishedTextureHandle(
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
@@ -1512,27 +1448,14 @@ namespace MobileGL::MG_Record {
nullptr, 0, nullptr, 0, nullptr);
}
// CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the
// tessellation patch parameter (docs/archive/contracts/CONTRACT-P5B.md §2 t2).
// =============================================================================
//
// SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a
// control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets
// the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(),
// which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no
// shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE
// TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span,
// after the record and before GLContext::EndTransformFeedback clears the live bindings,
// which is exactly where b1 already put it (GL_Drawing.cpp's
// MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same
// buffers twice and taking it INSTEAD of there would mark nothing.
//
// P5f fe extends Begin with the immutable capture snapshot: program CSO handle,
// XFB object lifetime id, and four buffer handle/range pairs. Buffer resource records
// precede this verb through BeforeReadOnlyVerb; AcquireShaderCso below publishes the
// program archive before Begin. Deferred driver Begin and End therefore need no
// frontend program or binding-point lookup. set_stream_output_targets stays unused:
// this state changes at the capture-span boundary, so it rides Begin itself.
// ---- Transform-feedback spans, XFB object bind, patch parameter ----
// These six start no shader (they open, close, pause, resume or retarget a capture
// span, or set the patch size), so each takes BeforeReadOnlyVerb(). The capture
// targets' GPU-write mark belongs at the end of the span, where GL_Drawing.cpp calls
// MarkEndTransformFeedbackCaptureTargets before the bindings are cleared; marking here
// too would mark twice. Begin carries the capture snapshot (program CSO, XFB lifetime
// id, four buffer ranges), so the deferred driver Begin/End need no frontend lookup;
// the state changes only at span boundaries, so set_stream_output_targets is unused.
void EmitBeginTransformFeedback(GLenum primitiveMode) {
VerbChannel session = ChannelFor("BeginTransformFeedback");
+13 -40
View File
@@ -81,23 +81,12 @@ namespace MobileGL::MG_Record {
// Returns the previous routing.
Bool SetMonolithVerbPortRoutingForTesting(Bool viaPort);
// THE E2 NEGATIVE CONTROLS (t1's debt against c1, BRIEF §7). When set, the named emitter
// SKIPS its record - it still runs the pre-verb hooks and still returns - so a replay that
// is really going through the wire loses that verb while a replay that fell through to the
// driver is unaffected. They are functions rather than knobs in Config.h for two reasons:
// a control has to be settable from a test process that has already started, and a knob
// would be a MOBILEGL_IPC_-shaped name for something no operator may ever set.
//
// Emissions actually skipped, so a control can assert that it DID something rather than
// that a picture changed - a control that silently never fired is the third shape of R-16's
// "a gate that cannot go red for its own reason".
//
// WHICH ONE E2'S RETRACE USES, and it is not the clear. Measured on the joint head: with
// MOBILEGL_IPC_E2_DROP_CLEAR=1 armed and its WARN in the library's own log, the OpenRA
// retrace under inproc still scored ssim=1.000000 / mismatchPixels=0 (joint-v1.md §3),
// because OpenRA covers every pixel it clears before the snapshot. Dropping the DRAWS is
// the control whose observable the golden is actually made of. See VerbPort.cpp's
// ArmControlKnobs for the trace census that settled it.
// Negative controls for the retrace lanes. When set, the named emitter skips its record but
// still runs the pre-verb hooks, so a replay that really goes through the wire loses the
// verb while one that fell through to the driver does not. Functions rather than Config.h
// knobs: a running test process sets them, and no operator may. The counts let a control
// assert that it fired. The OpenRA retrace uses the draw drop: OpenRA overdraws every pixel
// it clears, so dropping the clear leaves SSIM at 1.0 (see ArmControlKnobs in VerbPort.cpp).
void SetDropClearEmissionForNegativeControl(Bool drop);
Uint64 DroppedClearEmissions();
void SetDropDrawEmissionForNegativeControl(Bool drop);
@@ -126,29 +115,13 @@ namespace MobileGL::MG_Record {
GLsizei height, Uint64 bytesPerPixel,
const PixelStoreParameters& pack);
// P7 gate 5 (g5-readback). A glReadPixels whose tight answer is larger than one reply slot
// is NOT one record: the client splits the w*h rectangle into BANDS, emits each band as an
// ordinary read_pixels record with its own box and its own tight DstSize (so every answer
// still fits one slot, ID-47, and the server's PH-3 bound - its tight answer against its own
// LinkTerms.maxReplyBytes - holds unchanged), and scatters each band into the application's destination under its
// own GL_PACK_* state. No reply is chunked; the READ is.
//
// THE SHAPE. Whole-width row bands of floor(maxReplyBytes / rowBytes) rows when a row fits
// a reply; otherwise every row is cut into column pieces of floor(maxReplyBytes / bpp)
// pixels. Both shapes are contiguous in the tight layout, which is what lets the neutral-
// pack fast path keep reading straight into the application's pointer. Bands are in
// row-major order and cover the rectangle exactly once.
//
// NO PLAN exactly when not even ONE pixel fits a reply (maxReplyBytes < bytesPerPixel,
// including a link with no reply pool at all); the emitter then refuses BY NAME through
// ID-47's unchanged helper rather than emitting anything. That is the only ReadPixels shape
// left that ends in Fatal{ReplyTooLarge}: CONTRACT-P5 row 23 makes an answer that cannot fit
// a slot a named Fatal, not a GL error, and no pixel can be returned in pieces smaller than
// itself.
//
// THE PLAN IS TWO NUMBERS AND THE BANDS ARE WALKED, NOT LISTED: a band list would be one
// allocation per read that grows with the read, and the walk below is the only place the
// order and the coverage are decided - the emitter and the unit control both call it.
// A glReadPixels whose tight answer exceeds one reply slot is split into bands, each an
// ordinary read_pixels record with its own box and tight DstSize, scattered under the
// application's pack state (ARCHITECTURE.md §13.1.2). Bands are whole-width row runs when a
// row fits a reply, else column pieces of one row; they go in row-major order and cover the
// rectangle once. No plan means not even one pixel fits, and the emitter refuses by name
// (Fatal{ReplyTooLarge}). The plan is two numbers and bands are walked, not listed, so a
// read allocates nothing and the emitter and its unit test share one walk.
struct ReadbackBand {
Uint64 FirstRow = 0;
Uint64 Rows = 0;
+21 -62
View File
@@ -10,33 +10,13 @@
#include <Includes.h>
#include <Config.h>
// The CLIENT side of P3a's vertex-input family (brief D-G, D-H, D-I): the bound VAO's
// format as create/bind_vertex_elements, its buffers as set_vertex_buffers with an explicit
// baseInstance, and its element binding as set_index_buffer.
//
// UNLIKE THE RESOURCE FAMILY, these three emit at the VALIDATE POINT, from
// MGPipeValidateForVerb's step 3 in the fixed order elements -> buffers -> index. That is
// the ordinary rule (ARCHITECTURE.md 5.1); the resource family is the one exception to it.
//
// THE CSO IS IDENTITY-ADDRESSED, NOT CONTENT-ADDRESSED (D-G1, a recorded deviation from
// ARCHITECTURE.md's 1024-entry content-addressed scheme). One handle per frontend
// VertexArrayObject, minted off its lifetime id, and create_vertex_elements is RE-ISSUED on
// the same handle whenever the configuration moves - legal, because MGPipeHandle::Gen
// increments only on slot reuse and never on a respecify. Espryt has no vertex-elements CSO
// to share: its twin owns one driver VAO name plus 64 scratch buffer ids, which two frontend
// VAOs cannot share, so content addressing would be strictly slower on the only backend this
// phase touches. P7 adds the hash-probe-memcmp layer above these same three calls when
// Magma's VertexInputStateFactory takes the CSO over.
//
// WHAT THE UNIT GATE READS. G6 is "the emitted blob + set + index record reproduce exactly
// what the backend's VAO twin reads from the frontend today, field by field, for all 32
// slots", and G7 is a scripted control that stops the conversion copying ONE field and
// expects the suite to go red NAMING it. So the conversion is a pure function per field
// (MGPipeBuildVertexAttribWire / MGPipeBuildVertexBindingPointWire) and the staging buffers
// the emitter builds into are readable afterwards - the emitter passes m_blob and m_entries
// straight to the applier, so "what was emitted" costs no copy at all.
//
// HEADER-ONLY, for the ownership reason Tracker.h states in full.
// Client side of the vertex-input family: the bound VAO's format as create/bind_vertex_elements,
// its buffers as set_vertex_buffers (with an explicit baseInstance) and its element binding as
// set_index_buffer, emitted at the validate point in that order. The vertex-elements CSO is
// addressed by VAO identity, not content (ARCHITECTURE.md §2.3): one handle per VAO lifetime id,
// with create_vertex_elements re-issued on it when the configuration moves, because Espryt's
// twin owns a driver VAO that two VAOs cannot share. The wire conversion is one pure function
// per view, and the emitter builds into members tests can read back without a copy.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -57,25 +37,14 @@ namespace MobileGL::MG_Pipe {
// D-G2: the wire conversion, one pure function per view
// ---------------------------------------------------------------------------------
// EVERY FIELD OF VertexAttribute THE WIRE FORM CARRIES, and nothing else:
//
// Divisor is deliberately absent - it is resolved per binding point and travels in
// MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads
// it. Carrying it twice would let a malformed record disagree with itself.
// LegacyStride / LegacyPointer are deliberately absent - they are the
// glGetVertexAttrib* query answers and nothing but the query path reads them, so
// they stay client-side.
// Buffer is deliberately absent - identity travels in set_vertex_buffers, which is
// what keeps this record stable while the buffers under it change.
// Stride is the RESOLVED distance and a surviving 0 is MEANINGFUL: a pointer call's 0
// was already resolved to the element size by the frontend, so a 0 here can only
// have come from the binding model, where it means every vertex reads the SAME
// element. Collapsing it back into the element size is what made
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
// IsLong travels SEPARATELY from Type == Float64: VertexAttribFormat(GL_DOUBLE) reads
// doubles and asks for them converted to float, VertexAttribLFormat keeps all 64
// bits, and the backend's fp64 narrowing and its Adreno disabled-attribute
// workaround both key on telling the two apart.
// The wire form carries every VertexAttribute field except Divisor (sent once, per binding
// point, in MGPVertexBuffer::Divisor), LegacyStride/LegacyPointer (query answers, client-only)
// and Buffer (identity travels in set_vertex_buffers, so this record stays stable while the
// buffers change). Stride is the resolved distance: a 0 here comes only from the binding
// model and means every vertex reads the same element, so it must not become the element
// size. IsLong is separate from Type == Float64: VertexAttribFormat(GL_DOUBLE) converts to
// float, VertexAttribLFormat keeps 64 bits, and the backend's fp64 narrowing and Adreno
// disabled-attribute workaround tell the two apart.
inline MGPVertexAttribWire MGPipeBuildVertexAttribWire(const MG_State::GLState::VertexAttribute& attrib,
Uint32 bindingIndex) {
// ASSERT RATHER THAN ASSUME, in both directions, because the three narrowing casts
@@ -425,22 +394,12 @@ namespace MobileGL::MG_Pipe {
Uint64 VertexBufferSetCount() const { return m_bufferSets; }
Uint64 IndexBufferSetCount() const { return m_indexSets; }
// ---- C-1: "does the applier hold a record for exactly this handle?" ----
//
// The CSO's death path (MGPipeEmitVertexElementsDestroyAndFree) needs that answer and
// MUST NOT GUESS IT FROM THE SLOT. A VertexElementsCso slot can exist with no record
// behind it, because a backend that keys its twins on the handle mints the slot itself
// (DirectGLES' BackendSlotTable::GetOrCreate -> MGPipeSlots().Acquire) whether or not
// bit 8 ever asked this client to emit anything - which is exactly what a
// MOBILEGL_PIPE_PUSH=0x7f lane runs. delete_vertex_elements on such a handle is a
// REFUSED call, and the applier's resolver asserts on a refusal
// (PipeApply.cpp's ResolveVertexElements), i.e. a stop in a verify build.
//
// Kept OUT of Reset(), unlike the create/bind latch beside it, and for the mirror
// image of Reset()'s own reason: "a fresh context is a fresh server" is true of the
// per-context half of this table, and object RECORDS are precisely what
// MGPipeApplierReset does not clear (PipeApply.h's two halves). This half tracks those
// records, so it lives exactly as long as they do.
// Whether the applier holds a vertex-elements record for exactly `handle`. The death
// path (MGPipeEmitVertexElementsDestroyAndFree) must ask this instead of inferring it
// from the slot: a backend that keys its twins on handles (DirectGLES'
// BackendSlotTable) can mint a VertexElementsCso slot with no record behind it, and
// delete_vertex_elements on such a handle is refused, which the applier asserts on.
// This half of the latch survives Reset(), because MGPipeApplierReset keeps records.
Bool RecordIsPublished(MGPipeHandle handle) const {
if (MGPipeHandleIsNull(handle)) return false;
const SizeT slot = handle.Slot;
+2 -2
View File
@@ -738,8 +738,8 @@ namespace MobileGL::MG_Remote::Client {
// this handle". That is a fact about the SERVER's table, and the client already keeps its
// own copy of exactly that fact - MGPipeHandleIsPublished, set when the object's create was
// ACCEPTED and never cleared while the record lives (the applier keeps texture records
// across a make-current: PipeFill.cpp:3756-3759). So the wait is asking the server a
// question the client has already answered.
// across a make-current: MGPipeApplierReset clears only MGPipeWorkingState). So the wait
// is asking the server a question the client has already answered.
//
// WHAT REPLACES IT, on the device run's numbers: this row paid 18,056 of one frame's
// 43,232 round trips at ~5 ms each, for records carrying a few hundred bytes. The object's
@@ -49,3 +49,10 @@ client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明:
- CSO(render state、vertex elements、sampler、sampler view、shader)在 **client 侧内容寻址**(Mesa `cso_cache` 先例),LRU 淘汰时发 `delete_*`;render-state 容量 64(P13 重调)、sampler state 256。
- Buffer / Texture / Renderbuffer 走 create / respecify / subdata / destroy;Framebuffer / Xfb 是 per-context 身份 + `set_*` payload。
- 两条偏离:**D-G1** Espryt 的 vertex-elements 按 VAO 身份寻址(twin 持驱动 VAO 名,不能共享);**D-F2** Espryt 的 sampler view 按纹理身份寻址,sampler state 内容寻址且带引用计数(被 `BuiltinSampler` 指着的项不许被 LRU 挤掉)。
#### 2.3.1 sampler state 缓存:引用计数与值生命周期
sampler state 在 client 侧按值寻址(`MGPipeSamplerCsoCache`,容量 256):哈希、探测、再 memcmp 确认;只凭 64 位哈希相等会把两种状态并进一个 CSO,造成没有门看得见的错误过滤。
- 活句柄躲开 LRU 靠引用计数而不是容量:已发布的 `MGPTextureParams` 在纹理参数不动时一直指着它的 `BuiltinSampler`,applier 不解析这个句柄,而淘汰不算参数变化,既不会重发也没人拒绝。所以每次 `Acquire` 取一个引用、`Release` 还一个;LRU 只考虑引用为 0 的项;全被钉住时超容量铸造并计入 `OverCapacityMints`(多一个槽费内存,句柄被抽走费正确性)。
- 持有引用的是纹理的内建 sampler(纹理死亡或参数变化重取时释放)和 `bind_sampler_states` 当前绑定的每个 CSO;凡在返回后还会指名该句柄的调用方都必须持有。容量大于 `kMGPipeMaxTextureUnits` 只是尺寸界:装不下一整轮发射的缓存会每轮都超容量铸造。
- 槽不带 lifetime id:CSO 属于值而不属于前端对象(别的 `SamplerObject` 可能持有同值),`~SamplerObject` 不释放任何槽,唯一死亡路径是 client 侧的 LRU 淘汰。因此来自 `~SamplerObject` 的 `NotifyStateObjectDestroyed(SamplerCso, lifetimeId)` 背后永远没有句柄;后端不得按 `SamplerObject` 的 lifetime id 键控 sampler twin,twin 的生命只是 `create_sampler_state` 到 LRU 发出的 `delete_sampler_state`。
+30
View File
@@ -25,6 +25,22 @@ per-setter 推送会把 Blaze3D 每个 batch 的冗余 `glEnable/glDisable` 变
位答案由脚本推导:RenderState 族与 `RenderState.cpp` 双向比对(`BumpVersions()` 动两个计数器,裸 `++m_version` 只动 `NEW_RENDER_STATE`);其余位把快门读的成员 / 字段与 mutator 传递写入的成员 / 字段求交,只能查出"该位的快门在任何路径上都不动"这一种错答,证明不了每条路径都动。写入根定位不了的函数被污染,依赖它的答案为 UNDECIDED,须列入 `MGP_DIRTY_SURFACE_UNDECIDED_LIST`,列了却已可判定同样报红。散文答案不经推导(`kPulledPartialShutter` 所附的位除外);扫描器按文本匹配函数体,lambda 里的 mutator 记到外层函数上,经 helper 发布的修改读作延迟发布,语义证明仍是 `MOBILEGL_PIPE_VERIFY`。
#### 5.2.2 记录字段 → setter → 快门对照表
由 `Tracker.h` 的 dirty 位触发的每条记录,其每个字段只能经由会推动该位快门输入的 setter 改变,否则必须在 setter 处无条件发射(`resource_*` 族、`set_texture_params`)。`DirtySurface.def` 只把 mutator 映射到位,看不出派生字段依赖另一族的 mutator,所以下表手工维护;记录新增字段时在此加一行。
| 位 | 记录与字段 | 改变字段的 setter | 快门输入 |
|---|---|---|---|
| 6 | `create/bind_shader_state`、`set_draw/dispatch_program`:Cso、StageMask、GlobalUboSize、artefact blob ref、两个已绑定句柄 | `glUseProgram`、`glLinkProgram`、`glBindProgramPipeline`、`glUseProgramStages` | lifetime id × link version;pipeline 下为 `stageLinks`(pipeline 名 × 各 stage 的 lifetime id 与 link version) |
| 7 | program 绑定:image unit、block binding、uniform 写集合 | opaque uniform 上的 `glUniform1i`、`glUniformBlockBinding`、`glShaderStorageBlockBinding`、任意 `glUniform*` | 四个 per-program 计数器;pipeline 下再混入 `stageLinks` |
| 8 | `set_global_constants`:ShaderCso、Version、默认 uniform 块镜像 | 默认块上的 `glUniform*`、`glUseProgram` | lifetime id × UBO content version;pipeline 下为 `stageState` |
| 11 | `set_framebuffer_state`:Fbo、Color[8]/Depth/Stencil/ReadSurface(Res、Kind、InternalFormat、TextureTarget、Layered、Level、Layer、UploadTarget)、DrawBuffers[8]、Width/Height/Layers/Samples/FixedSampleLocations、IsDefault、Complete、Target | `glFramebufferTexture*`、`glFramebufferRenderbuffer`、`glDrawBuffer(s)`、`glReadBuffer`、`glFramebufferParameteri`、`glBindFramebuffer`;已挂接纹理或 renderbuffer 的存储重定义(格式、纹理目标、尺寸、采样数、完备性在发射时内联,所以 `PipePublishDescriptor` bump attachment 聚合) | attachment 聚合 × draw / read 绑定槽版本 |
| 12 | `set_sampler_views`:每单元 {View, Texture} | `glBindTexture`、`glActiveTexture`、纹理或 sampler 参数、定义 level 的上传、默认纹理出现;在用 program(`glUseProgram`、relink、sampler uniform 的 `glUniform1i`) | content × params × bind generation × `opaqueUnits` |
| 13 | `bind_sampler_states`:每单元 sampler CSO 句柄 | `glBindSampler`、`glDeleteSamplers`、`glSamplerParameter*`、`glTexParameter*` | params × sampling resolution × bind generation |
| 14 | `set_shader_images`:program image-unit 窗口内每单元 {Res, InternalFormat, Layer, Level, Layered, Access} | `glBindImageTexture`、纹理 content / params、image uniform 的 `glUniform1i`;在用 program(`glUseProgram`、relink) | content × params × bind generation × `programImages` |
位 11–14 还在 share group 的孪生聚合时钟变化时触发(setter 经兄弟 context 调用);最大触及纹理单元变化时 12、13 也触发。
### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集(D-B1)
```
@@ -50,6 +66,14 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义,`resource_destroy` 在析构时发;死亡转发给每个 emitter(只释放 slot 曾导致 UAF)。view 先于存储属主销毁、FBO attachment 钉住纹理、buffer texture 钉住 buffer。
- program pipeline 合成体(`CompositeResolver`)完全在前端合成、推一个句柄;释放恰好一次(resolver 换掉或 `~ProgramObject`,D-H7);记忆按 `(ContextId, 管线 GL 名)` 键控。
#### 5.6.1 program pipeline 合成体的句柄、释放与记忆键
- 合成体由 `GLContext::GetProgramForDraw()` 在前端合成、缓存在各自的 `ProgramPipelineObject` 里;program 发射器按合成体自己的 lifetime id 从 `ShaderCso` 保留高段(§2.1)给它一个句柄,照普通 program 发 `create_shader_state`,server 不知道它是合成体。`MGPipeSlotAllocator::Allocate` 拒绝该段,`AllocateComposite` 是唯一入口;用保留段而不用句柄 flag,是因为 flag 要被两侧每个 `ShaderCso` 消费者携带和屏蔽,而保留段让"普通 program 拿不到合成体 slot"由分配器保证。
- slot 有两条释放路径:签名变化时 `MGPipeCompositeResolver::Observe` 释放旧合成体,以及合成体自己的 `~ProgramObject`。两条都走 `MGPipeEmitShaderCsoDestroyAndFree`,后到的一条是空操作(`MGPipeSlotAllocator::Free` 拒绝不在该世代 live 的 slot,也不推进世代)。只靠析构不够:前端一旦多持一份引用,签名变化时析构就不会先到。
- 签名取 `ComputeDrawProgramSignature()`(每个图形 stage 的 lifetime id 与 link version),不含 `GetBackendStateVersion()`:后者在 sampler uniform 的 `glUniform1i` 时也会变,会让合成体每次 draw 都重新编译。`Tracker.h` 的位 6 快门读同一组值。
- 记忆键是 `(GLContext::GetTextureContextId(), 管线 GL 名)`:resolver 是进程单例,而 pipeline 名按 context 分配,只按名键控会在 make-current 后命中另一 context 的同名项、释放一个仍活着的合成体,每次切换都重建 program;context 地址也不行,销毁重建的 context 会落在旧地址上,而这个 id 永不复用。
- context 销毁时其合成体经 `~ProgramObject` 释放,残留项再也查不到;make-current 时的 `Reset()` 丢弃它们而不释放,表长因此只受合成体 slot 仍 live 的项限制。
### 5.7 emulation 的归属
规则:**驱动表达不了的变换在 tracker 里 lowering,硬件 / 驱动强加的变换在 driver 里 lowering。**
@@ -77,6 +101,12 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与
- 只换元数据(`RespecifyRedefinesNoStorage`:sticky 的 `BindMask` / `ImageBindableHint` 变化时重发的描述符)不丢任何项。
- buffer 没有待上传项,也永远不算只换元数据。
#### 6.1.1 region 列表是精确脏足迹
纹理 `resource_subdata` 的 region 列表是该 level 自上次变干净以来的**精确脏足迹**:互不相交的盒子,覆盖每个写过的纹素,不多一个(`MGPipeTextureEmitter::EmitOneLevel`)。原因:server 端图像一旦可能含 GPU 写入,超出应用写入范围的上传就会盖掉它们。Magma 总把列表当作要上传的纹素;Espryt 在驱动纹理可能含 GPU 写入时同样如此,否则仍自己在 box 与 rect 之间选上传形状(Mali 作业数照旧由 server 决定)。
- `RegionCount == 0` 只表示整 level 写;其余写入一律带盒子(只有一个也带),所以 applier 的累积不会因为某一侧「只有 box」而把精确贡献放宽成并集盒。
- 例外:足迹超过一条记录能带的 `kMGPipeMaxPendingUploadRegions` 个盒子时,退回存储的 rect 列表(只保证覆盖,日志记一次);该列表又被扣下(rect 填满并集盒的 3/4)时,0 是一个**不精确**的部分盒。把「只有 box」读作「client 拥有盒内每个纹素」的读者(`AdoptDriverLevelIntoShadow` 的覆盖盒规则)只在这种情况下不精确。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 过线的是逐 stage SPIR-V + 反射归档,**不是源码**:glslang 全在 client、SPIRV-Cross 全在 server,文件级切割;server 侧无编译池。归档序列化器 `ProgramArtifactsCodec`,monolith 只在 verify 构建里调它(D-H3)。
@@ -22,6 +22,15 @@ client 的残余填充在自己的 verb 边界给全部字段盖戳,server 读
- 非边界记录(如两次 draw 之间的 `set_dynamic_state`)不盖戳,沿用前一边界的序号、掩码与 verb 名,但盖戳标志已在上一条记录后清除,读检查按盖戳之外处理;20 个 draw 入口都盖成 `DrawArrays`,掩码正确,Fatal 里的名字是泛称。
- 读检查只在 server 盖戳的 verb 内按归属类(§3.1 的 G8)分流:`RECORD_SUPPLIED` / `APPLIER_DERIVED` 读到旧值是 `Fatal{RoleViolation}`,`FATAL` 直接中止,`BARRIER_PULLED`(仅供生成器对照,没有生产行)在该 verb 类内计入 `rsp`,由 `CountBarrierPull` 决定何时改为中止(记录未 barrier、两角色用不同块、strict 下未准入)。盖戳之外一律是普通 poison `Fatal{UnmigratedPipeInput}`。
- 盖戳标志由 `PipeApplier::LeaveApplier` 在每条记录之后清除;server 进程里没有 client 半边替它清,漏清会让之后的读都按上一个 verb 的掩码判定。
#### 9.2.2 run-ahead 下 client 对 `PipeInputs` 的写
run-ahead 下,unbarriered 记录 apply 期间 `PipeInputs` 是 server 角色内存:对这类 verb,client 不填充、不盖戳、不撤 server 的戳,只跑 tracker 与 emitter(`MGPipeValidateForVerb` 的 `fillOwed`)。GL 线程唯一合法的写是 barriered verb 的填充,而且写的那一刻就得成立:
- 验证点的顺序是填充 → 发射 → park,填充时 apply 线程可能还在执行先前发布的 unbarriered 记录,所以每次写之前 `QuiesceApplierBeforeFill` 先等 applier 追上最后发布的序号(`ClientSession::WaitForApplyToCatchUp`,与 `glFinish` 同一种强制等待)。
- 块的写分两段、夹着记录发布:发射前写 verb 序号、撤 server 戳与 verb 名,发射后做残余字段遍历;发出的记录 apply 时要读这块,一次等待盖不住两段。
- `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` 查的是事实(apply 线程不在 applier 内),不采信调用方的声明;漏掉等待即 `Fatal{RoleViolation, "gPipeInputs"}`。
- lockstep 下 `appliedSeq` 已在最后发布的序号上,等待立即返回。client 与 server 用不同块时(`MGPipeBlocksAreDistinct`:分角色演练或 spawn),client 只写自己的块、不必等待,server 块的戳只由 `PipeApplier::LeaveApplier` 清除。
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
# define MGB_CTX (::MG_State::pGLContext)
@@ -29,6 +29,22 @@ T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `has
- `resource_respecify`(`MGPipeResourceRespecifyWantsItsReply`):wire 臂上 buffer 那半不等;纹理那半等到对象的 create 被接受(`MGPipeHandleIsPublished`),之后 acceptance 已是 client 知道的事实,首次使用时付一次阻塞的 create;create 被拒则闩不置位,后续照常等真答案,代价是吞吐而不是静默。
- monolith 下答案在调用线程上直接返回、没有代价,谓词保持原答案(纹理那半照收)。
#### 13.1.2 client 侧保守 GPU 写集合
`BufferObject::SyncGpuWrites` 在应用线程上、于 `glMapBuffer` / `glGetBufferSubData` 时回答"GPU 是否可能写过这块 buffer"。后端的标记点在 server 侧,逐次去问是稳态禁止的 roundtrip,所以 client 在每条 draw / dispatch 记录发出之前,从自己持有的绑定构建这个集合(`MG_Impl/Pipe/Verb/GpuWriteSet.h`)。
- 宁多勿少:多标一次的代价是一次回读,漏标则静默读到陈旧 shadow。每个生产者有计数器(`ProducerMarkCount`),单元用例断言它确实触发。
- 前四个生产者对应后端标记点:SSBO 绑定点、atomic counter 绑定点(标全部 touched 点,比后端宽)、`Access != GL_READ_ONLY` 的 image buffer texture、XFB 捕获目标。后端故意窄的两处照搬:只读 image 绑定不标;image 遍历在 draw 准备时做,不在 `glBindImageTexture` 时做。
- 后端没有的两个生产者:读进 pack PBO 的 `glReadPixels` 发出即返回、只做标记,拷贝推迟到第一次真要读时;传输下 `glEndTransformFeedback` 不再无界等 fence,只标记捕获目标(monolith 仍等)。
- 收窄尚未实现:`MGPipeClientOnGpuWritten` 只接受一个整 buffer 范围;零个范围留给将来的"已完全收窄",不能当作整块已写。
- 不覆盖后端自己的 scratch buffer(Espryt 的转换顶点流与 primitive-restart 替换 buffer、Magma 的 UBO ring):后端那边同样没有 `MarkGpuWritten`,是两边共有的洞。
#### 13.1.3 glReadPixels:线上只有矩形,pack 状态留在 client
`read_pixels` 记录带的是矩形而不是布局:`MGPReadbackInfo::DstSize` 是紧凑的 `w*h*bytesPerPixel`,server 用中性 pack 状态读进这段紧凑行(就是 reply payload),client——唯一持有应用 pack 状态的一侧——再按应用的 `GL_PACK_*` 把各行散射到应用指针,行间空隙不写。若 `DstSize` 取打包后的尺寸,server 按它分配、后端又按实时 pack 状态写,就会越界(4x3 RGBA8、`PACK_ROW_LENGTH=8`、`SKIP_ROWS=1`、`SKIP_PIXELS=2`:分配 80 B,驱动写到第 120 B)。规则只关乎布局,深度与深度模板读同样适用。
- 紧凑答案大于一个 reply slot(`LinkTerms.maxReplyBytes`)时切的是读,不是 reply:一行放得下就切成 `floor(maxReplyBytes / rowBytes)` 行的整宽行带,否则把每行切成 `floor(maxReplyBytes / bpp)` 像素的列段。每段是一条普通 `read_pixels` 记录,带自己的框与紧凑 `DstSize`,各自按 pack 状态散射;段按行主序恰好覆盖矩形一次,且在紧凑布局里连续,中性 pack 的快路径因此仍可直接读进应用指针(`PlanReadbackBands` / `ForEachReadbackBand`)。
- 只有连一个像素都放不进 reply(含没有 reply 池的链路)时,client 才在发射前具名拒绝为 `Fatal{ReplyTooLarge}`:像素不能拆得比自身更小。
### 13.2 五部分验证门(取代 monolith 的字节一致门)
1. **接口纯度**:A 门 include 图(`scripts/check_include_closure.py`)、B 门 server 镜像不引用 `MG_State::GLState` 与 glslang(P6 的 a6 实测 server 仍链 `MG_Impl`,由 `scripts/link_ratchet.py` 的下行棘轮追踪;P13 把 172 条降到 27 条具名债务、`# P13` 注解清零并由脚本拒绝回潮,W7 的 `MobileGL_server_linkcheck` 把它变成真链接)、C 门 `MG_Backend` 里零 `pGLContext`;外加"每个后端 memo 键都是 `{slot, gen}`"(`HandleRecycleScenario`)。