Files
MobileGL/MobileGL/MG_Impl/Pipe/Tracker.h
T

865 lines
55 KiB
C++

// MobileGL - MobileGL/MG_Impl/Pipe/Tracker.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
//
// 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.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring>
namespace MobileGL::MG_Pipe {
// One bit per row of the ARCHITECTURE.md 5.2 table, hand-written rather than generated:
// the list is design, not derived data, and the generator has nothing to derive it from.
enum class MGPipeDirty : Uint32 {
// ---- value class: P2 emits for these five ----
NewRenderState = 0, // RenderState::m_version -> set_dynamic_state
NewPipelineState, // RenderState::m_pipelineStateVersion -> create/bind_render_state
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
NewPatchState, // the patch trio, NaN legal -> set_patch_state
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a and the other three from
// P4a - the program family, one subsystem, three bits because the frontend moves them
// as three separate events ----
NewVertexElements, // the bound VAO's attribute configuration -> create/bind_vertex_elements
NewShader, // the current program's link version -> create/bind_shader_state,
// set_draw_program, set_dispatch_program (P4a)
NewShaderBindings, // image units, block bindings, uniform write set (P4a)
NewGlobalConstants, // the default-uniform-block image -> set_global_constants (P4a)
// ---- object class. THE FIRST TWO ARE P3a's, not P3b/P4b's: the roadmap puts
// set_vertex_buffers and set_index_buffer in the same phase as the vertex-elements
// trio, and this comment said otherwise until the commit that wired them. THE NEXT
// FOUR ARE P4a's. The last three are still computed and counted only, until P4b. ----
NewVertexBuffers, // -> set_vertex_buffers (P3a)
NewIndexBuffer, // -> set_index_buffer (P3a)
NewFramebuffer, // -> set_framebuffer_state, per bound target (P4a)
NewSamplerViews, // -> set_sampler_views (P4a)
NewSamplers, // -> bind_sampler_states (P4a)
NewShaderImages, // -> set_shader_images (P4a)
NewConstBuffers,
NewShaderBuffers,
NewSoTargets,
Count,
};
inline constexpr SizeT kMGPipeDirtyCount = static_cast<SizeT>(MGPipeDirty::Count);
static_assert(kMGPipeDirtyCount <= 32, "the dirty mask is a Uint32");
inline constexpr Uint32 MGPipeDirtyBit(MGPipeDirty bit) {
return Uint32{1} << static_cast<Uint32>(bit);
}
// The five P2 emits for. Each phase's constant survives as the next phase's A/B control
// and as what a test compares the subsystem map against, so none of them is edited in
// place when a later phase takes more bits over.
inline constexpr Uint32 kMGPipeDirtyEmittedAtP2 =
MGPipeDirtyBit(MGPipeDirty::NewRenderState) | MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
MGPipeDirtyBit(MGPipeDirty::NewPixelPack) | MGPipeDirtyBit(MGPipeDirty::NewPatchState) |
MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults);
// The three P3a adds: the vertex-input family, all on one subsystem.
inline constexpr Uint32 kMGPipeDirtyEmittedAtP3a =
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers) | MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer);
// The SEVEN P4a adds, across FOUR subsystems: bits 6/7/8 are the program family, 11 the
// framebuffer, and 12/13/14 the sampler-view / sampler-state / image-unit sets. Added
// rather than edited into the two above, for the reason those two exist: each phase's
// constant survives as the next phase's A/B control and as what a test compares the
// subsystem map against.
//
// EVERY ONE OF THESE SHUTTERS WAS ALREADY COMPUTED, LATCHED AND COUNTED before P4a; what
// P4a adds is an emitter for them. That is why this is a one-line constant and not seven
// new shutters - and it is also why the two narrowings below are stated as requirements.
inline constexpr Uint32 kMGPipeDirtyEmittedAtP4a =
kMGPipeDirtyEmittedAtP3a | MGPipeDirtyBit(MGPipeDirty::NewShader) |
MGPipeDirtyBit(MGPipeDirty::NewShaderBindings) |
MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants) |
MGPipeDirtyBit(MGPipeDirty::NewFramebuffer) | MGPipeDirtyBit(MGPipeDirty::NewSamplerViews) |
MGPipeDirtyBit(MGPipeDirty::NewSamplers) | MGPipeDirtyBit(MGPipeDirty::NewShaderImages);
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
"NEW_RENDER_STATE",
"NEW_PIPELINE_STATE",
"NEW_PIXEL_PACK",
"NEW_PATCH_STATE",
"NEW_VERTEX_ATTRIB_DEFAULTS",
"NEW_VERTEX_ELEMENTS",
"NEW_SHADER",
"NEW_SHADER_BINDINGS",
"NEW_GLOBAL_CONSTANTS",
"NEW_VERTEX_BUFFERS",
"NEW_INDEX_BUFFER",
"NEW_FRAMEBUFFER",
"NEW_SAMPLER_VIEWS",
"NEW_SAMPLERS",
"NEW_SHADER_IMAGES",
"NEW_CONST_BUFFERS",
"NEW_SHADER_BUFFERS",
"NEW_SO_TARGETS",
};
// Which runtime MOBILEGL_PIPE_PUSH subsystem bit gates a dirty bit's emission. Zero for
// a bit P2 does not emit, which is what makes "the bitmask is a true per-subsystem A/B"
// literally true rather than approximately.
inline constexpr Uint64 MGPipeSubsystemForDirty(MGPipeDirty bit) {
switch (bit) {
case MGPipeDirty::NewRenderState:
case MGPipeDirty::NewPipelineState:
return kMGPipeSubsystemRenderState;
case MGPipeDirty::NewPixelPack:
return kMGPipeSubsystemPixelPack;
case MGPipeDirty::NewPatchState:
return kMGPipeSubsystemPatchState;
case MGPipeDirty::NewVertexAttribDefaults:
return kMGPipeSubsystemVertexAttribDefaults;
// P3a's three, all one subsystem: create/bind_vertex_elements, set_vertex_buffers
// and set_index_buffer are the vertex-input family and an operator switching it off
// has to get the whole family's legacy arm, not two thirds of it.
// PipeFill.cpp's SubsystemForEmitter carries the pairing static_asserts.
case MGPipeDirty::NewVertexElements:
case MGPipeDirty::NewVertexBuffers:
case MGPipeDirty::NewIndexBuffer:
return kMGPipeSubsystemVertexInput;
// P4a's seven, across four subsystems. FOUR AND NOT ONE for P3a's reason one level
// out: a framebuffer path that regressed, a texture path that regressed, a sampler
// path that regressed and a program path that regressed are four different findings.
//
// The program family is three bits because the frontend moves them separately - a
// relink, a binding change and a uniform write are three events - but one subsystem,
// because an operator switching programs off has to get the whole family's legacy arm.
// Same for the three unit sets: create_sampler_state, create_sampler_view and the
// three kVarTail sets are one family, and half of it is not a control.
case MGPipeDirty::NewShader:
case MGPipeDirty::NewShaderBindings:
case MGPipeDirty::NewGlobalConstants:
return kMGPipeSubsystemPrograms;
case MGPipeDirty::NewFramebuffer:
return kMGPipeSubsystemFramebuffer;
case MGPipeDirty::NewSamplerViews:
case MGPipeDirty::NewSamplers:
case MGPipeDirty::NewShaderImages:
return kMGPipeSubsystemSamplers;
// NO BIT NAMES kMGPipeSubsystemTextureResources, and that is deliberate rather than an
// omission: the texture and renderbuffer resource_* calls and set_texture_params are
// dispatched from the GL entry points that cause them - a constructor, a storage
// definition, a glTexParameter - not from a dirty walk, exactly as P3a's buffer family
// is. Bit 10 gates those dispatch sites; there is no dirty bit to map onto it and
// there must not be one, or the emission would be gated twice and disagree with itself.
default:
// The remaining bits have no call of their own until P4b, so there is no
// subsystem to switch and the residual fill keeps supplying their fields.
return 0;
}
}
// 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: each mix takes a 64-bit input into a 64-bit
// accumulator, so two DIFFERENT vertex configurations collide with probability ~2^-64
// per pair, and the inputs are a monotone lifetime id, a monotone configuration version
// and a widened slot version - 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.
inline constexpr Uint64 MGPipeMixShutter(Uint64 accumulator, Uint64 value) {
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
return accumulator;
}
// A Uint16 counter widened at the TRACKER boundary, never in MG_State
// (ARCHITECTURE.md 5.2: MG_State is not changed for this). A decrease is a wrap and adds
// 65536. A wrap is harmless locally - one extra re-push, never a missed one - which is
// exactly what TrackerTest.WrapAroundRePushesButNeverMisses pins.
//
// THE ONE CASE IT CANNOT SEE, stated because "never a missed push" is otherwise stronger
// than what is true: the wrap test is `now < m_last`, so a counter that advances by
// EXACTLY 65536 (or a multiple) between two walks reads as unchanged. That needs 65536
// render-state mutations inside one verb boundary, and it is pre-existing in class -
// both backends already compare raw Uint16 versions the same way - so P2 records it
// rather than widening MG_State's counters, which ARCHITECTURE.md 5.2 rules out.
class MGPipeWidenedCounter {
public:
Uint64 Observe(Uint16 now) {
if (m_started && now < m_last) m_high += 0x10000ull;
m_started = true;
m_last = now;
return m_high + now;
}
void Reset() {
m_high = 0;
m_last = 0;
m_started = false;
}
private:
Uint64 m_high = 0;
Uint16 m_last = 0;
Bool m_started = false;
};
class MGPipeTracker {
public:
using GLContext = MG_State::GLState::GLContext;
// The dirty walk. Compares every shutter against what was last pushed, LATCHES the
// new values, counts the fires per verb class, and returns the mask. Latching here
// rather than after emission is deliberate: a bit whose subsystem is switched off is
// not emitted, but its fields are then still pulled by the residual fill, so the
// pushed block is correct either way and a bit can never fire twice for one change.
Uint32 Update(GLContext& ctx, MGPipeVerbClass verbClass) {
// A different context is a different server: nothing the tracker latched about
// the old one says anything about this one, and the first walk on a fresh
// context must publish a COMPLETE state rather than an increment.
if (m_context != &ctx) {
Reset();
m_context = &ctx;
}
const Bool wasPrimed = m_primed;
Uint64 now[kMGPipeDirtyCount];
const RenderStateParameters& render = ctx.GetRenderStateParameters();
// ---- bits 0..1: the two Uint16 render-state counters, widened HERE ----
now[Index(MGPipeDirty::NewRenderState)] =
m_renderStateVersion.Observe(static_cast<Uint16>(ctx.GetRenderStateParametersVersion()));
now[Index(MGPipeDirty::NewPipelineState)] =
m_pipelineStateVersion.Observe(static_cast<Uint16>(ctx.GetPipelineStateVersion()));
// ---- bit 4 and the value-class bits 5..8 ----
now[Index(MGPipeDirty::NewVertexAttribDefaults)] = ctx.GetAnyVertexAttribDefaultGeneration();
const auto& vao = ctx.GetBoundVertexArray();
const Uint64 vaoIdentity =
vao ? MGPipeMixShutter(vao->GetLifetimeId(), vao->GetConfigVersion()) : 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.
const auto& program = ctx.GetCurrentProgram();
Uint64 shader = 0;
Uint64 bindings = 0;
Uint64 constants = 0;
Uint64 programImages = 0;
// THE PROGRAM INPUT OF THE PROGRAM-RESOLVED VIEW SET (P4a fable seam F-1).
// set_sampler_views is resolved for the program in use (SamplerEmit.h: the sampler
// uniform's TYPE picks which of a unit's targets is the view) and the emitter
// memoises that resolution on (lifetime id, link version, backend state version). A
// shutter that read only the texture generations therefore missed a glUseProgram:
// `glBindTexture x N; glUseProgram(P1); draw; glUseProgram(P2); draw` moved nothing
// bit 12 read, so the view set stayed P1's - and E's record epoch, keyed on the two
// set serials, then never rebuilt the texture sync list for P2 either. This value is
// that memo key, and bit 12 mixes it in below: over-firing costs one re-resolution
// the set-hash suppressor absorbs, under-firing left the record describing the
// previous program's units.
Uint64 opaqueUnits = 0;
if (program) {
shader = MGPipeMixShutter(program->GetLifetimeId(), program->GetLinkVersion());
bindings = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(program->GetImageUnitVersion(),
program->GetBackendStateVersion()),
program->GetBlockBindingVersion()),
program->GetUniformWriteSetVersion());
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
// THE IDENTITY IS MIXED IN (P4a fable seam F-2), exactly as the pipeline arm
// below mixes stageLinks into its half: the counter alone is a per-program
// number two programs routinely share - 0 == 0 for any pair that never moved an
// image unit through glUniform1i, and 0 == 0 against no program at all - so a
// glUseProgram between them fired nothing, set_shader_images' window stayed the
// previous program's, and a program whose only image is a BUFFER image (E's
// SD-4: nothing else moves between the bind and the dispatch) never reached the
// record at all.
programImages = MGPipeMixShutter(shader, program->GetImageUnitVersion());
opaqueUnits = MGPipeMixShutter(shader, program->GetBackendStateVersion());
} else if (const auto& pipeline = ctx.GetBoundProgramPipeline(); pipeline) {
using Pipeline = MG_State::GLState::ProgramPipelineObject;
// THE FIELDS ARE READ DIRECTLY RATHER THAN THROUGH THE TWO FUNCTIONS THAT
// ALREADY PACK THEM, and that is a gate constraint, not a preference. Calling
// ComputeDrawProgramSignature() / ComputeUniformMirrorVersions() would say
// "the same pairs the composite cache and the uniform-mirror gate compare"
// far better than this loop does - but gen_pipe_dirty_surface.py derives a
// shutter by following each accessor to the member it returns, and both of
// those build a LOCAL array and return that, which it cannot place. A shutter
// naming them is UNRESOLVED, and then every DirtySurface.def row that names
// bits 6, 7, 8 or 14 loses its verdict - including the derivation that is the
// only mechanism able to catch the next under-fire here. So the pairs are
// spelled out, and the two static_asserts below are what say they must stay in
// step with the functions they mirror.
static_assert(sizeof(Pipeline::DrawProgramSignature) ==
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
"bit 6 reads the {lifetimeId, linkVersion} pair per graphics "
"stage that ComputeDrawProgramSignature packs");
static_assert(sizeof(Pipeline::UniformMirrorVersions) ==
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
"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.
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
Uint64 stageState = 0;
Uint64 stageImages = 0;
// The per-stage sampler/image unit assignments alone (glUniform1i on a stage
// program's sampler moves its backend state version and reaches the composite
// through the uniform mirror), for bit 12's program input below.
Uint64 stageOpaque = 0;
for (SizeT stage = 0; stage < Pipeline::kGraphicsStageCount; ++stage) {
const auto& staged = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!staged) continue;
stageLinks = MGPipeMixShutter(
MGPipeMixShutter(stageLinks, staged->GetLifetimeId()), staged->GetLinkVersion());
stageState = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(stageState, staged->GetBackendStateVersion()),
MGPipeMixShutter(staged->GetUBOContentVersion(),
staged->GetBlockBindingVersion())),
staged->GetUniformWriteSetVersion());
stageImages = MGPipeMixShutter(stageImages, staged->GetImageUnitVersion());
stageOpaque = MGPipeMixShutter(stageOpaque, staged->GetBackendStateVersion());
}
shader = stageLinks;
stageState = MGPipeMixShutter(stageLinks, stageState);
bindings = MGPipeMixShutter(stageState, stageImages);
constants = stageState;
programImages = MGPipeMixShutter(stageLinks, stageImages);
opaqueUnits = MGPipeMixShutter(stageLinks, stageOpaque);
}
now[Index(MGPipeDirty::NewShader)] = shader;
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)
// ===========================================================================
// ---- the object-class bits 9..17 ----
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
const Uint64 buffers = ctx.GetAnyBufferChangeGeneration();
// Bit 9. The VAO attribute aggregate mixed with the bound VAO's identity is
// already exact for the SET - it is bumped by all three Bump*Version functions,
// which are the only writers of an attribute's format, buffer or enable state -
// and a driver-id re-mint that moves no client counter is caught server-side by
// the backend's own id generation.
//
// THE PENDING BASE INSTANCE IS MIXED IN, and this is a deviation from the design
// note that said "keep the shutter" (recorded in client-v1.md): the draw's
// baseInstance is now an EXPLICIT field of set_vertex_buffers and a
// ContentHash input, and it moves neither the attribute aggregate nor the VAO
// identity. Without it here, a draw whose only change is its base instance would
// never reach the emitter at all and the server would keep the previous fetch
// shift - which is the same silently-wrong-geometry the backend's
// baseInstanceDirty flag exists to prevent, one level further out. It fires
// extra only on the draws that actually carry one.
now[Index(MGPipeDirty::NewVertexBuffers)] = MGPipeMixShutter(
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity), m_pendingBaseInstance);
// Bit 10, NARROWED (P3a, D-I). It used to mix the whole buffer-CONTENT aggregate
// with the VAO identity and therefore fired on any buffer write anywhere; what
// it guards is one binding slot, so it now reads that slot's own version and the
// identity of what is bound to it. The version is a WRAPPING Uint16 bumped only
// on a real change, so it goes through the widened counter at this boundary; the
// bound object's lifetime id joins it because identity is what closes the wrap
// hole. The VAO identity stays in the mix because the element slot BELONGS to
// the bound VAO - switching VAOs switches slots.
Uint64 indexShutter = 0;
if (vao) {
const auto& indexSlot = vao->GetIndexBufferBindingSlot();
const auto& indexObject = indexSlot.GetBoundObject();
indexShutter = MGPipeMixShutter(m_indexSlotVersion.Observe(indexSlot.GetVersion()),
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.
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
MGPipeMixShutter(
ctx.GetAnyFramebufferAttachmentGeneration(),
m_framebufferBind.Observe(
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
m_readFramebufferBind.Observe(
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
// Bit 12 reads FOUR things (F-1): the two texture aggregates, the bind generation
// and the program input computed above. The params aggregate is here because
// SamplerEmit.h drops a unit's view to null when SamplesAsIncompleteTexture says so,
// and that predicate reads the effective sampler's filters - a glTexParameteri(
// MIN_FILTER) that completes a texture fired bit 13 and not this one, so the entry
// stayed null. The program input is here because the set is resolved FOR THE
// PROGRAM IN USE, and a glUseProgram alone moved nothing this shutter read.
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.
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
ctx.GetTextureBindGeneration());
now[Index(MGPipeDirty::NewShaderImages)] = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages),
ctx.GetTextureBindGeneration());
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
now[Index(MGPipeDirty::NewShaderBuffers)] = buffers;
now[Index(MGPipeDirty::NewSoTargets)] =
MGPipeMixShutter(buffers, ctx.GetTransformFeedbackGeneration());
Uint32 dirty = 0;
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
// Bits 2 and 3 are handled below: they are BitwiseEqual shutters, not
// counters, so they have no entry in `now`.
if (i == Index(MGPipeDirty::NewPixelPack) || i == Index(MGPipeDirty::NewPatchState)) {
continue;
}
if (!m_primed || now[i] != m_lastPushed[i]) dirty |= Uint32{1} << static_cast<Uint32>(i);
m_lastPushed[i] = now[i];
}
// ---- bit 2: the PACK half of the pixel store, BitwiseEqual ----
const PixelStoreParameters pack = ctx.GetPixelStoreParameters(false);
if (!m_primed || std::memcmp(&pack, &m_pack, sizeof(pack)) != 0) {
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPixelPack);
m_pack = pack;
}
// ---- bit 3: the patch trio, BitwiseEqual, and NaN IS LEGAL ----
// A NaN outer level is a legal glPatchParameterfv value and must compare equal to
// itself (ARCHITECTURE.md 5.2). Float equality says it is not; memcmp says it is,
// which is the whole reason this is a byte compare.
PatchTrio patch{};
patch.PatchVertices = render.PatchVertices;
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = render.PatchDefaultOuterLevel[i];
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = render.PatchDefaultInnerLevel[i];
if (!m_primed || std::memcmp(&patch, &m_patch, sizeof(patch)) != 0) {
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPatchState);
m_patch = patch;
}
m_primed = true;
m_freshlyPrimed = !wasPrimed;
m_lastDirty = dirty;
if (MG_Util::PipeStats::Enabled()) {
const SizeT cls = static_cast<SizeT>(verbClass);
++m_walks[cls];
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
if (dirty & (Uint32{1} << static_cast<Uint32>(i))) ++m_fires[i][cls];
}
}
return dirty;
}
// Context teardown, server reset, a unit test's fixture. The next Update returns
// every bit set, which is what makes the first verb on a fresh context publish a
// complete state rather than an increment. Deliberately does NOT clear the fire
// tallies: they are a per-run measurement, not per-context state.
//
// AND IT DELIBERATELY DOES NOT CLEAR m_pendingBaseInstance. Everything else this
// function clears is a LATCH describing what the server was last told; the pending
// base instance is THIS CALL'S ARGUMENT, written by the draw entry point one
// statement before MGP_FILL and not yet read by anybody. Update() calls Reset() from
// inside itself whenever the current GLContext pointer moves, so clearing it here
// meant that `eglMakeCurrent(ctxB); glDrawArraysInstancedBaseInstance(..., 7)` put a
// BaseInstance of 0 on the wire - one silently mis-shifted instanced draw per context
// switch, on the emulation path, with nothing to catch it. The value is cleared by the
// verb that consumes it (PipeFill.cpp's step 3, and its no-context early return) and
// by MGPipeLeaveVerb, which is where a per-call argument belongs.
void Reset() {
std::memset(m_lastPushed, 0, sizeof(m_lastPushed));
m_renderStateVersion.Reset();
m_pipelineStateVersion.Reset();
m_framebufferBind.Reset();
m_readFramebufferBind.Reset();
m_indexSlotVersion.Reset();
m_pack = PixelStoreParameters{};
m_patch = PatchTrio{};
m_staged = RenderStateParameters{};
m_stagedAttribs = AttribDefaults{};
m_context = nullptr;
m_lastDirty = 0;
m_primed = false;
m_freshlyPrimed = false;
}
void ResetCounters() {
std::memset(m_fires, 0, sizeof(m_fires));
std::memset(m_walks, 0, sizeof(m_walks));
}
Uint64 FireCount(MGPipeDirty bit, MGPipeVerbClass verbClass) const {
return m_fires[Index(bit)][static_cast<SizeT>(verbClass)];
}
Uint64 FireCount(MGPipeDirty bit) const {
Uint64 total = 0;
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_fires[Index(bit)][i];
return total;
}
Uint64 WalkCount(MGPipeVerbClass verbClass) const {
return m_walks[static_cast<SizeT>(verbClass)];
}
Uint64 WalkCount() const {
Uint64 total = 0;
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_walks[i];
return total;
}
Uint32 LastDirty() const { return m_lastDirty; }
Bool Primed() const { return m_primed; }
// True when the LAST Update was the first one after a Reset - a fresh context, or a
// server reset. The emission step reads it to send a COMPLETE state rather than an
// increment against a staging mirror that describes a context that is gone.
Bool FreshlyPrimed() const { return m_freshlyPrimed; }
// "What the server has" (P2 brief D8). set_dynamic_state sends the dynamic chunks
// that differ from this, which is the chunk-level suppressor; a chunk that
// memcmp-matches is not sent at all.
RenderStateParameters& Staged() { return m_staged; }
const RenderStateParameters& Staged() const { return m_staged; }
// The same mirror for the 32 glVertexAttrib* defaults: set_vertex_attrib_defaults
// names only the attributes that differ from it, which is the var-tail's own
// suppressor underneath D11's set-hash one.
using AttribDefaults = Array<MG_State::GLState::CurrentVertexAttributeValue,
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>;
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.
void SetPendingBaseInstance(Uint32 baseInstance) { m_pendingBaseInstance = baseInstance; }
Uint32 PendingBaseInstance() const { return m_pendingBaseInstance; }
void ClearPendingBaseInstance() { m_pendingBaseInstance = 0; }
private:
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
struct PatchTrio {
Uint PatchVertices;
Float Outer[4];
Float Inner[2];
};
Uint64 m_lastPushed[kMGPipeDirtyCount]{};
MGPipeWidenedCounter m_renderStateVersion;
MGPipeWidenedCounter m_pipelineStateVersion;
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
// that wrapped would let a composite shutter repeat and cost a missed fire.
MGPipeWidenedCounter m_framebufferBind;
// P4a: the READ framebuffer binding slot's version, its own counter for the same
// reason the draw one exists. Two counters rather than one over both slots: a single
// widened counter fed two independent Uint16s reads a decrease as a wrap on every
// alternation and would add 65536 per switch, which costs nothing in correctness
// (over-firing) but makes the high word meaningless.
MGPipeWidenedCounter m_readFramebufferBind;
// The BOUND VAO's element-array slot version, widened for the same reason. One
// counter over a slot that changes with the bound VAO: a stale high word can only
// ADD a fire, never drop one, and the VAO identity in the same mix is what makes a
// switch between two VAOs differ whatever their slot versions read.
MGPipeWidenedCounter m_indexSlotVersion;
Uint32 m_pendingBaseInstance = 0;
// Bits 2 and 3 are BitwiseEqual shutters, not counters.
PixelStoreParameters m_pack{};
PatchTrio m_patch{};
RenderStateParameters m_staged{};
AttribDefaults m_stagedAttribs{};
const void* m_context = nullptr;
Uint32 m_lastDirty = 0;
Bool m_primed = false;
Bool m_freshlyPrimed = false;
Uint64 m_fires[kMGPipeDirtyCount][kMGPipeVerbClassCount]{};
Uint64 m_walks[kMGPipeVerbClassCount]{};
};
// ONE attribute default, flattened onto the wire (P2 brief D10). A named function rather
// than four lines inside the emitter because this flattening is the whole correctness
// question of set_vertex_attrib_defaults: a CurrentVertexAttributeValue is one value in
// three views and GLContext converts NUMERICALLY between them, so four words alone are
// not the value - glVertexAttrib4f(loc, 1.5f, ...) leaves 1 in intValue and 0x3FC00000 in
// floatValue. MGPAttribValue::ValueClass is what makes the four words readable again, and
// TrackerAttribPayload pins that here instead of leaving it to the emitter's shape.
inline void MGPipeFillAttribValue(Uint32 location,
const MG_State::GLState::CurrentVertexAttributeValue& value,
Uint32 writtenClass, MGPAttribValue& out) {
out = MGPAttribValue{};
out.Location = location;
out.ValueClass = static_cast<Uint8>(writtenClass);
static_assert(sizeof(out.Data) == sizeof(value.floatValue), "MGPAttribValue::Data is four words");
switch (writtenClass) {
case MG_State::GLState::kVertexAttribValueClassInt:
std::memcpy(out.Data, value.intValue.data(), sizeof(out.Data));
break;
case MG_State::GLState::kVertexAttribValueClassUint:
std::memcpy(out.Data, value.uintValue.data(), sizeof(out.Data));
break;
default:
std::memcpy(out.Data, value.floatValue.data(), sizeof(out.Data));
break;
}
}
// The monolith's one tracker. Under split there is one per client context; the context
// identity check inside Update is what makes the single instance safe today.
inline MGPipeTracker& MGPipeTrackerInstance() {
// NEVER DESTROYED, for MGPipeSlots()' reason (MG_Impl/Pipe/SlotAllocator.cpp). The
// rule is stated over the SET of MGPipe process singletons rather than over the two
// that a frontend destructor reaches today: which of them a destructor reaches is a
// property of the emitters, and the emitters change (C-1 added a second reaching
// path in one commit). One allocation per process, no destructor to lose - this type
// has none - and nothing can then answer a late call out of freed storage.
static MGPipeTracker* tracker = new MGPipeTracker();
return *tracker;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH