mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Docs] (docs/Disaggregated, MG_Impl/Pipe, MG_Remote/Client): shorten long comments and move their design arguments into docs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
+180
-671
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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`)。
|
||||
|
||||
Reference in New Issue
Block a user