mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 00:58:30 +09:00
[Feat] (Pipe): land the P4a contract - the resource target enum, the framebuffer target byte, the texture params' builtin sampler, the sampler-parameter field table, four subsystem bits, seven dirty arms and the program archive codec
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/FramebufferEmit.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 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.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR, and it is 0 until the emitter below
|
||||
// has a body. 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.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
||||
|
||||
// set_framebuffer_state. STUB AT THE CONTRACT COMMIT: it emits nothing and returns 0
|
||||
// payload bytes, so the validate point's ladder has its final shape and the package that
|
||||
// fills this in never edits PipeFill.cpp.
|
||||
class MGPipeFramebufferEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||
// LATCHES reset here - the applier's object records survive a make-current and
|
||||
// re-publishing them would move their serials for nothing.
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||
// rule covers every MGPipe process singleton, not only the ones a frontend destructor
|
||||
// reaches today, and it is what keeps exit() out of a torn-down pipe.
|
||||
static MGPipeFramebufferEmitter* emitter = new MGPipeFramebufferEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -0,0 +1,62 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/ImageEmit.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 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.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeImageEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeImageEmitter& MGPipeImageEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||
// intentionally leaked at exit, like every other MGPipe process singleton.
|
||||
static MGPipeImageEmitter* emitter = new MGPipeImageEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -19,9 +19,19 @@
|
||||
#include <MG_State/GLState/StateObjectDeathNotice.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/CsoCache.h>
|
||||
// P4a's five client emitters. This translation unit is the ONLY one that includes them in the
|
||||
// library, exactly as it is for Tracker.h, CsoCache.h, ResourceTracker.h and VertexInputEmit.h
|
||||
// - all of them header-only for the same ownership reason. Each carries its family's
|
||||
// kMGPipeWired*Subsystem constant, so the bit that switches a family on is added by the commit
|
||||
// that gives that family's emitters their bodies, and no two packages ever edit one file.
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#include <MG_Impl/Pipe/ImageEmit.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
@@ -849,6 +859,125 @@ namespace MobileGL::MG_Pipe {
|
||||
return published;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P4a: one client-side death helper per kind P4a mints (D-I1)
|
||||
// ================================================================================
|
||||
//
|
||||
// BACKEND-NEUTRAL FROM THE FIRST COMMIT, which is the whole point: before P3a's C-1 fix
|
||||
// the only thing that ever returned a VertexElementsCso slot was DirectGLES'
|
||||
// StateObjectDeathOps table, so under a backend that installs none every VAO leaked a slot
|
||||
// and a ~1.3 KB applier record for the life of the process. P4a mints SIX kinds and there
|
||||
// is no intermediate state in which a backend table is the only path for any of them.
|
||||
//
|
||||
// THE THREE-STEP ORDER IS FIXED and each position is load-bearing (see PipeMutation.h):
|
||||
// wire delete, then the death notice, then the slot free. Each helper returns whether its
|
||||
// delete actually went out, which is the LATCH taken at the object's create - asking a
|
||||
// live predicate twice pairs a create emitted under one registration with a destroy gated
|
||||
// on another, and either direction leaks.
|
||||
//
|
||||
// EVERY ONE OF THEM IS PUBLISHED-GATED RATHER THAN SLOT-GATED. A slot is not evidence of a
|
||||
// record: a backend twin table mints one through MGPipeSlots().Acquire whether or not the
|
||||
// subsystem ever asked this client to emit a create - which is exactly what a
|
||||
// MOBILEGL_PIPE_PUSH lane with P4a's bits clear runs - and a delete_* on such a handle is
|
||||
// a refused call the applier counts and asserts on. So the emitter is asked
|
||||
// RecordIsPublished(handle) before any delete goes out.
|
||||
//
|
||||
// AT THE CONTRACT COMMIT the five family emitters are stubs that publish nothing, so every
|
||||
// helper here answers false and the legacy path runs unchanged - which is what makes this
|
||||
// commit behaviourally inert while the SHAPE is already the final one.
|
||||
namespace {
|
||||
// Steps 2 and 3, shared: raise the notice while the handle still resolves, then return
|
||||
// the slot. Raised UNCONDITIONALLY, exactly as the five destructors raised it before
|
||||
// P4a: whether a slot exists is this client's business, and a consumer that records
|
||||
// notices must not stop seeing a class announce itself.
|
||||
void NotifyAndFree(MGPipeKind kind, Uint64 lifetimeId, MGPipeHandle handle) {
|
||||
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId);
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published = false; // the sampler emitter publishes nothing yet
|
||||
// A sampler view has no frontend object of its own - it is minted off the texture's
|
||||
// lifetime id - so there is no NotifyStateObjectDestroyed for kind SamplerViewCso to
|
||||
// raise and step 2 is vacuous here. The slot still goes back, last.
|
||||
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::SamplerViewCso, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published = false; // the texture emitter publishes nothing yet
|
||||
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
|
||||
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
|
||||
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
|
||||
// whenever the restrictions move. Released AFTER the texture's own record, so a server
|
||||
// that reads the view to answer "what is this texture" still can while the texture is
|
||||
// being dropped.
|
||||
//
|
||||
// THE BUILT-IN SAMPLER IS NOT RELEASED HERE, and that is a correction to the design
|
||||
// table rather than an omission: the SamplerObject every ITextureObject owns is a real
|
||||
// frontend object with its OWN lifetime id and its own #if MOBILEGL_PIPE_PUSH
|
||||
// destructor, so freeing it from the texture's lifetime id would resolve the wrong slot
|
||||
// (or, worse, a live one belonging to another object). ~SamplerObject runs immediately
|
||||
// after this - a member's destructor follows its owner's body - and takes
|
||||
// MGPipeEmitSamplerCsoDestroyAndFree below, which is the same helper, the same order
|
||||
// and idempotent.
|
||||
MGPipeEmitSamplerViewCsoDestroyAndFree(lifetimeId);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitRenderbufferDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published = false; // the texture/renderbuffer emitter publishes nothing yet
|
||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitFramebufferDestroyAndFree(Uint64 lifetimeId) {
|
||||
// NO WIRE DELETE EXISTS FOR THIS KIND, and none is invented: PipeCalls.def has
|
||||
// resource_destroy and the five delete_* rows and no framebuffer delete, because a
|
||||
// framebuffer is not a resource and is not a CSO - it is STATE, and
|
||||
// set_framebuffer_state is the only call that names one. The catalogue is closed.
|
||||
//
|
||||
// So the handle is minted and freed entirely client-side and this helper is steps 2
|
||||
// and 3 only. What makes a dangling Fbo unreachable is the frontend's own
|
||||
// MarkFramebufferObjectForDeletion path, which already rebinds any slot holding the
|
||||
// victim to framebuffer 0; and a RECYCLED framebuffer handle can never be suppressed
|
||||
// against its predecessor's record, because Fbo carries Gen and Gen is inside the
|
||||
// record's ContentHash.
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
|
||||
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitSamplerCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||
const Bool published = false; // the sampler emitter publishes nothing yet
|
||||
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitShaderCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
// ORDINARY PROGRAMS AND PIPELINE COMPOSITES TAKE THE SAME PATH, deliberately: the
|
||||
// server never learns a composite is a composite, and the only difference on this side
|
||||
// is which band the slot came out of. A composite's slot has TWO independent release
|
||||
// paths - the pipeline cache's LRU eviction and the composite ProgramObject's own
|
||||
// destructor - and the second is a proven no-op, because MGPipeSlotAllocator::Free
|
||||
// refuses a slot that is not live at that generation and bumps no generation of its
|
||||
// own (the bump rides the next handout).
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const Bool published = false; // the program emitter publishes nothing yet
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||
if (verb == nullptr || field == nullptr) {
|
||||
g_omission = PoisonOmission{};
|
||||
@@ -970,6 +1099,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// causes them rather than filled into a PipeInputs field.
|
||||
case MGPipeFieldEmitter::BindVertexElements:
|
||||
return kMGPipeSubsystemVertexInput;
|
||||
// P4a's six emitted rows, across three of its four subsystems. The fourth,
|
||||
// kMGPipeSubsystemTextureResources, names NO emitted field and cannot: the texture
|
||||
// and renderbuffer resource_* calls and set_texture_params are dispatched at the
|
||||
// GL call that causes them rather than filled into a PipeInputs field, exactly as
|
||||
// P3a's buffer family is, so there is no Coverage.def emitted row for them and
|
||||
// there must not be one.
|
||||
case MGPipeFieldEmitter::SetFramebufferState:
|
||||
return kMGPipeSubsystemFramebuffer;
|
||||
case MGPipeFieldEmitter::SetSamplerViews:
|
||||
case MGPipeFieldEmitter::SetShaderImages:
|
||||
return kMGPipeSubsystemSamplers;
|
||||
case MGPipeFieldEmitter::SetDrawProgram:
|
||||
case MGPipeFieldEmitter::SetDispatchProgram:
|
||||
return kMGPipeSubsystemPrograms;
|
||||
case MGPipeFieldEmitter::kNone:
|
||||
break;
|
||||
}
|
||||
@@ -1038,6 +1181,72 @@ namespace MobileGL::MG_Pipe {
|
||||
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||
"the MGPipe vertex-attribute capacity and the frontend's have drifted");
|
||||
|
||||
// ---- P4a's SEVEN pairings, and EVERY ONE OF THEM COMPARES AGAINST
|
||||
// SubsystemForEmitter RATHER THAN AGAINST A CONSTANT. That is the lesson written out
|
||||
// twenty lines above and it is not a style preference: naming the subsystem constant
|
||||
// directly pins the dirty half to a constant instead of pinning the two MAPS to each
|
||||
// other, so an emitter row moved onto another subsystem would still satisfy the
|
||||
// assertion while the emission gate and the residual-fill skip had begun to disagree.
|
||||
//
|
||||
// One emitter row stands for each family: set_framebuffer_state for the framebuffer,
|
||||
// set_sampler_views for the sampler family (set_shader_images is the same subsystem
|
||||
// and is pinned to it below), and set_draw_program for the program family.
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewFramebuffer) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetFramebufferState),
|
||||
"set_framebuffer_state and NEW_FRAMEBUFFER must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplerViews) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetSamplerViews),
|
||||
"set_sampler_views and NEW_SAMPLER_VIEWS must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplers) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetSamplerViews),
|
||||
"bind_sampler_states and NEW_SAMPLERS must name the sampler subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderImages) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetShaderImages),
|
||||
"set_shader_images and NEW_SHADER_IMAGES must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShader) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram),
|
||||
"create/bind_shader_state and NEW_SHADER must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBindings) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram),
|
||||
"the program family and NEW_SHADER_BINDINGS must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewGlobalConstants) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDispatchProgram),
|
||||
"set_global_constants and NEW_GLOBAL_CONSTANTS must name one subsystem");
|
||||
// And the two program emitters really are one subsystem, which is what makes the two
|
||||
// assertions above a statement about the family rather than about one call.
|
||||
static_assert(SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDispatchProgram),
|
||||
"set_draw_program and set_dispatch_program are one family and one A/B");
|
||||
|
||||
// THE TEXTURE-RESOURCE SUBSYSTEM HAS NO DIRTY BIT, and that has to be asserted rather
|
||||
// than left as an absence: its calls are dispatched from the GL entry points that
|
||||
// cause them, so a bit that started naming it would gate the emission twice - once at
|
||||
// the dispatch site and once in the walk - and the two would disagree the first time
|
||||
// one of them was edited. Exactly the shape NoDirtyBitOwnsTheResidualSubsystem uses.
|
||||
constexpr Bool NoDirtyBitOwnsTheTextureResourceSubsystem() {
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||
if (MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(i)) ==
|
||||
kMGPipeSubsystemTextureResources) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(NoDirtyBitOwnsTheTextureResourceSubsystem(),
|
||||
"a MGPipeDirty bit now owns kMGPipeSubsystemTextureResources: the texture "
|
||||
"and renderbuffer resource_* calls are dispatched at the GL call that "
|
||||
"causes them, so a dirty bit would gate them a second time");
|
||||
|
||||
// The two texture-unit capacities are one number on both sides of the boundary, and
|
||||
// this is the one translation unit that sees the frontend constant and the MG_Pipe
|
||||
// one - the same pinning kMGPipeMaxVertexAttribs gets, for the same reason.
|
||||
static_assert(kMGPipeMaxTextureUnits ==
|
||||
static_cast<Uint32>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS),
|
||||
"the MGPipe texture-unit capacity and the frontend's have drifted");
|
||||
static_assert(kMGPipeMaxImageUnits ==
|
||||
static_cast<Uint32>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS),
|
||||
"the MGPipe image-unit capacity and the frontend's have drifted");
|
||||
|
||||
// Which of those subsystems THIS BUILD actually emits for. It grows one commit at a
|
||||
// time, and a field whose emitter is not wired here keeps being pulled - so adding a
|
||||
// row to Coverage.def can never silently drop a field on the floor before the call
|
||||
@@ -1068,12 +1277,43 @@ namespace MobileGL::MG_Pipe {
|
||||
// and every one of those calls lands in RefusedResourceCalls. Bit 7 without bit 8 is
|
||||
// fine. Neither the P3a default (0x1ff, both on) nor G12's control (0x7f, both off)
|
||||
// is in that arm, which is why nothing in the phase trips over it.
|
||||
// P4a's FOUR ARE NOT WRITTEN HERE AT ALL, and that is the structural half of the
|
||||
// ownership rule rather than a stylistic choice. This file is the contract package's
|
||||
// for the entire phase: it carries Coverage.def's enum-coupled switch, the validate
|
||||
// point and the death helpers, so the packages that fill the emitters in must never
|
||||
// edit it - which is exactly the merge trap that produced a push and verify build that
|
||||
// did not compile on the integrated tree while both branches were green apart. So each
|
||||
// family's bit is the value of a constant DEFINED IN THAT FAMILY'S OWN EMIT HEADER,
|
||||
// initialised to 0 there and set to the subsystem constant by the commit that gives
|
||||
// those emitters their bodies. A mistake is then a compile error at the contract
|
||||
// commit, not at the merge, and no file is touched twice.
|
||||
//
|
||||
// The sampler bit covers SamplerEmit.h AND ImageEmit.h: one family, one A/B.
|
||||
constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState |
|
||||
kMGPipeSubsystemPixelPack |
|
||||
kMGPipeSubsystemPatchState |
|
||||
kMGPipeSubsystemVertexAttribDefaults |
|
||||
kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemVertexInput;
|
||||
kMGPipeSubsystemVertexInput |
|
||||
kMGPipeWiredFramebufferSubsystem |
|
||||
kMGPipeWiredTextureSubsystem |
|
||||
kMGPipeWiredSamplerSubsystem |
|
||||
kMGPipeWiredProgramSubsystem;
|
||||
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
|
||||
// this a header that set the wrong constant - the sampler bit in the program header,
|
||||
// say - would switch the wrong family on and every gate would still pass.
|
||||
static_assert(kMGPipeWiredFramebufferSubsystem == 0 ||
|
||||
kMGPipeWiredFramebufferSubsystem == kMGPipeSubsystemFramebuffer,
|
||||
"FramebufferEmit.h's wired constant must be 0 or the framebuffer bit");
|
||||
static_assert(kMGPipeWiredTextureSubsystem == 0 ||
|
||||
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources,
|
||||
"TextureEmit.h's wired constant must be 0 or the texture-resource bit");
|
||||
static_assert(kMGPipeWiredSamplerSubsystem == 0 ||
|
||||
kMGPipeWiredSamplerSubsystem == kMGPipeSubsystemSamplers,
|
||||
"SamplerEmit.h's wired constant must be 0 or the sampler bit");
|
||||
static_assert(kMGPipeWiredProgramSubsystem == 0 ||
|
||||
kMGPipeWiredProgramSubsystem == kMGPipeSubsystemPrograms,
|
||||
"ProgramEmit.h's wired constant must be 0 or the program bit");
|
||||
|
||||
// A field an emitted call supplies COMPLETELY, so the residual fill may stop pulling
|
||||
// it. Two rows of Coverage.def's emitted list do not qualify and each has its reason
|
||||
@@ -1113,11 +1353,36 @@ namespace MobileGL::MG_Pipe {
|
||||
// coming through the residual fill because the mirror is a pointer only the
|
||||
// client can hold. What retires the pull is not a better applier - it is P8,
|
||||
// where the backend stops reading a frontend VAO at all.
|
||||
// P4a's SIX ROWS ARE ALL FALSE, and five of them for GetBoundVertexArray's exact
|
||||
// reason: the field's storage is a frontend heap reference - a
|
||||
// BindingSlot<FramebufferObject>, an ImageTextureBinding, a TextureUnit, two
|
||||
// SharedPtr<ProgramObject> - and the calls that supply them carry eight-byte
|
||||
// {slot, gen} handles and fully resolved descriptors. The applier has no way to
|
||||
// produce a pointer and P4a deliberately does not give it one: a payload never
|
||||
// contains a pointer, and the whole point of the conversion is that the server
|
||||
// stops holding frontend references. Skipping the pull would leave those mirrors
|
||||
// null on every draw of every push build. What retires them is not a better
|
||||
// applier, it is the phase where the backend stops reading a frontend object.
|
||||
//
|
||||
// GetMaxTouchedTextureUnit is the sixth and its argument is different, which is
|
||||
// why it is written out: it is a plain Int, and set_sampler_views' Count IS that
|
||||
// value plus one. But the set is SUPPRESSED on an unchanged content hash and is
|
||||
// emitted only when NEW_SAMPLER_VIEWS fires, and that bit's shutter -
|
||||
// Mix(textureContent, GetTextureBindGeneration()) - does NOT move on a redundant
|
||||
// re-bind of the object a unit already holds, while the high-water mark DOES. So
|
||||
// the applier's Count can lag the frontend's mark by exactly the case the
|
||||
// suppressor exists to swallow, and the field keeps being pulled.
|
||||
constexpr Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field) {
|
||||
switch (field) {
|
||||
case MGPipeInputField::GetPixelStoreParameters:
|
||||
case MGPipeInputField::GetCurrentVertexAttribute:
|
||||
case MGPipeInputField::GetBoundVertexArray:
|
||||
case MGPipeInputField::GetFramebufferBindingSlot:
|
||||
case MGPipeInputField::GetImageTextureBinding:
|
||||
case MGPipeInputField::GetTextureUnitObject:
|
||||
case MGPipeInputField::GetProgramForDraw:
|
||||
case MGPipeInputField::GetProgramForDispatch:
|
||||
case MGPipeInputField::GetMaxTouchedTextureUnit:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
@@ -1496,6 +1761,53 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||
return MGPipeVertexInputEmitterInstance().EmitIndexBuffer(ctx);
|
||||
}
|
||||
|
||||
// ---- P4a's seven emitters (D-C, D-D, D-F, D-G, D-H) ----
|
||||
//
|
||||
// THE SHAPE IS THE CONTRACT COMMIT'S, exactly as P3a's three were: seven adapters
|
||||
// whose bodies live in the five family headers, so the commits that fill those
|
||||
// emitters in never touch this file. Every one of them returns 0 today.
|
||||
//
|
||||
// THE ORDER IS ARCHITECTURE.md 5.4's RECOMMENDED ONE - framebuffer, then program, then
|
||||
// textures/sampler/image/global constants - and that document is explicit that the
|
||||
// order is code organisation and NOT a contract: all of a verb's set_*/bind_* must
|
||||
// complete before the verb, and apart from "a resource create precedes a bind to it"
|
||||
// there is no ordering requirement between them. The server specialises the shader and
|
||||
// the pipeline lazily at the verb, from everything it holds at that moment, which is
|
||||
// what makes deriving the fragColor broadcast count from the framebuffer record legal
|
||||
// at the verb rather than at the FBO sync.
|
||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||
return MGPipeFramebufferEmitterInstance().EmitFramebufferState(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitShaderState(GLContext& ctx) {
|
||||
return MGPipeProgramEmitterInstance().EmitShaderState(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||
return MGPipeProgramEmitterInstance().EmitGlobalConstants(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitSamplerViews(GLContext& ctx) {
|
||||
return MGPipeSamplerEmitterInstance().EmitSamplerViews(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitSamplerStates(GLContext& ctx) {
|
||||
return MGPipeSamplerEmitterInstance().EmitSamplerStates(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||
return MGPipeImageEmitterInstance().EmitShaderImages(ctx);
|
||||
}
|
||||
|
||||
// The texture sub-data DRAIN, and it is the one P4a emitter with no dirty bit over it.
|
||||
// Its calls are dispatched from the GL entry points that cause them (a constructor, a
|
||||
// storage definition, a glTexParameter) and the only thing that has to wait for the
|
||||
// validate point is the accumulated upload, so the gate is the subsystem bit alone.
|
||||
// With nothing dirty the drain list is empty and this is one test.
|
||||
Uint64 DrainTextureSubData(GLContext& ctx) {
|
||||
return MGPipeTextureEmitterInstance().DrainTextureSubData(ctx);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint64 MGPipeVertexAttribDefaultRepairCount() { return g_attribDefaultRepairs; }
|
||||
@@ -1581,9 +1893,60 @@ namespace MobileGL::MG_Pipe {
|
||||
// is not. The resource tracker is deliberately NOT reset here for the same
|
||||
// reason its records survive: see ResourceTracker.h's ResetForTest.
|
||||
MGPipeVertexInputEmitterInstance().Reset();
|
||||
// P4a's five, and ONLY their latches: MGPipeApplierReset clears the framebuffer
|
||||
// records, the three unit sets and the three program handles, so the emitters'
|
||||
// mirrors of those must go with them or the first emission after a make-current
|
||||
// would be suppressed as unchanged and the server would draw with the previous
|
||||
// context's bindings. What must NOT reset is the RECORD half - the applier keeps
|
||||
// its texture, sampler, view and shader-CSO records across a make-current, because
|
||||
// a GL object lives in a share group, and re-publishing one would move its Serial
|
||||
// for nothing.
|
||||
MGPipeFramebufferEmitterInstance().Reset();
|
||||
MGPipeTextureEmitterInstance().Reset();
|
||||
MGPipeSamplerEmitterInstance().Reset();
|
||||
MGPipeImageEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
g_residualDue = true;
|
||||
}
|
||||
|
||||
// P4a's segment, in ARCHITECTURE.md 5.4's RECOMMENDED order - framebuffer, then
|
||||
// program, then textures / sampler / image / global constants - which is why it stands
|
||||
// before the render-state block rather than after it. That order is explicitly code
|
||||
// organisation and not a contract (all of a verb's set_*/bind_* complete before the
|
||||
// verb, and the server specialises lazily AT the verb from everything it then holds),
|
||||
// so nothing about the P2 and P3a emissions changes by standing after it; what it buys
|
||||
// is that the file reads in the order the design states.
|
||||
//
|
||||
// ALL SEVEN ARE STUBS AT THE CONTRACT COMMIT and all four family bits are absent from
|
||||
// kMGPipeWiredSubsystems, so `wants()` is false for every one of them and this whole
|
||||
// block is dead until the packages that own the emitters land. Placing it here, once,
|
||||
// is what keeps those packages out of this file.
|
||||
if (wants(MGPipeDirty::NewFramebuffer)) {
|
||||
payloadBytes += EmitFramebufferState(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewShader) || wants(MGPipeDirty::NewShaderBindings)) {
|
||||
payloadBytes += EmitShaderState(*ctx);
|
||||
}
|
||||
// The texture drain has no dirty bit over it (see its definition); it is gated on the
|
||||
// subsystem bit and on this build having wired the family at all, which is the same
|
||||
// pair `wants()` applies to every other emission.
|
||||
if ((pushMask & kMGPipeSubsystemTextureResources) != 0 &&
|
||||
(kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0) {
|
||||
payloadBytes += DrainTextureSubData(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewSamplerViews)) {
|
||||
payloadBytes += EmitSamplerViews(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewSamplers)) {
|
||||
payloadBytes += EmitSamplerStates(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewShaderImages)) {
|
||||
payloadBytes += EmitShaderImages(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewGlobalConstants)) {
|
||||
payloadBytes += EmitGlobalConstants(*ctx);
|
||||
}
|
||||
|
||||
if (wants(MGPipeDirty::NewPipelineState) || wants(MGPipeDirty::NewRenderState)) {
|
||||
payloadBytes += EmitRenderState(*ctx, dirty, tracker.FreshlyPrimed());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/ProgramEmit.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 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.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeProgramEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// create_shader_state (re-issued on the SAME handle whenever the link version moves -
|
||||
// Gen moves only on slot reuse), then bind_shader_state and set_draw_program /
|
||||
// set_dispatch_program. Two program calls because the frontend has two joins and two
|
||||
// PipeInputs slots.
|
||||
Uint64 EmitShaderState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// set_global_constants: the DEFAULT UNIFORM BLOCK only, keyed (ShaderCso, Version) and
|
||||
// at most once per program per frame. Version is GetUBOContentVersion() and must never
|
||||
// be ~0u, which is the backends' "never uploaded" sentinel - the wrap skips it.
|
||||
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeProgramEmitter& MGPipeProgramEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||
// intentionally leaked at exit, and it MUST NOT hold a frontend SharedPtr - that is
|
||||
// the exit-order rule, stated over every MGPipe process singleton rather than over the
|
||||
// ones a destructor reaches today.
|
||||
static MGPipeProgramEmitter* emitter = new MGPipeProgramEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -57,33 +57,16 @@ namespace MobileGL::MG_Pipe {
|
||||
// D-A3: BindMask
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// MGPResourceDesc::BindMask's twelve bits, in the order MGPipeTypes.h names them:
|
||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY.
|
||||
// MGPResourceDesc::BindMask's twelve bits MOVED TO MG_Pipe/MGPipeTypes.h AT P4a, beside
|
||||
// the field, exactly as the note that stood here said they would when a second producer
|
||||
// appeared: P4a's texture family sets kMGPipeBindSampler / kMGPipeBindShaderImage /
|
||||
// kMGPipeBindRenderTarget / kMGPipeBindDepthStencil, the four bits nothing set before.
|
||||
// No alias is written for them because none is possible or needed - both files are
|
||||
// namespace MobileGL::MG_Pipe and this one includes that header, so every spelling below
|
||||
// and in package B's code is unchanged.
|
||||
//
|
||||
// They are spelled HERE rather than in MGPipeTypes.h because that header is the contract
|
||||
// package's and the mask has, so far, exactly one producer: this file. The integrator
|
||||
// moves them beside the field when a second producer appears (P4a's texture family).
|
||||
enum MGPipeBindBit : Uint16 {
|
||||
kMGPipeBindNone = 0,
|
||||
kMGPipeBindVertex = 1u << 0,
|
||||
kMGPipeBindIndex = 1u << 1,
|
||||
kMGPipeBindConstant = 1u << 2,
|
||||
kMGPipeBindShaderBuffer = 1u << 3,
|
||||
kMGPipeBindIndirect = 1u << 4,
|
||||
kMGPipeBindSampler = 1u << 5,
|
||||
kMGPipeBindShaderImage = 1u << 6,
|
||||
kMGPipeBindRenderTarget = 1u << 7,
|
||||
kMGPipeBindDepthStencil = 1u << 8,
|
||||
kMGPipeBindStreamOutput = 1u << 9,
|
||||
kMGPipeBindAtomic = 1u << 10,
|
||||
// THE D-B7 SWITCH. With kCapNeedsHostIndexBytes set the server mirrors this
|
||||
// resource's bytes so it can rewrite restart indices and flatten multi-draws
|
||||
// (ARCHITECTURE.md 10.3). Getting it wrong is invisible in monolith and silently
|
||||
// disables both under split, which is why it is set from the same table as every
|
||||
// other bit rather than from a special case at the emission site.
|
||||
kMGPipeBindElementArray = 1u << 11,
|
||||
};
|
||||
// What stays here is the BUFFER half of the mapping, which is this file's own: the
|
||||
// BufferTarget table, its sentinel and its completeness assert.
|
||||
|
||||
// A sentinel the table below returns for an enumerator it does not name. It is NOT a
|
||||
// legal mask value: every enumerator must be listed, including the ones that map to no
|
||||
@@ -152,12 +135,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// The discriminators MGPResourceDesc / MGPSubData carry for a BUFFER
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MGPipeTypes.h documents Target as "Buffer | Tex1D..TexCubeArray | Renderbuffer |
|
||||
// TexBuffer" and StorageKind as "== TextureStorageType", but P3a is buffer-only and the
|
||||
// contract package minted no enum for the first list. Buffer is its leading member and
|
||||
// is therefore 0, which is also what a zero-initialised record already says; the second
|
||||
// is the frontend enum, named rather than open-coded.
|
||||
inline constexpr Uint16 kMGPipeResourceTargetBuffer = 0;
|
||||
// P4a MINTED THE FIRST LIST: MGPipeTypes.h now carries enum MGPipeResourceTarget beside
|
||||
// the field, and kMGPipeResourceTargetBuffer moved there with it - the narrowed
|
||||
// resource_respecify ack predicate lives in that header and has to name the buffer target
|
||||
// explicitly, and it may not reach into MG_Impl to do so. The second discriminator is the
|
||||
// frontend enum, named rather than open-coded, and stays here because only this file
|
||||
// produces it.
|
||||
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
||||
static_cast<Uint8>(MobileGL::TextureStorageType::Buffer);
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/SamplerEmit.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 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.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below and in ImageEmit.h have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeSamplerEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// 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.
|
||||
Uint64 EmitSamplerViews(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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.
|
||||
Uint64 EmitSamplerStates(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeSamplerEmitter& MGPipeSamplerEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason - and this one is named in the
|
||||
// phase's own risk list: a new client singleton that held a frontend SharedPtr, or
|
||||
// that had a destructor an exit handler could run into a torn-down pipe, is the
|
||||
// exit-order UAF P3a closed. Heap-constructed and intentionally leaked at exit.
|
||||
static MGPipeSamplerEmitter* emitter = new MGPipeSamplerEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -18,7 +18,9 @@
|
||||
// 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.
|
||||
// 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
|
||||
@@ -40,15 +42,28 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list).
|
||||
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list), PLUS
|
||||
// SetFramebufferState, which is not kVarTail at all: MGPFramebufferState carries a
|
||||
// ContentHash for TWO jobs - the server's render-pass memo key and the client's emission
|
||||
// suppressor - and the second one needs a slot here like any other. The enum is
|
||||
// CLIENT-ONLY and is not a wire opcode, so appending before Count is safe.
|
||||
enum class MGPipeSuppressorSlot : Uint32 {
|
||||
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
||||
SetSamplerViews, // P3b
|
||||
BindSamplerStates, // P3b
|
||||
SetShaderImages, // P4b
|
||||
// P4a - WIRED. The three unit sets' suppressors are not optional and were never a
|
||||
// later phase's: MGPipeTypes.h makes the pattern mandatory for every kVarTail set_*,
|
||||
// because GetTextureBindGeneration() bumps on a REDUNDANT rebind - MC 26.2 rebinds the
|
||||
// same sampler at every texture-unit switch - so an unsuppressed set is a
|
||||
// several-hundred-byte variable-length record per batch, which is the exact regression
|
||||
// the design names. What P3b/P4b owns is the ~175-line BACKEND debounce these replace
|
||||
// (UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and the two
|
||||
// g_*SyncList tables); P4a wires the carrier, P3b/P4b deletes the backend copy.
|
||||
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
SetShaderBuffers, // P4b
|
||||
SetStreamOutputTargets, // P4b
|
||||
SetVertexAttribDefaults, // P2 - the one consumer that is wired
|
||||
SetFramebufferState, // P4a - wired
|
||||
Count,
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,22 @@ namespace MobileGL::MG_Pipe {
|
||||
return m_kinds[index < kKindCount ? index : 0];
|
||||
}
|
||||
|
||||
MGPipeSlotAllocator::SlotState* MGPipeSlotAllocator::EntryOf(KindState& state, MGPipeKind kind,
|
||||
Uint32 slot) {
|
||||
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(slot)) {
|
||||
const SizeT index = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
if (index >= state.BandSlots.size()) return nullptr;
|
||||
return &state.BandSlots[index];
|
||||
}
|
||||
if (slot >= state.Slots.size()) return nullptr;
|
||||
return &state.Slots[slot];
|
||||
}
|
||||
|
||||
const MGPipeSlotAllocator::SlotState*
|
||||
MGPipeSlotAllocator::EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot) {
|
||||
return EntryOf(const_cast<KindState&>(state), kind, slot);
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::Allocate(MGPipeKind kind) {
|
||||
KindState& state = StateOf(kind);
|
||||
if (state.Slots.empty()) {
|
||||
@@ -94,14 +110,67 @@ namespace MobileGL::MG_Pipe {
|
||||
return handle;
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::AllocateComposite(Uint64 lifetimeId) {
|
||||
// P4a, D-H7. The mirror image of Allocate() above, restricted to the band that one
|
||||
// refuses, and kept in a table of its own so both spaces stay DENSE: the band's base
|
||||
// is 983040, and minting one composite into the slot-indexed vector would allocate
|
||||
// ~23 MB of SlotState for a single program pipeline.
|
||||
KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||
|
||||
Uint32 slot = 0;
|
||||
Bool reused = false;
|
||||
if (!state.BandFreeList.empty()) {
|
||||
slot = state.BandFreeList.back();
|
||||
state.BandFreeList.pop_back();
|
||||
reused = true;
|
||||
}
|
||||
|
||||
if (!reused) {
|
||||
const SizeT next = kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size();
|
||||
slot = static_cast<Uint32>(next);
|
||||
// The band's own exhaustion assert, mirroring Allocate()'s: a composite that
|
||||
// cannot be minted is a NAMED failure, not a silent fall-through into the ordinary
|
||||
// program slots, which is exactly what reserving a band rather than setting a flag
|
||||
// buys.
|
||||
MOBILEGL_ASSERT(next < kMGPipeShaderCsoSlotLimit,
|
||||
"the MGPipe ShaderCso COMPOSITE band is exhausted at slot %zu; a "
|
||||
"program-pipeline composite cannot be minted and must not take an "
|
||||
"ordinary program's slot",
|
||||
next);
|
||||
if (next >= kMGPipeShaderCsoSlotLimit) return kMGPipeNullHandle;
|
||||
state.BandSlots.emplace_back();
|
||||
}
|
||||
|
||||
SlotState* entry = EntryOf(state, MGPipeKind::ShaderCso, slot);
|
||||
if (entry == nullptr) return kMGPipeNullHandle;
|
||||
if (entry->EverHandedOut) {
|
||||
MOBILEGL_ASSERT(entry->Gen != ~Uint32{0},
|
||||
"MGPipe handle generation wrapped on the ShaderCso composite band, "
|
||||
"slot %u; {slot, gen} is no longer unique",
|
||||
slot);
|
||||
++entry->Gen;
|
||||
}
|
||||
entry->EverHandedOut = true;
|
||||
entry->Live = true;
|
||||
entry->LifetimeId = lifetimeId;
|
||||
++state.LiveCount;
|
||||
if (lifetimeId != 0) {
|
||||
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
|
||||
"lifetime id %llu already owns a ShaderCso slot",
|
||||
static_cast<unsigned long long>(lifetimeId));
|
||||
state.ByLifetimeId[lifetimeId] = slot;
|
||||
}
|
||||
return MGPipeHandle{slot, entry->Gen};
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
|
||||
if (lifetimeId == 0) return kMGPipeNullHandle;
|
||||
const KindState& state = StateOf(kind);
|
||||
const auto it = state.ByLifetimeId.find(lifetimeId);
|
||||
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
|
||||
const Uint32 slot = it->second;
|
||||
if (slot >= state.Slots.size() || !state.Slots[slot].Live) return kMGPipeNullHandle;
|
||||
return MGPipeHandle{slot, state.Slots[slot].Gen};
|
||||
const SlotState* entry = EntryOf(state, kind, it->second);
|
||||
if (entry == nullptr || !entry->Live) return kMGPipeNullHandle;
|
||||
return MGPipeHandle{it->second, entry->Gen};
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
||||
@@ -112,56 +181,68 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
|
||||
KindState& state = StateOf(kind);
|
||||
if (handle.Slot >= state.Slots.size()) return;
|
||||
SlotState& entry = state.Slots[handle.Slot];
|
||||
SlotState* entry = EntryOf(state, kind, handle.Slot);
|
||||
if (entry == nullptr) return;
|
||||
// A stale handle must not free the slot its successor now owns - that is the whole
|
||||
// reason the generation is in the key.
|
||||
if (!entry.Live || entry.Gen != handle.Gen) return;
|
||||
if (entry.LifetimeId != 0) {
|
||||
const auto it = state.ByLifetimeId.find(entry.LifetimeId);
|
||||
// reason the generation is in the key. It is also what makes the SECOND of a
|
||||
// composite's two independent release paths a proven no-op.
|
||||
if (!entry->Live || entry->Gen != handle.Gen) return;
|
||||
if (entry->LifetimeId != 0) {
|
||||
const auto it = state.ByLifetimeId.find(entry->LifetimeId);
|
||||
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
|
||||
state.ByLifetimeId.erase(it);
|
||||
}
|
||||
}
|
||||
entry.Live = false;
|
||||
entry.LifetimeId = 0;
|
||||
entry->Live = false;
|
||||
entry->LifetimeId = 0;
|
||||
--state.LiveCount;
|
||||
state.FreeList.push_back(handle.Slot);
|
||||
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(handle.Slot)) {
|
||||
state.BandFreeList.push_back(handle.Slot);
|
||||
} else {
|
||||
state.FreeList.push_back(handle.Slot);
|
||||
}
|
||||
}
|
||||
|
||||
Bool MGPipeSlotAllocator::IsLive(MGPipeKind kind, MGPipeHandle handle) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (handle.Slot >= state.Slots.size()) return false;
|
||||
const SlotState& entry = state.Slots[handle.Slot];
|
||||
return entry.Live && entry.Gen == handle.Gen;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, handle.Slot);
|
||||
return entry != nullptr && entry->Live && entry->Gen == handle.Gen;
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (slot >= state.Slots.size()) return 0;
|
||||
return state.Slots[slot].Gen;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||
return entry != nullptr ? entry->Gen : 0;
|
||||
}
|
||||
|
||||
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (slot >= state.Slots.size()) return 0;
|
||||
return state.Slots[slot].LifetimeId;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||
return entry != nullptr ? entry->LifetimeId : 0;
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
|
||||
return static_cast<Uint32>(StateOf(kind).Slots.size());
|
||||
const KindState& state = StateOf(kind);
|
||||
// Literally "one past the highest slot ever handed out", composites included, so a
|
||||
// leaked composite slot moves it exactly as a leaked ordinary one does - which is what
|
||||
// the per-kind leak cases assert on and what would otherwise make the composite case
|
||||
// green for ever and mean nothing.
|
||||
if (!state.BandSlots.empty()) {
|
||||
return static_cast<Uint32>(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size());
|
||||
}
|
||||
return static_cast<Uint32>(state.Slots.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
|
||||
|
||||
Uint32 MGPipeSlotAllocator::FreeCount(MGPipeKind kind) const {
|
||||
return static_cast<Uint32>(StateOf(kind).FreeList.size());
|
||||
const KindState& state = StateOf(kind);
|
||||
return static_cast<Uint32>(state.FreeList.size() + state.BandFreeList.size());
|
||||
}
|
||||
|
||||
void MGPipeSlotAllocator::Reset() {
|
||||
for (KindState& state : m_kinds) {
|
||||
state.Slots.clear();
|
||||
state.FreeList.clear();
|
||||
state.BandSlots.clear();
|
||||
state.BandFreeList.clear();
|
||||
state.ByLifetimeId.clear();
|
||||
state.LiveCount = 0;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,25 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle Allocate(MGPipeKind kind);
|
||||
// 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.
|
||||
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
|
||||
// object, so the map key is unique for the life of the process.
|
||||
@@ -64,8 +83,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 slot ever handed out of this kind, i.e. what a server-side
|
||||
// slot-indexed table must be sized to.
|
||||
// One past the highest slot ever handed out of this kind - which for ShaderCso means
|
||||
// the COMPOSITE band's top once a composite has been minted, because that really is
|
||||
// the highest slot handed out. It is what the leak cases read (a leaked slot of any
|
||||
// kind, composite included, moves it), and it is NOT a table size for kind ShaderCso:
|
||||
// the band is sparse against the ordinary space by design, so a consumer indexing by
|
||||
// slot keeps the band in a table of its own, exactly as this allocator does.
|
||||
Uint32 HighWater(MGPipeKind kind) const;
|
||||
Uint32 LiveCount(MGPipeKind kind) const;
|
||||
Uint32 FreeCount(MGPipeKind kind) const;
|
||||
@@ -85,12 +108,27 @@ namespace MobileGL::MG_Pipe {
|
||||
// Indexed by slot; [0] is the reserved slot and is never live.
|
||||
Vector<SlotState> Slots;
|
||||
Vector<Uint32> FreeList;
|
||||
// P4a: the ShaderCso COMPOSITE band, indexed by (slot - the band's base) and
|
||||
// EMPTY for every other kind. A SECOND VECTOR RATHER THAN MORE OF THE FIRST, and
|
||||
// it is not a micro-optimisation: the band starts at 983040, so minting one
|
||||
// composite into the slot-indexed vector above would allocate ~983k SlotStates -
|
||||
// ~23 MB - for a single program pipeline, and a consumer that sized a table off
|
||||
// HighWater would pay the same shape again with a far bigger record. Both spaces
|
||||
// stay dense against their own high-water mark, which is the property this
|
||||
// allocator exists to give the server.
|
||||
Vector<SlotState> BandSlots;
|
||||
Vector<Uint32> BandFreeList;
|
||||
UnorderedMap<Uint64, Uint32> ByLifetimeId;
|
||||
Uint32 LiveCount = 0;
|
||||
};
|
||||
|
||||
KindState& StateOf(MGPipeKind kind);
|
||||
const KindState& StateOf(MGPipeKind kind) const;
|
||||
// The SlotState a (kind, slot) names, in whichever of the two vectors holds it, or
|
||||
// null when the slot has never been handed out. One resolver, so a caller that forgets
|
||||
// the band cannot exist.
|
||||
static SlotState* EntryOf(KindState& state, MGPipeKind kind, Uint32 slot);
|
||||
static const SlotState* EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot);
|
||||
|
||||
Array<KindState, kKindCount> m_kinds{};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/TextureEmit.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 CLIENT side of P4a's 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.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitter below has a body; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeTextureEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// The DRAIN LIST, at the validate point: one resource_subdata per dirty
|
||||
// (storage owner, upload target, level) that was appended on its FIRST dirty mark and
|
||||
// is cleared at emission. Keyed on the STORAGE OWNER from day one - a view and its
|
||||
// owner already share one dirty state - so an upload through a view and an upload
|
||||
// through the owner land on the same key.
|
||||
//
|
||||
// The client clears its own dirty flags here, and ONLY for the levels whose record the
|
||||
// applier accepted; the applier accumulates the emitted shape into a server-side
|
||||
// pending-upload set that survives Espryt's bail arms, which is what stops a bail from
|
||||
// losing texels.
|
||||
//
|
||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||
Uint64 DrainTextureSubData(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason.
|
||||
static MGPipeTextureEmitter* emitter = new MGPipeTextureEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
+102
-18
@@ -20,9 +20,17 @@
|
||||
//
|
||||
// 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 the
|
||||
// rest are still computed, latched and counted so the per-bit fire rate is a measurement
|
||||
// rather than a plan, with their fields going through the residual fill until P3b/P4a/P4b.
|
||||
// 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 ONE THING: 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. Over-firing is
|
||||
// free; that was an under-fire.
|
||||
//
|
||||
// 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
|
||||
@@ -61,22 +69,24 @@ namespace MobileGL::MG_Pipe {
|
||||
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; the other three are
|
||||
// still computed and counted, and are emitted from P3b/P4a on ----
|
||||
// ---- 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
|
||||
NewShaderBindings, // image units, block bindings, uniform write set
|
||||
NewGlobalConstants, // the default-uniform-block image
|
||||
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 rest
|
||||
// are still computed and counted only. ----
|
||||
// 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,
|
||||
NewSamplerViews,
|
||||
NewSamplers,
|
||||
NewShaderImages,
|
||||
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,
|
||||
@@ -103,6 +113,22 @@ namespace MobileGL::MG_Pipe {
|
||||
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",
|
||||
@@ -146,8 +172,33 @@ namespace MobileGL::MG_Pipe {
|
||||
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 P3b/P4a/P4b, so there is no
|
||||
// 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;
|
||||
}
|
||||
@@ -300,10 +351,36 @@ 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 RENDERBUFFER RESPECIFY IS STILL INVISIBLE HERE, and deliberately so:
|
||||
// RenderbufferObject's SetInternalFormat / AllocateStorage / SetSamples bump no
|
||||
// version and raise no notice, so re-storaging an ALREADY-ATTACHED renderbuffer
|
||||
// moves neither half of this shutter. That hole is closed by emitting
|
||||
// resource_respecify straight from the storage entry point - not by widening this
|
||||
// shutter and not by adding a version counter to RenderbufferObject, which would
|
||||
// resize the pull build's object and break G1.
|
||||
//
|
||||
// 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(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion()));
|
||||
MGPipeMixShutter(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
|
||||
m_readFramebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
|
||||
now[Index(MGPipeDirty::NewSamplerViews)] =
|
||||
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewSamplers)] =
|
||||
@@ -380,6 +457,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_renderStateVersion.Reset();
|
||||
m_pipelineStateVersion.Reset();
|
||||
m_framebufferBind.Reset();
|
||||
m_readFramebufferBind.Reset();
|
||||
m_indexSlotVersion.Reset();
|
||||
m_pack = PixelStoreParameters{};
|
||||
m_patch = PatchTrio{};
|
||||
@@ -469,6 +547,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user