mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +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:
@@ -491,6 +491,12 @@ if (MOBILEGL_PIPE_PUSH)
|
|||||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||||
MobileGL/MG_Pipe/PipeApply.cpp
|
MobileGL/MG_Pipe/PipeApply.cpp
|
||||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||||
|
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||||
|
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||||
|
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||||
|
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||||
|
# where the applier serialises, deserialises and field-compares before storing.
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -322,16 +322,25 @@ namespace MobileGL::MG_Config {
|
|||||||
// 0 - the only shipped value until the migration lands - is "pull everything",
|
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||||
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP3a), so MOBILEGL_PIPE_PUSH=0 in the
|
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a), so MOBILEGL_PIPE_PUSH=0 in the
|
||||||
// environment is the all-pull control and 0x7f (kMGPipeSubsystemsMigratedAtP2) is
|
// environment is the all-pull control and 0x1ff (kMGPipeSubsystemsMigratedAtP3a) is
|
||||||
// the "P2 only" control P3a's A/B is run against. Accepts decimal or 0x-prefixed
|
// the "everything before P4a" control P4a's A/B is run against - each phase's
|
||||||
// hex, and operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h
|
// constant survives as the next phase's control, which is why none of them is ever
|
||||||
// owns them):
|
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
|
||||||
|
// bits are listed here (MG_Pipe/MGPipe.h owns them):
|
||||||
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
||||||
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||||
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||||
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||||
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||||
|
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||||
|
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||||
|
// set_texture_params) - requires 0x80
|
||||||
|
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||||
|
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||||
|
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||||
|
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
|
||||||
|
// family runs its legacy arm; it is never half-run.
|
||||||
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||||
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||||
// never probed. The negative control the CSO design is measured against.
|
// never probed. The negative control the CSO design is measured against.
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
// For kMGPipeSubsystemsMigratedAtP3a, the push build's PipePush default (the P2 constant
|
// For kMGPipeSubsystemsMigratedAtP4a, the push build's PipePush default (the P2 and P3a
|
||||||
// beside it is the phase-by-phase control, not the default). Push-only, so the pull
|
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||||
// build's translation unit is unchanged.
|
// pull build's translation unit is unchanged.
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -252,9 +252,9 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||||
// kMGPipeSubsystemsMigratedAtP2 (0x7f) is the phase-by-phase control - P3a's two
|
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||||
// subsystems off, everything P2 landed still on.
|
// subsystems off, everything P3a landed still on.
|
||||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP3a);
|
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
|
||||||
#else
|
#else
|
||||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||||
// "pull everything" and that stays literally true.
|
// "pull everything" and that stays literally true.
|
||||||
|
|||||||
@@ -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_State/GLState/StateObjectDeathNotice.h>
|
||||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
#include <MG_Impl/Pipe/CsoCache.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/PipeFill.h>
|
||||||
|
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||||
|
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
#include <MG_Impl/Pipe/Tracker.h>
|
#include <MG_Impl/Pipe/Tracker.h>
|
||||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
@@ -849,6 +859,125 @@ namespace MobileGL::MG_Pipe {
|
|||||||
return published;
|
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) {
|
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||||
if (verb == nullptr || field == nullptr) {
|
if (verb == nullptr || field == nullptr) {
|
||||||
g_omission = PoisonOmission{};
|
g_omission = PoisonOmission{};
|
||||||
@@ -970,6 +1099,20 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// causes them rather than filled into a PipeInputs field.
|
// causes them rather than filled into a PipeInputs field.
|
||||||
case MGPipeFieldEmitter::BindVertexElements:
|
case MGPipeFieldEmitter::BindVertexElements:
|
||||||
return kMGPipeSubsystemVertexInput;
|
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:
|
case MGPipeFieldEmitter::kNone:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1038,6 +1181,72 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||||
"the MGPipe vertex-attribute capacity and the frontend's have drifted");
|
"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
|
// 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
|
// 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
|
// 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
|
// 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)
|
// 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.
|
// 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 |
|
constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState |
|
||||||
kMGPipeSubsystemPixelPack |
|
kMGPipeSubsystemPixelPack |
|
||||||
kMGPipeSubsystemPatchState |
|
kMGPipeSubsystemPatchState |
|
||||||
kMGPipeSubsystemVertexAttribDefaults |
|
kMGPipeSubsystemVertexAttribDefaults |
|
||||||
kMGPipeSubsystemResources |
|
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
|
// 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
|
// 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
|
// 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,
|
// 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.
|
// 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) {
|
constexpr Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field) {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case MGPipeInputField::GetPixelStoreParameters:
|
case MGPipeInputField::GetPixelStoreParameters:
|
||||||
case MGPipeInputField::GetCurrentVertexAttribute:
|
case MGPipeInputField::GetCurrentVertexAttribute:
|
||||||
case MGPipeInputField::GetBoundVertexArray:
|
case MGPipeInputField::GetBoundVertexArray:
|
||||||
|
case MGPipeInputField::GetFramebufferBindingSlot:
|
||||||
|
case MGPipeInputField::GetImageTextureBinding:
|
||||||
|
case MGPipeInputField::GetTextureUnitObject:
|
||||||
|
case MGPipeInputField::GetProgramForDraw:
|
||||||
|
case MGPipeInputField::GetProgramForDispatch:
|
||||||
|
case MGPipeInputField::GetMaxTouchedTextureUnit:
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
@@ -1496,6 +1761,53 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||||
return MGPipeVertexInputEmitterInstance().EmitIndexBuffer(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
|
} // namespace
|
||||||
|
|
||||||
Uint64 MGPipeVertexAttribDefaultRepairCount() { return g_attribDefaultRepairs; }
|
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
|
// is not. The resource tracker is deliberately NOT reset here for the same
|
||||||
// reason its records survive: see ResourceTracker.h's ResetForTest.
|
// reason its records survive: see ResourceTracker.h's ResetForTest.
|
||||||
MGPipeVertexInputEmitterInstance().Reset();
|
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;
|
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)) {
|
if (wants(MGPipeDirty::NewPipelineState) || wants(MGPipeDirty::NewRenderState)) {
|
||||||
payloadBytes += EmitRenderState(*ctx, dirty, tracker.FreshlyPrimed());
|
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
|
// D-A3: BindMask
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
// MGPResourceDesc::BindMask's twelve bits, in the order MGPipeTypes.h names them:
|
// MGPResourceDesc::BindMask's twelve bits MOVED TO MG_Pipe/MGPipeTypes.h AT P4a, beside
|
||||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
// the field, exactly as the note that stood here said they would when a second producer
|
||||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY.
|
// 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
|
// What stays here is the BUFFER half of the mapping, which is this file's own: the
|
||||||
// package's and the mask has, so far, exactly one producer: this file. The integrator
|
// BufferTarget table, its sentinel and its completeness assert.
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A sentinel the table below returns for an enumerator it does not name. It is NOT a
|
// 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
|
// 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
|
// The discriminators MGPResourceDesc / MGPSubData carry for a BUFFER
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// MGPipeTypes.h documents Target as "Buffer | Tex1D..TexCubeArray | Renderbuffer |
|
// P4a MINTED THE FIRST LIST: MGPipeTypes.h now carries enum MGPipeResourceTarget beside
|
||||||
// TexBuffer" and StorageKind as "== TextureStorageType", but P3a is buffer-only and the
|
// the field, and kMGPipeResourceTargetBuffer moved there with it - the narrowed
|
||||||
// contract package minted no enum for the first list. Buffer is its leading member and
|
// resource_respecify ack predicate lives in that header and has to name the buffer target
|
||||||
// is therefore 0, which is also what a zero-initialised record already says; the second
|
// explicitly, and it may not reach into MG_Impl to do so. The second discriminator is the
|
||||||
// is the frontend enum, named rather than open-coded.
|
// frontend enum, named rather than open-coded, and stays here because only this file
|
||||||
inline constexpr Uint16 kMGPipeResourceTargetBuffer = 0;
|
// produces it.
|
||||||
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
||||||
static_cast<Uint8>(MobileGL::TextureStorageType::Buffer);
|
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
|
// 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
|
// 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
|
// 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
|
// 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
|
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
|
||||||
@@ -40,15 +42,28 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Pipe {
|
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 {
|
enum class MGPipeSuppressorSlot : Uint32 {
|
||||||
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
||||||
SetSamplerViews, // P3b
|
// P4a - WIRED. The three unit sets' suppressors are not optional and were never a
|
||||||
BindSamplerStates, // P3b
|
// later phase's: MGPipeTypes.h makes the pattern mandatory for every kVarTail set_*,
|
||||||
SetShaderImages, // P4b
|
// 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
|
SetShaderBuffers, // P4b
|
||||||
SetStreamOutputTargets, // P4b
|
SetStreamOutputTargets, // P4b
|
||||||
SetVertexAttribDefaults, // P2 - the one consumer that is wired
|
SetVertexAttribDefaults, // P2 - the one consumer that is wired
|
||||||
|
SetFramebufferState, // P4a - wired
|
||||||
Count,
|
Count,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ namespace MobileGL::MG_Pipe {
|
|||||||
return m_kinds[index < kKindCount ? index : 0];
|
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) {
|
MGPipeHandle MGPipeSlotAllocator::Allocate(MGPipeKind kind) {
|
||||||
KindState& state = StateOf(kind);
|
KindState& state = StateOf(kind);
|
||||||
if (state.Slots.empty()) {
|
if (state.Slots.empty()) {
|
||||||
@@ -94,14 +110,67 @@ namespace MobileGL::MG_Pipe {
|
|||||||
return handle;
|
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 {
|
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
|
||||||
if (lifetimeId == 0) return kMGPipeNullHandle;
|
if (lifetimeId == 0) return kMGPipeNullHandle;
|
||||||
const KindState& state = StateOf(kind);
|
const KindState& state = StateOf(kind);
|
||||||
const auto it = state.ByLifetimeId.find(lifetimeId);
|
const auto it = state.ByLifetimeId.find(lifetimeId);
|
||||||
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
|
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
|
||||||
const Uint32 slot = it->second;
|
const SlotState* entry = EntryOf(state, kind, it->second);
|
||||||
if (slot >= state.Slots.size() || !state.Slots[slot].Live) return kMGPipeNullHandle;
|
if (entry == nullptr || !entry->Live) return kMGPipeNullHandle;
|
||||||
return MGPipeHandle{slot, state.Slots[slot].Gen};
|
return MGPipeHandle{it->second, entry->Gen};
|
||||||
}
|
}
|
||||||
|
|
||||||
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
@@ -112,56 +181,68 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
|
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
|
||||||
KindState& state = StateOf(kind);
|
KindState& state = StateOf(kind);
|
||||||
if (handle.Slot >= state.Slots.size()) return;
|
SlotState* entry = EntryOf(state, kind, handle.Slot);
|
||||||
SlotState& entry = state.Slots[handle.Slot];
|
if (entry == nullptr) return;
|
||||||
// A stale handle must not free the slot its successor now owns - that is the whole
|
// A stale handle must not free the slot its successor now owns - that is the whole
|
||||||
// reason the generation is in the key.
|
// reason the generation is in the key. It is also what makes the SECOND of a
|
||||||
if (!entry.Live || entry.Gen != handle.Gen) return;
|
// composite's two independent release paths a proven no-op.
|
||||||
if (entry.LifetimeId != 0) {
|
if (!entry->Live || entry->Gen != handle.Gen) return;
|
||||||
const auto it = state.ByLifetimeId.find(entry.LifetimeId);
|
if (entry->LifetimeId != 0) {
|
||||||
|
const auto it = state.ByLifetimeId.find(entry->LifetimeId);
|
||||||
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
|
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
|
||||||
state.ByLifetimeId.erase(it);
|
state.ByLifetimeId.erase(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry.Live = false;
|
entry->Live = false;
|
||||||
entry.LifetimeId = 0;
|
entry->LifetimeId = 0;
|
||||||
--state.LiveCount;
|
--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 {
|
Bool MGPipeSlotAllocator::IsLive(MGPipeKind kind, MGPipeHandle handle) const {
|
||||||
const KindState& state = StateOf(kind);
|
const SlotState* entry = EntryOf(StateOf(kind), kind, handle.Slot);
|
||||||
if (handle.Slot >= state.Slots.size()) return false;
|
return entry != nullptr && entry->Live && entry->Gen == handle.Gen;
|
||||||
const SlotState& entry = state.Slots[handle.Slot];
|
|
||||||
return entry.Live && entry.Gen == handle.Gen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
|
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||||
const KindState& state = StateOf(kind);
|
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||||
if (slot >= state.Slots.size()) return 0;
|
return entry != nullptr ? entry->Gen : 0;
|
||||||
return state.Slots[slot].Gen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
|
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||||
const KindState& state = StateOf(kind);
|
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||||
if (slot >= state.Slots.size()) return 0;
|
return entry != nullptr ? entry->LifetimeId : 0;
|
||||||
return state.Slots[slot].LifetimeId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
|
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::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
|
||||||
|
|
||||||
Uint32 MGPipeSlotAllocator::FreeCount(MGPipeKind kind) const {
|
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() {
|
void MGPipeSlotAllocator::Reset() {
|
||||||
for (KindState& state : m_kinds) {
|
for (KindState& state : m_kinds) {
|
||||||
state.Slots.clear();
|
state.Slots.clear();
|
||||||
state.FreeList.clear();
|
state.FreeList.clear();
|
||||||
|
state.BandSlots.clear();
|
||||||
|
state.BandFreeList.clear();
|
||||||
state.ByLifetimeId.clear();
|
state.ByLifetimeId.clear();
|
||||||
state.LiveCount = 0;
|
state.LiveCount = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,25 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeHandle Allocate(MGPipeKind kind);
|
MGPipeHandle Allocate(MGPipeKind kind);
|
||||||
// Allocate and remember `lifetimeId` as this handle's frontend identity.
|
// Allocate and remember `lifetimeId` as this handle's frontend identity.
|
||||||
MGPipeHandle AllocateFor(MGPipeKind kind, Uint64 lifetimeId);
|
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
|
// 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
|
// 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.
|
// 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.
|
// otherwise, live or not.
|
||||||
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
|
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
|
||||||
Uint64 LifetimeIdOfSlot(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
|
// One past the highest slot ever handed out of this kind - which for ShaderCso means
|
||||||
// slot-indexed table must be sized to.
|
// 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 HighWater(MGPipeKind kind) const;
|
||||||
Uint32 LiveCount(MGPipeKind kind) const;
|
Uint32 LiveCount(MGPipeKind kind) const;
|
||||||
Uint32 FreeCount(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.
|
// Indexed by slot; [0] is the reserved slot and is never live.
|
||||||
Vector<SlotState> Slots;
|
Vector<SlotState> Slots;
|
||||||
Vector<Uint32> FreeList;
|
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;
|
UnorderedMap<Uint64, Uint32> ByLifetimeId;
|
||||||
Uint32 LiveCount = 0;
|
Uint32 LiveCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
KindState& StateOf(MGPipeKind kind);
|
KindState& StateOf(MGPipeKind kind);
|
||||||
const KindState& StateOf(MGPipeKind kind) const;
|
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{};
|
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,
|
// 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
|
// 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
|
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and P4a
|
||||||
// rest are still computed, latched and counted so the per-bit fire rate is a measurement
|
// adds SEVEN: 6, 7 and 8 (the program family), 11 (the framebuffer) and 12, 13 and 14 (the
|
||||||
// rather than a plan, with their fields going through the residual fill until P3b/P4a/P4b.
|
// 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
|
// 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
|
// 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
|
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
|
||||||
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
||||||
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
||||||
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a; the other three are
|
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a and the other three from
|
||||||
// still computed and counted, and are emitted from P3b/P4a on ----
|
// 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
|
NewVertexElements, // the bound VAO's attribute configuration -> create/bind_vertex_elements
|
||||||
NewShader, // the current program's link version
|
NewShader, // the current program's link version -> create/bind_shader_state,
|
||||||
NewShaderBindings, // image units, block bindings, uniform write set
|
// set_draw_program, set_dispatch_program (P4a)
|
||||||
NewGlobalConstants, // the default-uniform-block image
|
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
|
// ---- 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
|
// 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
|
// trio, and this comment said otherwise until the commit that wired them. THE NEXT
|
||||||
// are still computed and counted only. ----
|
// FOUR ARE P4a's. The last three are still computed and counted only, until P4b. ----
|
||||||
NewVertexBuffers, // -> set_vertex_buffers (P3a)
|
NewVertexBuffers, // -> set_vertex_buffers (P3a)
|
||||||
NewIndexBuffer, // -> set_index_buffer (P3a)
|
NewIndexBuffer, // -> set_index_buffer (P3a)
|
||||||
NewFramebuffer,
|
NewFramebuffer, // -> set_framebuffer_state, per bound target (P4a)
|
||||||
NewSamplerViews,
|
NewSamplerViews, // -> set_sampler_views (P4a)
|
||||||
NewSamplers,
|
NewSamplers, // -> bind_sampler_states (P4a)
|
||||||
NewShaderImages,
|
NewShaderImages, // -> set_shader_images (P4a)
|
||||||
NewConstBuffers,
|
NewConstBuffers,
|
||||||
NewShaderBuffers,
|
NewShaderBuffers,
|
||||||
NewSoTargets,
|
NewSoTargets,
|
||||||
@@ -103,6 +113,22 @@ namespace MobileGL::MG_Pipe {
|
|||||||
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
|
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
|
||||||
MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers) | MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer);
|
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] = {
|
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
|
||||||
"NEW_RENDER_STATE",
|
"NEW_RENDER_STATE",
|
||||||
"NEW_PIPELINE_STATE",
|
"NEW_PIPELINE_STATE",
|
||||||
@@ -146,8 +172,33 @@ namespace MobileGL::MG_Pipe {
|
|||||||
case MGPipeDirty::NewVertexBuffers:
|
case MGPipeDirty::NewVertexBuffers:
|
||||||
case MGPipeDirty::NewIndexBuffer:
|
case MGPipeDirty::NewIndexBuffer:
|
||||||
return kMGPipeSubsystemVertexInput;
|
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:
|
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.
|
// subsystem to switch and the residual fill keeps supplying their fields.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -300,10 +351,36 @@ namespace MobileGL::MG_Pipe {
|
|||||||
indexObject ? indexObject->GetLifetimeId() : 0);
|
indexObject ? indexObject->GetLifetimeId() : 0);
|
||||||
}
|
}
|
||||||
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(vaoIdentity, indexShutter);
|
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(
|
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
|
||||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
MGPipeMixShutter(
|
||||||
m_framebufferBind.Observe(
|
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion()));
|
m_framebufferBind.Observe(
|
||||||
|
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
|
||||||
|
m_readFramebufferBind.Observe(
|
||||||
|
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
|
||||||
now[Index(MGPipeDirty::NewSamplerViews)] =
|
now[Index(MGPipeDirty::NewSamplerViews)] =
|
||||||
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
||||||
now[Index(MGPipeDirty::NewSamplers)] =
|
now[Index(MGPipeDirty::NewSamplers)] =
|
||||||
@@ -380,6 +457,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
m_renderStateVersion.Reset();
|
m_renderStateVersion.Reset();
|
||||||
m_pipelineStateVersion.Reset();
|
m_pipelineStateVersion.Reset();
|
||||||
m_framebufferBind.Reset();
|
m_framebufferBind.Reset();
|
||||||
|
m_readFramebufferBind.Reset();
|
||||||
m_indexSlotVersion.Reset();
|
m_indexSlotVersion.Reset();
|
||||||
m_pack = PixelStoreParameters{};
|
m_pack = PixelStoreParameters{};
|
||||||
m_patch = PatchTrio{};
|
m_patch = PatchTrio{};
|
||||||
@@ -469,6 +547,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
|
// 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.
|
// that wrapped would let a composite shutter repeat and cost a missed fire.
|
||||||
MGPipeWidenedCounter m_framebufferBind;
|
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
|
// 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
|
// 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
|
// ADD a fire, never drop one, and the VAO identity in the same mix is what makes a
|
||||||
|
|||||||
@@ -195,6 +195,46 @@
|
|||||||
// resolution reads the applier's BoundVertexElements instead of the object - at which point
|
// resolution reads the applier's BoundVertexElements instead of the object - at which point
|
||||||
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
|
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
|
||||||
// rather than silently by this row's presence.
|
// rather than silently by this row's presence.
|
||||||
|
//
|
||||||
|
// P4a ADDS SIX ROWS, and the same note applies to every one of them: each is SHAPE-ONLY, each
|
||||||
|
// lands in PipeFill.cpp's EmittedCallSuppliesTheWholeField FALSE arm, and the decision is
|
||||||
|
// taken THERE rather than inherited from a row's presence here. The rows and their calls:
|
||||||
|
//
|
||||||
|
// GetFramebufferBindingSlot -> SetFramebufferState GetProgramForDraw -> SetDrawProgram
|
||||||
|
// GetImageTextureBinding -> SetShaderImages GetProgramForDispatch -> SetDispatchProgram
|
||||||
|
// GetTextureUnitObject -> SetSamplerViews GetMaxTouchedTextureUnit -> SetSamplerViews
|
||||||
|
//
|
||||||
|
// Five of the six are the pointer-storage case GetBoundVertexArray already documents: the
|
||||||
|
// field is a BindingSlot<FramebufferObject>, an ImageTextureBinding, a TextureUnit or a
|
||||||
|
// SharedPtr<ProgramObject> - frontend heap references - and the calls carry eight-byte
|
||||||
|
// {slot, gen} handles and resolved descriptors. The applier has no way to produce a pointer
|
||||||
|
// and P4a deliberately does not give it one; skipping the pull would leave every one of those
|
||||||
|
// mirrors null on every draw of every push build. What retires those pulls is not a better
|
||||||
|
// applier, it is the phase where the backend stops reading a frontend object at all.
|
||||||
|
//
|
||||||
|
// THE SIXTH IS A DIFFERENT ARGUMENT AND IT IS WORTH WRITING DOWN, because it looks like the
|
||||||
|
// easy one. GetMaxTouchedTextureUnit is a plain Int, and set_sampler_views' Count IS that
|
||||||
|
// value plus one (the second merge rule: a high-water mark is directly the count argument).
|
||||||
|
// But the set is SUPPRESSED on an unchanged content hash and is emitted only when bit 12 fires,
|
||||||
|
// and bit 12's shutter is Mix(textureContent, GetTextureBindGeneration()) - which does NOT
|
||||||
|
// move on a redundant re-bind of the object a unit already holds, while the high-water mark
|
||||||
|
// DOES (see NoteUnitTouched in DirtySurface.def). So the applier's Count can lag the frontend's
|
||||||
|
// high-water mark by exactly the case the suppressor exists to swallow, and the field keeps
|
||||||
|
// being pulled. Narrowing that is P3b/P4b's, with the backend debounce it takes over.
|
||||||
|
//
|
||||||
|
// FOUR ACCESSORS THAT MAP TO A P4a CALL ARE DELIBERATELY NOT HERE, for GetPixelStoreParameters'
|
||||||
|
// reason - a row here says "this field is supplied", and for these it would be a half-truth:
|
||||||
|
// GetActiveTextureUnit - glActiveTexture's selector. set_sampler_views carries the RESOLVED
|
||||||
|
// per-unit set and no active-unit selector at all; nothing on the wire carries it.
|
||||||
|
// GetTextureContextId - a context identity the backend keys its own tables on. No call
|
||||||
|
// carries it and none should: it is the server's question about the client, not state.
|
||||||
|
// GetTextureBindGeneration / GetSamplingResolutionGeneration - frontend SHUTTERS. What
|
||||||
|
// replaces them server-side is the applier's own Serial, which is a different value with a
|
||||||
|
// different owner; claiming the sets supply the generations would make the fill loop skip
|
||||||
|
// two counters no record carries.
|
||||||
|
// And GetTextureObject / GetProgramObject are STICKY (see MGP_COVERAGE_STICKY_LIST): they are
|
||||||
|
// keyed by GL name, they are object lookups rather than verb state, and a forwarded field has
|
||||||
|
// no storage for an emitted call to supply.
|
||||||
#define MGP_COVERAGE_EMITTED_LIST(X) \
|
#define MGP_COVERAGE_EMITTED_LIST(X) \
|
||||||
X(GetBlendColor, SetDynamicState) \
|
X(GetBlendColor, SetDynamicState) \
|
||||||
X(GetBlendEquationIndexed, CreateRenderState) \
|
X(GetBlendEquationIndexed, CreateRenderState) \
|
||||||
@@ -210,8 +250,11 @@
|
|||||||
X(GetDepthFunc, CreateRenderState) \
|
X(GetDepthFunc, CreateRenderState) \
|
||||||
X(GetDepthMask, CreateRenderState) \
|
X(GetDepthMask, CreateRenderState) \
|
||||||
X(GetDepthRangeIndexed, SetDynamicState) \
|
X(GetDepthRangeIndexed, SetDynamicState) \
|
||||||
|
X(GetFramebufferBindingSlot, SetFramebufferState) \
|
||||||
|
X(GetImageTextureBinding, SetShaderImages) \
|
||||||
X(GetLineWidth, SetDynamicState) \
|
X(GetLineWidth, SetDynamicState) \
|
||||||
X(GetLogicOp, CreateRenderState) \
|
X(GetLogicOp, CreateRenderState) \
|
||||||
|
X(GetMaxTouchedTextureUnit, SetSamplerViews) \
|
||||||
X(GetMinSampleShadingValue, CreateRenderState) \
|
X(GetMinSampleShadingValue, CreateRenderState) \
|
||||||
X(GetPatchDefaultInnerLevel, SetPatchState) \
|
X(GetPatchDefaultInnerLevel, SetPatchState) \
|
||||||
X(GetPatchDefaultOuterLevel, SetPatchState) \
|
X(GetPatchDefaultOuterLevel, SetPatchState) \
|
||||||
@@ -221,11 +264,14 @@
|
|||||||
X(GetPolygonOffsetFactor, SetDynamicState) \
|
X(GetPolygonOffsetFactor, SetDynamicState) \
|
||||||
X(GetPolygonOffsetUnits, SetDynamicState) \
|
X(GetPolygonOffsetUnits, SetDynamicState) \
|
||||||
X(GetPrimitiveRestartIndex, SetDynamicState) \
|
X(GetPrimitiveRestartIndex, SetDynamicState) \
|
||||||
|
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||||
|
X(GetProgramForDraw, SetDrawProgram) \
|
||||||
X(GetProvokingVertexMode, CreateRenderState) \
|
X(GetProvokingVertexMode, CreateRenderState) \
|
||||||
X(GetRenderStateParameters, CreateRenderState) \
|
X(GetRenderStateParameters, CreateRenderState) \
|
||||||
X(GetRenderStateParametersVersion, BindRenderState) \
|
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||||
X(GetScissorBox, SetDynamicState) \
|
X(GetScissorBox, SetDynamicState) \
|
||||||
X(GetStencilState, CreateRenderState) \
|
X(GetStencilState, CreateRenderState) \
|
||||||
|
X(GetTextureUnitObject, SetSamplerViews) \
|
||||||
X(GetViewport, SetDynamicState) \
|
X(GetViewport, SetDynamicState) \
|
||||||
X(GetViewportIndexed, SetDynamicState) \
|
X(GetViewportIndexed, SetDynamicState) \
|
||||||
X(IsCapabilityEnabled, CreateRenderState) \
|
X(IsCapabilityEnabled, CreateRenderState) \
|
||||||
|
|||||||
@@ -154,6 +154,21 @@
|
|||||||
// The gate is therefore a COMPLETENESS gate over what the scanner does see. The semantic
|
// The gate is therefore a COMPLETENESS gate over what the scanner does see. The semantic
|
||||||
// proof stays the MOBILEGL_PIPE_VERIFY lane, which is blind to none of them.
|
// proof stays the MOBILEGL_PIPE_VERIFY lane, which is blind to none of them.
|
||||||
//
|
//
|
||||||
|
// THE MUTATOR PREFIX SET WIDENS AT P4a, and what it does NOT gain is the more interesting
|
||||||
|
// half. `pGLContext->` + Add|Set|Mark|Bump|Allocate|Truncate|Record|Notify|Begin|End could
|
||||||
|
// not see `UseProgram`, `BindVertexArray`, `BindProgramPipelineObject` or
|
||||||
|
// `BindTransformFeedbackObject` - four mutators that each move a field P3a or P4a pushes -
|
||||||
|
// because none of them starts with one of those words. `Use` and `Bind` are added, and the
|
||||||
|
// complete set the widening surfaces was enumerated by grep at the phase's base ref so it
|
||||||
|
// cannot surprise anybody: exactly those four names, on seven call sites.
|
||||||
|
//
|
||||||
|
// `Create*` and `Pop*` are DELIBERATELY NOT ADDED. They create or destroy objects rather than
|
||||||
|
// move a pushed field, and each object class's creation and destruction is already answered
|
||||||
|
// twice over - by its own Mark*ForDeletion row below and by the constructor-time
|
||||||
|
// resource_create - so adding them would produce rows that restate an answer this file already
|
||||||
|
// gives, and every one of them would have to be maintained against a mechanism that is not
|
||||||
|
// theirs. A gate whose rows do not each carry their own question is a gate nobody reads.
|
||||||
|
//
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|
||||||
// X(Mutator, Answer)
|
// X(Mutator, Answer)
|
||||||
@@ -279,6 +294,34 @@
|
|||||||
/* so the honest answer is the pull. Narrowing it is P3b's, when it takes the subsystem */ \
|
/* so the honest answer is the pull. Narrowing it is P3b's, when it takes the subsystem */ \
|
||||||
/* over and the binding points get a generation of their own. */ \
|
/* over and the binding points get a generation of their own. */ \
|
||||||
X(SetNamedTransformFeedbackBinding, kPulledEveryVerb) \
|
X(SetNamedTransformFeedbackBinding, kPulledEveryVerb) \
|
||||||
|
/* ---- P4a, THE FOUR THE WIDENED PREFIX SET SURFACES. Every one of them moves a field */ \
|
||||||
|
/* P3a or P4a pushes and none of them was visible to the scan before, because none */ \
|
||||||
|
/* begins with one of the ten words the pattern matched. */ \
|
||||||
|
/* UseProgram is bit 6's whole subject: the shutter is */ \
|
||||||
|
/* Mix(GetCurrentProgram()->GetLifetimeId(), GetLinkVersion()) and glUseProgram is */ \
|
||||||
|
/* what moves the object it reads through. Two call sites. */ \
|
||||||
|
/* BindVertexArray is bit 5's, for the same reason one level down: the shutter mixes */ \
|
||||||
|
/* the bound VAO's identity with its configuration version, and this is the bind. */ \
|
||||||
|
/* Three call sites. */ \
|
||||||
|
X(UseProgram, NEW_SHADER) \
|
||||||
|
X(BindVertexArray, NEW_VERTEX_ELEMENTS) \
|
||||||
|
/* NOT NEW_SHADER, and the derivation refutes it outright rather than leaving it a */ \
|
||||||
|
/* judgement: this mutator writes m_boundProgramPipeline (plus the pipeline name table) */ \
|
||||||
|
/* and bit 6's shutter reads m_currentProgram's lifetime id and link version - disjoint */ \
|
||||||
|
/* sets, on every path. That is not an oversight in the shutter either: it reads */ \
|
||||||
|
/* GetCurrentProgram() and DELIBERATELY NOT GetProgramForDraw(), because the tracker */ \
|
||||||
|
/* must not force a compile just to answer "did the shader move", and flattening a */ \
|
||||||
|
/* pipeline into its composite is exactly the compile it would force. What a bind moves */ \
|
||||||
|
/* is which program the validate point will flatten, and that field - */ \
|
||||||
|
/* GetProgramForDraw - is in the may-read mask of every class that draws and is copied */ \
|
||||||
|
/* by the residual fill at every verb of those classes, EMITTED-AND-STILL-PULLED like */ \
|
||||||
|
/* GetBoundVertexArray. So the pull is what holds on every path, and it is the answer. */ \
|
||||||
|
X(BindProgramPipelineObject, kPulledEveryVerb) \
|
||||||
|
/* No shutter at all, and none is needed: the transform-feedback binding reaches the */ \
|
||||||
|
/* backend through GetBoundTransformFeedbackLifetimeId and its siblings, which are in */ \
|
||||||
|
/* the kDraw and kXfbSpan may-read masks, so the residual fill copies them at every */ \
|
||||||
|
/* verb of those classes. Narrowing it is P4b's, with set_stream_output_targets. */ \
|
||||||
|
X(BindTransformFeedbackObject, kPulledEveryVerb) \
|
||||||
/* ---- an object's death: no generation, because there is no longer an object */ \
|
/* ---- an object's death: no generation, because there is no longer an object */ \
|
||||||
/* to carry one. Espryt 0b's delete_* / resource_destroy publishes the kinds */ \
|
/* to carry one. Espryt 0b's delete_* / resource_destroy publishes the kinds */ \
|
||||||
/* that have a handle on the wire; programs, program pipelines and shaders have */ \
|
/* that have a handle on the wire; programs, program pipelines and shaders have */ \
|
||||||
@@ -296,9 +339,24 @@
|
|||||||
/* Destroyed consumer, package espryt), not the client's: the client mints */ \
|
/* Destroyed consumer, package espryt), not the client's: the client mints */ \
|
||||||
/* the CSO handle and emits create/bind, and the free rides with that */ \
|
/* the CSO handle and emits create/bind, and the free rides with that */ \
|
||||||
/* consumer. Until it lands the row states the design, not the tree. */ \
|
/* consumer. Until it lands the row states the design, not the tree. */ \
|
||||||
|
/* P4a CLOSES ONE OF THE THREE HOLES ABOVE AND STATES WHY THE OTHER TWO ARE NOT HOLES. */ \
|
||||||
|
/* MarkProgramForDeletion -> kExplicitDestroy. delete_shader_state exists now and */ \
|
||||||
|
/* ~ProgramObject emits it through the client-side death helper, in the fixed */ \
|
||||||
|
/* order: the wire delete first, the backend notice second, the slot free last. A */ \
|
||||||
|
/* program pipeline COMPOSITE takes the same call on the same helper - the server */ \
|
||||||
|
/* never learns it is a composite. */ \
|
||||||
|
/* MarkProgramPipelineForDeletion stays kUnpublishedDestroy, and it is NOT waiting */ \
|
||||||
|
/* for a later phase: a ProgramPipelineObject has no lifetime id and no wire object */ \
|
||||||
|
/* at all (its only identity is m_everBound). It never gets a handle, so there is */ \
|
||||||
|
/* nothing for a delete to name. What its cache's eviction DOES publish is the */ \
|
||||||
|
/* composite's delete_shader_state, which is the row above. */ \
|
||||||
|
/* MarkShaderForDeletion stays kUnpublishedDestroy for the same kind of reason: a */ \
|
||||||
|
/* ShaderObject has no lifetime id and never crosses the boundary - the payload is */ \
|
||||||
|
/* per-stage SPIR-V plus the reflection archive, not source, and glslang lives */ \
|
||||||
|
/* entirely on the client. */ \
|
||||||
X(MarkBufferObjectForDeletion, kExplicitDestroy) \
|
X(MarkBufferObjectForDeletion, kExplicitDestroy) \
|
||||||
X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
|
X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
|
||||||
X(MarkProgramForDeletion, kUnpublishedDestroy) \
|
X(MarkProgramForDeletion, kExplicitDestroy) \
|
||||||
X(MarkProgramPipelineForDeletion, kUnpublishedDestroy) \
|
X(MarkProgramPipelineForDeletion, kUnpublishedDestroy) \
|
||||||
X(MarkRenderbufferObjectForDeletion, kExplicitDestroy) \
|
X(MarkRenderbufferObjectForDeletion, kExplicitDestroy) \
|
||||||
X(MarkSamplerObjectForDeletion, kExplicitDestroy) \
|
X(MarkSamplerObjectForDeletion, kExplicitDestroy) \
|
||||||
@@ -326,9 +384,32 @@
|
|||||||
// UNDECIDED, each with the reason --check prints for it. Every bit answer NOT listed here
|
// UNDECIDED, each with the reason --check prints for it. Every bit answer NOT listed here
|
||||||
// is marked derived: --check fails when the derivation cannot decide it, and fails again
|
// is marked derived: --check fails when the derivation cannot decide it, and fails again
|
||||||
// when a mark here names a pair the derivation now decides, so this list can neither hide a
|
// when a mark here names a pair the derivation now decides, so this list can neither hide a
|
||||||
// row nor outlive its reason. Empty today: every bit answer above is supported at field
|
// row nor outlive its reason.
|
||||||
// level. The ten mutators that reach a tainted body (--check prints the count) all carry a
|
//
|
||||||
// prose answer, which no derivation checks.
|
// IT WAS EMPTY UNTIL P4a, and it stops being empty for a reason that is a property of the
|
||||||
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X)
|
// SCANNER rather than of the two rows. Both entries below are bit answers that are plainly
|
||||||
|
// true - glUseProgram is what moves the object bit 6's shutter reads through, and
|
||||||
|
// glBindVertexArray is what moves the object bit 5's shutter reads through - and the write
|
||||||
|
// analysis cannot say so, because each of them reaches, BY NAME, a body that writes a member
|
||||||
|
// with no m_ prefix:
|
||||||
|
//
|
||||||
|
// UseProgram -> DestroyProgramSlot() writes `attachedShaders`
|
||||||
|
// BindVertexArray -> a call spelled `Bind(` resolves to every body of that name, one of
|
||||||
|
// which (ImageTextureBinding::Bind) writes `Access`
|
||||||
|
//
|
||||||
|
// A call resolved by name to every body of that name is one of the three over-approximations
|
||||||
|
// this analysis documents about itself, and an unplaceable write TAINTS the body it is in -
|
||||||
|
// which is the right default, because "it does not write anything the shutter reads" must
|
||||||
|
// never be claimed about code the script could not read. Widening the taint rule to ignore
|
||||||
|
// non-m_ writes would weaken the one mechanism that catches a genuine under-fire, so the rows
|
||||||
|
// are MARKED, with the tool's own reason, rather than the tool being made more permissive.
|
||||||
|
// Control 9c is what proves a marked row still needs the mark, and control 18 is what fails
|
||||||
|
// the moment either of these becomes decidable and the mark outlives its reason.
|
||||||
|
//
|
||||||
|
// The ten mutators that reach a tainted body (--check prints the count) all carry a prose
|
||||||
|
// answer, which no derivation checks; these two are the first that carry a bit answer.
|
||||||
|
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X) \
|
||||||
|
X(UseProgram, NEW_SHADER) \
|
||||||
|
X(BindVertexArray, NEW_VERTEX_ELEMENTS)
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -83,7 +83,30 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// the other.
|
// the other.
|
||||||
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7;
|
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7;
|
||||||
inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8;
|
inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8;
|
||||||
// bits 9..62 reserved for the later phases, allocated in ROADMAP order.
|
// P4a's four. 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, and clearing one must not disarm the
|
||||||
|
// other three.
|
||||||
|
//
|
||||||
|
// THREE OF THEM HAVE A DEPENDENCY and it is diagnosed at the first use, never half-run -
|
||||||
|
// one Resolve<Family>SubsystemArm per family beside the backend's existing
|
||||||
|
// ResolveResourceSubsystemArm, modelled on the bit-8-requires-bit-7 refusal it already
|
||||||
|
// ships, and lazy rather than at bring-up because a pre-flight child dying on a signal
|
||||||
|
// makes a whole lane SKIP green: bit 11 requires bit 10 because
|
||||||
|
// every MGPBoundView::Texture and MGPImageView::Res names a Texture handle and only bit 10
|
||||||
|
// puts one in the slot table; bit 9 requires bit 10 because MGPSurface::Res does; and bit
|
||||||
|
// 10 requires bit 7 because a buffer texture's BufferForTexBuffer names a Buffer handle.
|
||||||
|
// The mirror pairs (10 without 11, 10 without 9, 7 without 10) are all fine, and are
|
||||||
|
// stated as such because an unreachable branch that says something different is how the
|
||||||
|
// reachable one drifts. Bit 12 depends on nothing.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemFramebuffer = 1ull << 9; // set_framebuffer_state
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemTextureResources = 1ull << 10; // texture + renderbuffer
|
||||||
|
// resource_*, set_texture_params
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemSamplers = 1ull << 11; // sampler CSO, sampler view,
|
||||||
|
// the three unit sets
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemPrograms = 1ull << 12; // shader CSO, draw/dispatch
|
||||||
|
// program, global constants
|
||||||
|
// bits 13..62 reserved for the later phases, allocated in ROADMAP order.
|
||||||
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
|
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
|
||||||
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
|
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
|
||||||
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
|
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
|
||||||
@@ -93,6 +116,14 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// "everything P2 had and nothing of mine" arm is spelled MOBILEGL_PIPE_PUSH=0x7f.
|
// "everything P2 had and nothing of mine" arm is spelled MOBILEGL_PIPE_PUSH=0x7f.
|
||||||
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
|
||||||
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8
|
||||||
|
// P4a's, and the two above are NOT edited: 0x1ff is P4a's T2 arm and its "everything P3a
|
||||||
|
// had and nothing of mine" control, exactly as 0x7f was P3a's.
|
||||||
|
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP4a = 0x1fffull; // bits 0..12
|
||||||
|
static_assert(kMGPipeSubsystemsMigratedAtP4a ==
|
||||||
|
(kMGPipeSubsystemsMigratedAtP3a | kMGPipeSubsystemFramebuffer |
|
||||||
|
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers |
|
||||||
|
kMGPipeSubsystemPrograms),
|
||||||
|
"the P4a phase constant and P4a's four subsystem bits have drifted");
|
||||||
|
|
||||||
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
|
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
|
||||||
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// a pipeline object, and the server never learns it is a composite - it is just another
|
// a pipeline object, and the server never learns it is a composite - it is just another
|
||||||
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
||||||
// lifetime bookkeeping out of the ordinary program slot allocator.
|
// lifetime bookkeeping out of the ordinary program slot allocator.
|
||||||
|
//
|
||||||
|
// THE ONE ENTRY POINT INTO THE BAND is MGPipeSlotAllocator::AllocateComposite(lifetimeId)
|
||||||
|
// (MG_Impl/Pipe/SlotAllocator.h, P4a D-H7). MGPipeSlotAllocator::Allocate REFUSES the band
|
||||||
|
// for kind ShaderCso, which is what makes "an ordinary program can never be handed a
|
||||||
|
// composite slot" a property of the allocator rather than of its callers; the band carries
|
||||||
|
// its own exhaustion assert, so exhausting it is a named Fatal rather than silent slot
|
||||||
|
// theft from ordinary programs. A composite's slot has TWO independent release paths - the
|
||||||
|
// pipeline cache's LRU eviction and the composite ProgramObject's own destructor - and
|
||||||
|
// both go through one client-side death helper (MG_Pipe/PipeMutation.h's
|
||||||
|
// MGPipeEmitShaderCsoDestroyAndFree), whose second call is a proven no-op because Free
|
||||||
|
// refuses a slot that is not live at that generation.
|
||||||
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20;
|
||||||
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase =
|
||||||
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
||||||
|
|||||||
+231
-16
@@ -145,15 +145,152 @@ namespace MobileGL::MG_Pipe {
|
|||||||
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
||||||
"MGPCaps gained padding or a member; update the wire format");
|
"MGPCaps gained padding or a member; update the wire format");
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// MGPResourceDesc's two discriminators (P4a, D-A3 / D-A4)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MGPResourceDesc::Target. P3a minted no enum for this list because it had exactly one
|
||||||
|
// producer and used the leading member's value (0) for it; P4a's texture family is the
|
||||||
|
// second producer, so the list is written out here, beside the field, in the order the
|
||||||
|
// field's own comment already wrote it.
|
||||||
|
//
|
||||||
|
// TexRect IS A THIRTEENTH ENUMERATOR AND THE BRIEF'S LIST HAS TWELVE. MobileGL's
|
||||||
|
// TextureTarget has TextureRectangle (MG_State/GLState/TextureState/TextureEnum.h), the
|
||||||
|
// table below may not have a `default:` arm, and folding rectangle onto Tex2D would erase
|
||||||
|
// a distinction the frontend keeps and both backends switch on (Espryt's
|
||||||
|
// MapToBackendTextureTarget lowers Tex1D the same way and Tex1D still has its own
|
||||||
|
// enumerator here). It is appended AFTER TexBuffer so every value the design document
|
||||||
|
// names keeps the number it was given.
|
||||||
|
enum class MGPipeResourceTarget : Uint8 {
|
||||||
|
Buffer = 0,
|
||||||
|
Tex1D,
|
||||||
|
Tex2D,
|
||||||
|
Tex3D,
|
||||||
|
Tex1DArray,
|
||||||
|
Tex2DArray,
|
||||||
|
TexCube,
|
||||||
|
TexCubeArray,
|
||||||
|
Tex2DMS,
|
||||||
|
Tex2DMSArray,
|
||||||
|
Renderbuffer,
|
||||||
|
TexBuffer,
|
||||||
|
TexRect,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// P3a's constant, moved here from MG_Impl/Pipe/ResourceTracker.h with the enum: the ack
|
||||||
|
// predicate at the bottom of this header now names the buffer target explicitly (D-A2) and
|
||||||
|
// may not reach into MG_Impl to do it. The static_assert is what keeps the two spellings
|
||||||
|
// from drifting; nothing may open-code either.
|
||||||
|
inline constexpr Uint8 kMGPipeResourceTargetBuffer =
|
||||||
|
static_cast<Uint8>(MGPipeResourceTarget::Buffer);
|
||||||
|
static_assert(kMGPipeResourceTargetBuffer == static_cast<Uint8>(MGPipeResourceTarget::Buffer),
|
||||||
|
"P3a's kMGPipeResourceTargetBuffer and MGPipeResourceTarget::Buffer have drifted");
|
||||||
|
|
||||||
|
// A sentinel the table below returns for a TextureTarget enumerator it does not name. It
|
||||||
|
// is NOT a legal Target value - it does not fit the field's Uint8 - so an unmapped
|
||||||
|
// enumerator is a build break at the static_assert rather than a descriptor that quietly
|
||||||
|
// describes the wrong kind of storage. Exactly kMGPipeBindUnmapped's shape.
|
||||||
|
inline constexpr Uint32 kMGPipeResourceTargetUnmapped = 0x100u;
|
||||||
|
|
||||||
|
// The one table. No `default:` arm on purpose - that is what makes the static_assert
|
||||||
|
// below able to see an unnamed enumerator, and it is the shape
|
||||||
|
// MGPipeBindMaskForBufferTarget already uses for BufferTarget.
|
||||||
|
constexpr Uint32 MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget target) {
|
||||||
|
switch (target) {
|
||||||
|
case MobileGL::TextureTarget::Texture1D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex1D);
|
||||||
|
case MobileGL::TextureTarget::Texture2D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2D);
|
||||||
|
case MobileGL::TextureTarget::Texture3D:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex3D);
|
||||||
|
case MobileGL::TextureTarget::TextureCubeMap:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexCube);
|
||||||
|
// Its own enumerator rather than Tex2D: see the enum's comment.
|
||||||
|
case MobileGL::TextureTarget::TextureRectangle:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexRect);
|
||||||
|
case MobileGL::TextureTarget::Texture2DMultisample:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMS);
|
||||||
|
case MobileGL::TextureTarget::TextureBuffer:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexBuffer);
|
||||||
|
case MobileGL::TextureTarget::Texture1DArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex1DArray);
|
||||||
|
case MobileGL::TextureTarget::Texture2DArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DArray);
|
||||||
|
case MobileGL::TextureTarget::TextureCubeMapArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::TexCubeArray);
|
||||||
|
case MobileGL::TextureTarget::Texture2DMultisampleArray:
|
||||||
|
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMSArray);
|
||||||
|
// NOT TEXTURE TARGETS. Listed rather than defaulted so the completeness assert still
|
||||||
|
// sees them, and mapped to the sentinel because no descriptor may carry either: the
|
||||||
|
// count is the enum's bound and Unknown is what an unresolved GL enum becomes.
|
||||||
|
case MobileGL::TextureTarget::TextureTargetCount:
|
||||||
|
case MobileGL::TextureTarget::Unknown:
|
||||||
|
return kMGPipeResourceTargetUnmapped;
|
||||||
|
}
|
||||||
|
return kMGPipeResourceTargetUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Bool MGPipeEveryTextureTargetIsMapped() {
|
||||||
|
for (SizeT i = 0; i < static_cast<SizeT>(MobileGL::TextureTarget::TextureTargetCount); ++i) {
|
||||||
|
if (MGPipeResourceTargetForTextureTarget(static_cast<MobileGL::TextureTarget>(i)) ==
|
||||||
|
kMGPipeResourceTargetUnmapped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(MGPipeEveryTextureTargetIsMapped(),
|
||||||
|
"a TextureTarget enumerator has no MGPResourceDesc::Target row: add it to "
|
||||||
|
"MGPipeResourceTargetForTextureTarget, and give it an enumerator of its own "
|
||||||
|
"rather than folding it onto a neighbour (D-A3)");
|
||||||
|
static_assert(MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget::Texture2D) !=
|
||||||
|
MGPipeResourceTargetForTextureTarget(MobileGL::TextureTarget::TextureRectangle),
|
||||||
|
"a rectangle texture is not a 2D texture on the wire; both backends switch on "
|
||||||
|
"the difference");
|
||||||
|
|
||||||
|
// MGPResourceDesc::BindMask's twelve bits, in the order the field's comment names them.
|
||||||
|
//
|
||||||
|
// THEY LIVED IN MG_Impl/Pipe/ResourceTracker.h THROUGH P3a, with that file's own note
|
||||||
|
// saying "the integrator moves them beside the field when a second producer appears
|
||||||
|
// (P4a's texture family)". P4a is that producer: a texture sets kMGPipeBindSampler,
|
||||||
|
// kMGPipeBindShaderImage, kMGPipeBindRenderTarget and kMGPipeBindDepthStencil, which are
|
||||||
|
// the four bits nothing set before. The mask is STICKY - ORed, never cleared - and is
|
||||||
|
// emitted on both resource_create and every resource_respecify.
|
||||||
|
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 a table rather than from a
|
||||||
|
// special case at the emission site.
|
||||||
|
kMGPipeBindElementArray = 1u << 11,
|
||||||
|
};
|
||||||
|
|
||||||
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
|
// Discriminated resource descriptor: buffers, every texture target and renderbuffers
|
||||||
// share one create/respecify shape (section 4.5.1).
|
// share one create/respecify shape (section 4.5.1).
|
||||||
struct MGPResourceDesc {
|
struct MGPResourceDesc {
|
||||||
MGPipeHandle Resource;
|
MGPipeHandle Resource;
|
||||||
Uint8 Target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
|
// MGPipeResourceTarget: Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer |
|
||||||
|
// TexBuffer | TexRect. Never open-coded; the texture half comes from
|
||||||
|
// MGPipeResourceTargetForTextureTarget above.
|
||||||
|
Uint8 Target;
|
||||||
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
|
Uint8 StorageKind; // == TextureStorageType (Mipmap | Buffer)
|
||||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
// MGPipeBindBit, above: VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|
|
||||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
|
// SHADER_IMAGE|RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The
|
||||||
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
|
// ELEMENT_ARRAY bit is the D-B7 switch: with kCapNeedsHostIndexBytes set the server
|
||||||
|
// mirrors this resource.
|
||||||
Uint16 BindMask;
|
Uint16 BindMask;
|
||||||
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
||||||
Uint32 Width, Height, Depth;
|
Uint32 Width, Height, Depth;
|
||||||
@@ -304,18 +441,35 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGP_ASSERT_POD(MGPSamplerView, 36);
|
MGP_ASSERT_POD(MGPSamplerView, 36);
|
||||||
|
|
||||||
// Per texture OBJECT, independent of any view.
|
// Per texture OBJECT, independent of any view.
|
||||||
|
//
|
||||||
|
// P4a, D-E1: 32 -> 40 bytes. BuiltinSampler is the SamplerCso carrying the
|
||||||
|
// SamplerParameters of the SamplerObject every ITextureObject owns
|
||||||
|
// (TextureState/TextureObject.h's m_sampler, constructed by TextureObjectBase's
|
||||||
|
// constructor). GL 4.6 core table 23.18 makes filter/wrap/compare/border SAMPLER state,
|
||||||
|
// and Espryt pushes it with glTexParameter* onto the TEXTURE rather than with
|
||||||
|
// glBindSampler onto the unit - behaviour P4a preserves exactly. Naming the CSO rather
|
||||||
|
// than widening this payload with a filter/wrap/border block is what keeps ONE authority
|
||||||
|
// for one value: SyncTextureParamsToBackend reads this record, SyncBuiltinSamplerToBackend
|
||||||
|
// reads that CSO's SamplerParameters, and the two pushes stay two pushes.
|
||||||
struct MGPTextureParams {
|
struct MGPTextureParams {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res; // 0
|
||||||
Uint16 BaseLevel, MaxLevel;
|
// Kind SamplerCso. kMGPipeNullHandle is ILLEGAL - every texture object owns a sampler
|
||||||
Uint8 Swizzle[4];
|
// object, so a null here is Fatal{ProtocolCorruption} rather than "no sampler".
|
||||||
Uint8 DepthStencilMode;
|
MGPipeHandle BuiltinSampler; // 8
|
||||||
|
Uint16 BaseLevel, MaxLevel; // 16
|
||||||
|
Uint8 Swizzle[4]; // 20
|
||||||
|
Uint8 DepthStencilMode; // 24
|
||||||
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
||||||
// override that the frontend params version does not move for.
|
// override that the frontend params version does not move for.
|
||||||
Uint8 ForceResync;
|
Uint8 ForceResync; // 25
|
||||||
Uint8 Pad0[2];
|
// Mirrors m_forceSamplerResync, which had no wire spelling at all before P4a. What it
|
||||||
Float MinLod, MaxLod, LodBias;
|
// guards is not mis-filtering but an INCOMPLETE texture sampling (0,0,0,1) after a
|
||||||
|
// driver re-mint, which is why it is a second bit and not folded into ForceResync.
|
||||||
|
Uint8 SamplerResync; // 26
|
||||||
|
Uint8 Pad0; // 27
|
||||||
|
Float MinLod, MaxLod, LodBias; // 28
|
||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPTextureParams, 32);
|
MGP_ASSERT_POD(MGPTextureParams, 40);
|
||||||
|
|
||||||
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
|
// create_shader_state. The reflection blob is the whole LinkArtifacts + SpirvArtifacts
|
||||||
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
|
// archive; P0.5 extracts those types out of ProgramObject.h so a server can
|
||||||
@@ -352,6 +506,35 @@ namespace MobileGL::MG_Pipe {
|
|||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPSurface, 24);
|
MGP_ASSERT_POD(MGPSurface, 24);
|
||||||
|
|
||||||
|
// P4a, D-C2/D-C3: the record is emitted PER BOUND TARGET.
|
||||||
|
//
|
||||||
|
// GL has two independent framebuffer bindings and this record carries one Fbo and one
|
||||||
|
// ReadSurface, so Target says which binding it describes: 0 = Draw, 1 = Read, 2 = Both
|
||||||
|
// (one object bound to both targets). The draw-buffer array is applied only for a record
|
||||||
|
// whose Target is not Read - Espryt's own comment records the Minecraft 26.x OIT bug
|
||||||
|
// where a READ-only sync landed glDrawBuffers on the wrong framebuffer - and ReadSurface
|
||||||
|
// is resolved from the READ framebuffer's own read buffer, which is what makes the
|
||||||
|
// read-buffer-shared-FBO defect class unrepresentable rather than merely fixed.
|
||||||
|
enum class MGPipeFramebufferTarget : Uint8 {
|
||||||
|
Draw = 0,
|
||||||
|
Read = 1,
|
||||||
|
Both = 2,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MGPFramebufferState::Color[] and DrawBuffers[] are ONE array width, and it is the wire's
|
||||||
|
// bound rather than the driver's: GetDynamicParameters().MaxColorAttachments is the raw ES
|
||||||
|
// cap and is not clamped to 8 on the GLES path, so a driver reporting more would silently
|
||||||
|
// truncate this record. The framebuffer subsystem bit is REFUSED at its first lookup in
|
||||||
|
// that case, with one ERROR naming the cap, and the legacy arm runs - the same shape the
|
||||||
|
// backend's existing bit-8-requires-bit-7 refusal already ships
|
||||||
|
// (ResolveFramebufferSubsystemArm, beside ResolveResourceSubsystemArm). Widening the
|
||||||
|
// payload is a wire change nobody has evidence for, and truncating silently is the bug
|
||||||
|
// class this phase is closing.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxColorAttachments = 8;
|
||||||
|
static_assert(kMGPipeMaxColorAttachments == MobileGL::kMGMaxDrawBuffers,
|
||||||
|
"MGPFramebufferState::Color[] and DrawBuffers[] are one array width");
|
||||||
|
|
||||||
struct MGPFramebufferState {
|
struct MGPFramebufferState {
|
||||||
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
||||||
MGPSurface Color[8];
|
MGPSurface Color[8];
|
||||||
@@ -361,7 +544,16 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPSurface ReadSurface;
|
MGPSurface ReadSurface;
|
||||||
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
|
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
|
||||||
Uint16 Width, Height, Layers, Samples;
|
Uint16 Width, Height, Layers, Samples;
|
||||||
Uint8 FixedSampleLocations, IsDefault, Complete, Pad0;
|
// Complete is FramebufferObject::CheckCompleteness(), the frontend-only answer - NOT
|
||||||
|
// glCheckFramebufferStatus's. CheckFramebufferStatus_State additionally consults
|
||||||
|
// ActiveBackendRejectsDistinctDepthStencil() and HasNonRenderableColorAttachment,
|
||||||
|
// which read the backend's probed format-capability cache; a client emitting that
|
||||||
|
// answer would be reading the backend from the client side, which is the exact
|
||||||
|
// coupling this boundary exists to remove. A later phase must not assume the stronger
|
||||||
|
// answer, and glCheckFramebufferStatus keeps answering from the frontend as it does
|
||||||
|
// today.
|
||||||
|
Uint8 FixedSampleLocations, IsDefault, Complete;
|
||||||
|
Uint8 Target; // MGPipeFramebufferTarget, above (P4a, D-C2; was Pad0)
|
||||||
Uint32 Pad1;
|
Uint32 Pad1;
|
||||||
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
|
// Two jobs (section 4.5.6): the server's render-pass memo key, and the CLIENT's
|
||||||
// emission suppressor - an unchanged hash means this record is not sent at all.
|
// emission suppressor - an unchanged hash means this record is not sent at all.
|
||||||
@@ -378,6 +570,20 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// static_assert, because this header may not include a frontend one.
|
// static_assert, because this header may not include a frontend one.
|
||||||
inline constexpr Uint32 kMGPipeMaxVertexAttribs = 32;
|
inline constexpr Uint32 kMGPipeMaxVertexAttribs = 32;
|
||||||
|
|
||||||
|
// P4a, D-G2. MobileGL's texture-unit space is ONE MERGED array of
|
||||||
|
// TextureState::MAX_TEXTURE_IMAGE_UNITS = 192 - there is no stage dimension on
|
||||||
|
// set_sampler_views / bind_sampler_states / set_shader_images, because the same unit may
|
||||||
|
// be sampled from two stages and per-stage 32 is an advertised number rather than a
|
||||||
|
// storage shape. These two bound the three var-tail sets' Start + Count, and a record
|
||||||
|
// that names a window outside them is Fatal{ProtocolCorruption} - the var-tail window IS
|
||||||
|
// the bound and entries outside it are not cleared.
|
||||||
|
//
|
||||||
|
// Pinned against the frontend constant in MG_Impl/Pipe/PipeFill.cpp, the one translation
|
||||||
|
// unit that sees both, exactly as kMGPipeMaxVertexAttribs is: this header may not include
|
||||||
|
// a frontend one.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxTextureUnits = 192;
|
||||||
|
inline constexpr Uint32 kMGPipeMaxImageUnits = 192;
|
||||||
|
|
||||||
struct MGPVertexBuffer {
|
struct MGPVertexBuffer {
|
||||||
MGPipeHandle Res;
|
MGPipeHandle Res;
|
||||||
Uint64 Offset;
|
Uint64 Offset;
|
||||||
@@ -675,10 +881,19 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// is ((void)0) - the applier is one function call away - and the transport wires the
|
// is ((void)0) - the applier is one function call away - and the transport wires the
|
||||||
// doorbell to this predicate when it lands.
|
// doorbell to this predicate when it lands.
|
||||||
//
|
//
|
||||||
// Immutable is exactly the right discriminator: it is set iff the store came from a
|
// P4a, D-A2: THE PREDICATE IS NARROWED TO NAME THE BUFFER TARGET, and that is a
|
||||||
// glBufferStorage* entry point, which is the definition of the allowed case.
|
// requirement rather than a tidy-up. glTexStorage* also sets Immutable - it is a real
|
||||||
|
// descriptor fact the backend reads, and the client must set it - but texture allocation
|
||||||
|
// is already deferred to sync time in monolith (glTexImage*/glTexStorage* only
|
||||||
|
// MarkStorageDirty; even glRenderbufferStorage* allocates lazily inside SyncToBackend), so
|
||||||
|
// splitting changes no observable behaviour and this batch must NOT ack. glBufferStorage
|
||||||
|
// stays the only entry point allowed a synchronous acknowledgement.
|
||||||
|
//
|
||||||
|
// PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage drives glTexStorage2D and
|
||||||
|
// glRenderbufferStorage idioms through it, and is the negative control for a future
|
||||||
|
// widening.
|
||||||
inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) {
|
inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) {
|
||||||
return desc.Immutable != 0;
|
return desc.Immutable != 0 && desc.Target == kMGPipeResourceTargetBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
|
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
|
||||||
|
|||||||
@@ -626,6 +626,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// the backend's own bring-up and teardown, not by a state reset.
|
// the backend's own bring-up and teardown, not by a state reset.
|
||||||
g_applier.RefusedResourceCalls = 0;
|
g_applier.RefusedResourceCalls = 0;
|
||||||
g_applier.RefusedVertexInputCalls = 0;
|
g_applier.RefusedVertexInputCalls = 0;
|
||||||
|
g_applier.RefusedObjectCalls = 0;
|
||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
||||||
g_applier.VertexBuffers = {};
|
g_applier.VertexBuffers = {};
|
||||||
g_applier.VertexBufferStart = 0;
|
g_applier.VertexBufferStart = 0;
|
||||||
@@ -662,6 +663,31 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// the first compare after the switch is a mismatch, which is the safe direction.
|
// the first compare after the switch is a mismatch, which is the safe direction.
|
||||||
++g_applier.VertexBuffersSerial;
|
++g_applier.VertexBuffersSerial;
|
||||||
++g_applier.IndexBufferSerial;
|
++g_applier.IndexBufferSerial;
|
||||||
|
|
||||||
|
// ---- P4a's working state, cleared for the same reason and with the same serial rule
|
||||||
|
// (D-J4). The OBJECT records - texture and renderbuffer resources, sampler CSOs,
|
||||||
|
// sampler views, shader CSOs - are deliberately NOT here: a texture lives in a share
|
||||||
|
// group exactly as a buffer does, and its record is where the extent, the parameters
|
||||||
|
// and the pending-upload set the backend reads now live.
|
||||||
|
g_applier.DrawFramebuffer = MGPFramebufferState{};
|
||||||
|
g_applier.ReadFramebuffer = MGPFramebufferState{};
|
||||||
|
g_applier.BoundSamplerViews = {};
|
||||||
|
g_applier.SamplerViewStart = 0;
|
||||||
|
g_applier.SamplerViewCount = 0;
|
||||||
|
g_applier.BoundSamplerStates = {};
|
||||||
|
g_applier.SamplerStateStart = 0;
|
||||||
|
g_applier.SamplerStateCount = 0;
|
||||||
|
g_applier.BoundShaderImages = {};
|
||||||
|
g_applier.ShaderImageStart = 0;
|
||||||
|
g_applier.ShaderImageCount = 0;
|
||||||
|
g_applier.DrawProgram = kMGPipeNullHandle;
|
||||||
|
g_applier.DispatchProgram = kMGPipeNullHandle;
|
||||||
|
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
||||||
|
++g_applier.FramebufferSerial;
|
||||||
|
++g_applier.SamplerViewsSerial;
|
||||||
|
++g_applier.SamplerStatesSerial;
|
||||||
|
++g_applier.ShaderImagesSerial;
|
||||||
|
++g_applier.ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplierReleaseObjectRecords() {
|
void MGPipeApplierReleaseObjectRecords() {
|
||||||
@@ -675,6 +701,23 @@ namespace MobileGL::MG_Pipe {
|
|||||||
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
g_applier.BoundVertexElements = kMGPipeNullHandle;
|
||||||
++g_applier.VertexBuffersSerial;
|
++g_applier.VertexBuffersSerial;
|
||||||
++g_applier.IndexBufferSerial;
|
++g_applier.IndexBufferSerial;
|
||||||
|
// P4a's five object tables go with them, and the working handles they could name go
|
||||||
|
// too - a bound shader CSO whose record has just been dropped must not survive as a
|
||||||
|
// handle the next call resolves against.
|
||||||
|
g_applier.TextureResources.clear();
|
||||||
|
g_applier.RenderbufferResources.clear();
|
||||||
|
g_applier.SamplerCsos.clear();
|
||||||
|
g_applier.SamplerViewCsos.clear();
|
||||||
|
g_applier.ShaderCsos.clear();
|
||||||
|
g_applier.CompositeShaderCsos.clear();
|
||||||
|
g_applier.DrawProgram = kMGPipeNullHandle;
|
||||||
|
g_applier.DispatchProgram = kMGPipeNullHandle;
|
||||||
|
g_applier.BoundShaderCso = kMGPipeNullHandle;
|
||||||
|
++g_applier.FramebufferSerial;
|
||||||
|
++g_applier.SamplerViewsSerial;
|
||||||
|
++g_applier.SamplerStatesSerial;
|
||||||
|
++g_applier.ShaderImagesSerial;
|
||||||
|
++g_applier.ProgramBindingSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
|
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
|
||||||
@@ -1371,4 +1414,84 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void MGPipeDeriveRenderStateFieldsForChunks(PipeInputs& inputs, Uint32 globalChunkBits) {
|
void MGPipeDeriveRenderStateFieldsForChunks(PipeInputs& inputs, Uint32 globalChunkBits) {
|
||||||
MGPipeApplyAccess::DeriveRenderStateFields(inputs, globalChunkBits);
|
MGPipeApplyAccess::DeriveRenderStateFields(inputs, globalChunkBits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// P4a: the fifteen object and working-state entry points - STUBS (contract commit)
|
||||||
|
// ================================================================================
|
||||||
|
//
|
||||||
|
// Every body below is deliberately empty at the contract commit, exactly as P3a's nine
|
||||||
|
// resource entry points were at theirs. What this commit fixes is the SIGNATURE and the
|
||||||
|
// storage it will write into: packages B and C compile and link against these, package D
|
||||||
|
// and E read the records above, and the gates see the whole shape - so nothing after this
|
||||||
|
// commit has to change a declaration, and no two packages ever edit one file.
|
||||||
|
//
|
||||||
|
// The bodies land in w1 (framebuffer + texture resources), w2 (samplers, views and the
|
||||||
|
// three unit sets) and w3 (programs and the default uniform block), on this same branch
|
||||||
|
// and before any client package runs against them: a stub applier under a real client is
|
||||||
|
// how P3a's first Espryt round produced 202 red cases that had to be argued rather than
|
||||||
|
// measured, and the order exists to make that structurally impossible.
|
||||||
|
//
|
||||||
|
// (void) casts rather than unnamed parameters, so the parameter NAMES stay in the
|
||||||
|
// definition and the bodies that replace these start from the vocabulary the header uses.
|
||||||
|
|
||||||
|
void MGPipeApplySetFramebufferState(const MGPFramebufferState& state) { (void)state; }
|
||||||
|
|
||||||
|
void MGPipeApplyCreateSamplerState(const MGPSamplerDesc& desc, const SamplerParameters* parameters) {
|
||||||
|
(void)desc;
|
||||||
|
(void)parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplyCreateSamplerView(const MGPSamplerView& view) { (void)view; }
|
||||||
|
|
||||||
|
void MGPipeApplyDeleteSamplerView(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplySetTextureParams(const MGPTextureParams& params) { (void)params; }
|
||||||
|
|
||||||
|
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
|
||||||
|
(void)hdr;
|
||||||
|
(void)tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
|
||||||
|
(void)hdr;
|
||||||
|
(void)tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplySetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail) {
|
||||||
|
(void)hdr;
|
||||||
|
(void)tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
||||||
|
const MG_State::GLState::LinkArtifacts* link,
|
||||||
|
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||||
|
(void)desc;
|
||||||
|
(void)link;
|
||||||
|
(void)spirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle) { (void)handle; }
|
||||||
|
|
||||||
|
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes) {
|
||||||
|
(void)record;
|
||||||
|
(void)bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE MONOLITH BODY IS A NO-OP AND THAT IS THE WHOLE OF IT: the emulation this names still
|
||||||
|
// runs, exactly as it does today, on the same code path. What the call site buys is that
|
||||||
|
// the set of emulations a split server cannot serve is NAMED, GREPPABLE and PINNED, so P5
|
||||||
|
// and P8 give it teeth by editing one function instead of rediscovering five call sites.
|
||||||
|
//
|
||||||
|
// It takes a literal and does nothing with it. Not a log line, not a counter: it sits on
|
||||||
|
// paths a frame can reach many times, and ROADMAP.md forbids committing hot-path
|
||||||
|
// instrumentation.
|
||||||
|
void MGPipeUnmigratedEmulation(const char* name) { (void)name; }
|
||||||
} // namespace MobileGL::MG_Pipe
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
|||||||
@@ -28,6 +28,17 @@
|
|||||||
// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend.
|
// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend.
|
||||||
//
|
//
|
||||||
// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol.
|
// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol.
|
||||||
|
// P4a: create_shader_state carries the reflection ARCHIVE, and in monolith the archive does
|
||||||
|
// not travel - the two structs ride beside the record through the entry point's companion
|
||||||
|
// pointers, exactly as P3a's `const void* initialBytes` does (D-H3, the one Blob rule). So
|
||||||
|
// this header needs their NAMES and never their definitions; the forward declaration is the
|
||||||
|
// whole coupling and the closure gate is what keeps it one. The verify build is the only
|
||||||
|
// place the codec runs, and it runs from PipeApply.cpp.
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
struct LinkArtifacts;
|
||||||
|
struct SpirvArtifacts;
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|
||||||
namespace MobileGL::MG_Pipe {
|
namespace MobileGL::MG_Pipe {
|
||||||
struct PipeInputs;
|
struct PipeInputs;
|
||||||
|
|
||||||
@@ -109,8 +120,33 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// own dense high-water mark and no further, so the bound costs nothing until a record is
|
// own dense high-water mark and no further, so the bound costs nothing until a record is
|
||||||
// already corrupt. Package C bounds handle.Slot the same way before
|
// already corrupt. Package C bounds handle.Slot the same way before
|
||||||
// BackendSlotTable::EntryAt, which resizes on a client-supplied index too.
|
// BackendSlotTable::EntryAt, which resizes on a client-supplied index too.
|
||||||
|
// P4a: THE BOUND IS PER KIND, not per table, and that is what keeps one number honest
|
||||||
|
// while the number of tables grows. The slot spaces of kinds Buffer, Texture and
|
||||||
|
// Renderbuffer are INDEPENDENT (MGPipeSlotAllocator allocates per kind), so three
|
||||||
|
// different objects can hold slot 7; the applier therefore keeps one Vector per resource
|
||||||
|
// KIND and indexes it by slot, rather than one Vector indexed by slot alone. Each is
|
||||||
|
// bounded by kMGPipeMaxResourceSlots and each grows only to its own dense high-water mark.
|
||||||
inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20;
|
inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20;
|
||||||
inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16;
|
inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16;
|
||||||
|
// P4a's three, and the argument is written out for each because the records differ in
|
||||||
|
// size. None is ever allocated by being named: the tables grow to the client's own dense
|
||||||
|
// high-water mark and no further, so the bound costs nothing until a record is corrupt.
|
||||||
|
//
|
||||||
|
// A sampler CSO record is a 100-byte value plus a handle, and sampler CSOs are
|
||||||
|
// CONTENT-ADDRESSED at capacity 256 on the client, so the live population is bounded by
|
||||||
|
// that cache and not by the application. 1<<16 is far above anything a GL program can hold
|
||||||
|
// and small enough that a corrupt slot is refused rather than allocated.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxSamplerCsoSlots = 1u << 16;
|
||||||
|
// A sampler VIEW is identity-addressed one per ITextureObject (P4a D-F2), so its
|
||||||
|
// population tracks the texture population exactly and it takes the texture bound.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxSamplerViewSlots = 1u << 20;
|
||||||
|
// The shader-CSO bound is the SLOT LIMIT ITSELF, because the composite band lives inside
|
||||||
|
// that space (MGPipeHandles.h): a bound below it would refuse the very slots
|
||||||
|
// AllocateComposite is allowed to hand out.
|
||||||
|
inline constexpr Uint32 kMGPipeMaxShaderCsoSlots = kMGPipeShaderCsoSlotLimit;
|
||||||
|
static_assert(kMGPipeMaxShaderCsoSlots > kMGPipeShaderCsoCompositeSlotBase,
|
||||||
|
"the ShaderCso bound must contain the composite band, or a composite handle "
|
||||||
|
"is refused as out of range on arrival");
|
||||||
|
|
||||||
// One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the
|
// One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the
|
||||||
// reserved null handle and is never live.
|
// reserved null handle and is never live.
|
||||||
@@ -129,6 +165,102 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// persistent-mapped host writes can set it with zero new record kinds; a verify build
|
// persistent-mapped host writes can set it with zero new record kinds; a verify build
|
||||||
// pins that it is false, so that phase cannot land a silent semantic change under it.
|
// pins that it is false, so that phase cannot land a silent semantic change under it.
|
||||||
Bool HasLiveHostWrites = false;
|
Bool HasLiveHostWrites = false;
|
||||||
|
|
||||||
|
// ---- P4a. Only a record of kind Texture ever carries these; a buffer's stay at
|
||||||
|
// their defaults, which is what keeps ONE record type for the discriminated
|
||||||
|
// descriptor rather than a second one that would have to be kept in step with it.
|
||||||
|
|
||||||
|
// set_texture_params, per texture OBJECT and independent of any binding - which is
|
||||||
|
// the whole point of addressing it by resource: a texture that is only an FBO
|
||||||
|
// attachment, only an image-unit binding or only a glCopyImageSubData endpoint has no
|
||||||
|
// sampler view to hang its parameters on, and today the READ-attachment case reaches
|
||||||
|
// no parameter push at all. ParamsSerial replaces the twin's
|
||||||
|
// m_syncedTextureParamsVersion + m_forceTextureParamsResync pair.
|
||||||
|
MGPTextureParams Params{};
|
||||||
|
Uint64 ParamsSerial = 0;
|
||||||
|
// The SamplerViewCso minted for this texture (P4a D-F2: one per ITextureObject,
|
||||||
|
// re-issued on the same handle whenever the restrictions move).
|
||||||
|
MGPipeHandle ViewCso = kMGPipeNullHandle;
|
||||||
|
// THE PENDING-UPLOAD SET, and it is server-side state on purpose (D-D5). The client
|
||||||
|
// clears its own dirty flags at EMISSION, for the levels whose record the applier
|
||||||
|
// accepted; Espryt's upload loop has bail arms - an incomplete texture returns early,
|
||||||
|
// a multisample target refreshes and skips - that today leave the frontend flag set,
|
||||||
|
// so a naive move of the clear to the client would lose those texels. The applier
|
||||||
|
// accumulates the emitted shape here instead, it survives any number of bails, and
|
||||||
|
// Espryt consumes and clears an entry only where it actually uploads.
|
||||||
|
//
|
||||||
|
// The verify lane's RETAIN MODE is what gates the shape: a consume-and-clear set
|
||||||
|
// cannot be recomputed after emission, so the tracker retains the pre-clear set and
|
||||||
|
// the comparator compares the emitted (UnionBox, RegionCount, Regions[]) against it
|
||||||
|
// field by field.
|
||||||
|
struct PendingUpload {
|
||||||
|
Uint16 UploadTarget = 0;
|
||||||
|
Uint16 Level = 0;
|
||||||
|
MGPBox UnionBox{};
|
||||||
|
Vector<MGPSubRegion> Regions;
|
||||||
|
};
|
||||||
|
Vector<PendingUpload> PendingUploads;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P4a: the three new object-record kinds (D-J1)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// All three follow MGPipeResourceRecord's shape exactly - Gen, Live, a payload and a
|
||||||
|
// server-owned monotone Serial - because the body-level idioms are the same ones:
|
||||||
|
// a create starts the record OVER rather than editing it (a recycled slot's record must
|
||||||
|
// not contribute one field, and Serial stays 0 because a create is not a mutation, so a
|
||||||
|
// fresh backend twin starting at 0 agrees without either side publishing anything); the
|
||||||
|
// serial moves BEFORE the backend is told; a destroy drops the record whole and keeps the
|
||||||
|
// generation, and the CLIENT frees the slot afterwards.
|
||||||
|
|
||||||
|
// create_sampler_state / delete_sampler_state. The parameters cross byte for byte
|
||||||
|
// INCLUDING borderColorForm - all three border representations are always numerically
|
||||||
|
// populated, so the value alone cannot say which driver entry point to use - and
|
||||||
|
// MOBILEGL_PIPE_VERIFY compares them FIELD BY FIELD (PipeFields.def's
|
||||||
|
// MGP_FIELDS_SamplerParameters), because the struct has three bytes of trailing padding
|
||||||
|
// and a byte comparison of it is a coin flip rather than a gate.
|
||||||
|
struct MGPipeSamplerCsoRecord {
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Bool Live = false;
|
||||||
|
SamplerParameters Params{};
|
||||||
|
Uint64 Serial = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// create_sampler_view / delete_sampler_view: ONLY the view restrictions. Everything a
|
||||||
|
// glTexParameter writes lives on set_texture_params instead. Re-issuing on the same
|
||||||
|
// handle is how a restriction change travels (Gen moves only on slot reuse); it bumps
|
||||||
|
// Serial and does not rebind anything.
|
||||||
|
struct MGPipeSamplerViewRecord {
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Bool Live = false;
|
||||||
|
MGPSamplerView View{};
|
||||||
|
Uint64 Serial = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// create/bind/delete_shader_state, plus set_global_constants' per-program half.
|
||||||
|
//
|
||||||
|
// THE ARTEFACTS ARE NOT HELD HERE IN MONOLITH: MGPProgramDesc's seven MGPBlobRefs are all
|
||||||
|
// declared with Size 0 ("this record does not declare its blob") and the LinkArtifacts /
|
||||||
|
// SpirvArtifacts ride beside the record through the entry point's companion pointers, so
|
||||||
|
// the applier stores the DESCRIPTOR and the identity and the server reads the frontend's
|
||||||
|
// own archive. That is what keeps the codec off the monolith hot path entirely; the verify
|
||||||
|
// build is where it is exercised, by serialising, deserialising and field-comparing before
|
||||||
|
// storing.
|
||||||
|
//
|
||||||
|
// GlobalConstants is the one allocation P4a adds per program, it is bounded by
|
||||||
|
// Desc.GlobalUboSize, and it is NOT on the hot path: set_global_constants is
|
||||||
|
// (ShaderCso, Version) keyed and fires at most once per program per frame.
|
||||||
|
struct MGPipeShaderCsoRecord {
|
||||||
|
Uint32 Gen = 0;
|
||||||
|
Bool Live = false;
|
||||||
|
MGPProgramDesc Desc{};
|
||||||
|
// GetUBOContentVersion() as last received. ~0u is the backends' "never uploaded"
|
||||||
|
// sentinel and the client must never emit it, so it is also what this starts at.
|
||||||
|
Uint32 GlobalConstantsVersion = ~Uint32{0};
|
||||||
|
Vector<Uint8> GlobalConstants;
|
||||||
|
Uint64 GlobalConstantsSerial = 0;
|
||||||
|
Uint64 Serial = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus
|
// The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus
|
||||||
@@ -217,6 +349,30 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Vector<MGPipeResourceRecord> Resources;
|
Vector<MGPipeResourceRecord> Resources;
|
||||||
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
|
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
|
||||||
|
|
||||||
|
// ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate
|
||||||
|
// Vectors rather than more rows of `Resources` above because the slot space is PER
|
||||||
|
// KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a
|
||||||
|
// single slot-indexed table would alias three different objects onto one record. The
|
||||||
|
// record TYPE is shared - one discriminated descriptor for buffers, every texture
|
||||||
|
// target and renderbuffers - and the bound is shared; only the table is per kind.
|
||||||
|
//
|
||||||
|
// Like the two above they are share-group state: MGPipeApplierReset does not touch
|
||||||
|
// them, and only the object's own death signal and MGPipeApplierReleaseObjectRecords
|
||||||
|
// clear them.
|
||||||
|
Vector<MGPipeResourceRecord> TextureResources;
|
||||||
|
Vector<MGPipeResourceRecord> RenderbufferResources;
|
||||||
|
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
|
||||||
|
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
|
||||||
|
Vector<MGPipeShaderCsoRecord> ShaderCsos;
|
||||||
|
// The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the
|
||||||
|
// same reason MGPipeSlotAllocator keeps the band in a table of its own: the band
|
||||||
|
// starts at 983040, so one program-pipeline composite in the slot-indexed vector above
|
||||||
|
// would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT
|
||||||
|
// IS A COMPOSITE - the split is an indexing detail on this side of the wire, the
|
||||||
|
// handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it
|
||||||
|
// exactly as they name any other program.
|
||||||
|
Vector<MGPipeShaderCsoRecord> CompositeShaderCsos;
|
||||||
|
|
||||||
// Every call this applier REFUSED because it named a record this applier does not
|
// Every call this applier REFUSED because it named a record this applier does not
|
||||||
// have: an unknown slot, a slot that is not live, or a generation that has moved on
|
// have: an unknown slot, a slot that is not live, or a generation that has moved on
|
||||||
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
|
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
|
||||||
@@ -227,6 +383,19 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// build. Per context, like the four render-state wire counters above.
|
// build. Per context, like the four render-state wire counters above.
|
||||||
Uint64 RefusedResourceCalls = 0;
|
Uint64 RefusedResourceCalls = 0;
|
||||||
Uint64 RefusedVertexInputCalls = 0;
|
Uint64 RefusedVertexInputCalls = 0;
|
||||||
|
// P4a's, in the same shape and for the same reason: every framebuffer, sampler,
|
||||||
|
// sampler-view, program and texture-params call this applier refused because it named
|
||||||
|
// a record this applier does not have. One counter rather than five, because the five
|
||||||
|
// families share one legal refusal sequence (teardown ->
|
||||||
|
// MGPipeApplierReleaseObjectRecords -> ~Object -> death notices naming records already
|
||||||
|
// dropped) and an operator reading a log wants to know that ANY object call was
|
||||||
|
// dropped; the log line names the call and the handle.
|
||||||
|
//
|
||||||
|
// THE OTHER CLASS IS NOT COUNTED HERE AND MUST NOT BE: a var-tail window outside its
|
||||||
|
// bound, or a set_texture_params whose BuiltinSampler is the null handle, would make
|
||||||
|
// the backend act outside its own storage or sample an object that does not exist -
|
||||||
|
// that is Fatal{ProtocolCorruption}, not a dropped call.
|
||||||
|
Uint64 RefusedObjectCalls = 0;
|
||||||
|
|
||||||
// ---- working state: what the next draw fetches with. All of it is per context and
|
// ---- working state: what the next draw fetches with. All of it is per context and
|
||||||
// all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever
|
// all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever
|
||||||
@@ -271,6 +440,50 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the
|
// operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the
|
||||||
// applier-side observable a unit case reads without a stats window.
|
// applier-side observable a unit case reads without a stats window.
|
||||||
Uint64 MapPersistentRoundtrips = 0;
|
Uint64 MapPersistentRoundtrips = 0;
|
||||||
|
|
||||||
|
// ---- P4a's WORKING state. All of it is per context and all of it is cleared by
|
||||||
|
// MGPipeApplierReset, EXCEPT the serials, which only ever advance - a counter that
|
||||||
|
// restarts walks back through values already stamped into a twin that outlived the
|
||||||
|
// switch, and P4a deletes the identity patches that used to close that hole.
|
||||||
|
|
||||||
|
// set_framebuffer_state, per bound target (D-C2). Target = Both writes both. The
|
||||||
|
// record is fully resolved: ReadSurface comes from the READ framebuffer's own read
|
||||||
|
// buffer, so the shared-FBO case cannot lose it, and DrawBuffers[] is applied only for
|
||||||
|
// a record whose Target is not Read.
|
||||||
|
MGPFramebufferState DrawFramebuffer{};
|
||||||
|
MGPFramebufferState ReadFramebuffer{};
|
||||||
|
Uint64 FramebufferSerial = 0;
|
||||||
|
|
||||||
|
// The three kVarTail unit sets, as received. NO STAGE DIMENSION: MobileGL's
|
||||||
|
// texture-unit space is one merged array of 192, the same unit may be sampled from two
|
||||||
|
// stages, and stage is derived server-side from the reflection archive only where the
|
||||||
|
// target API needs it.
|
||||||
|
//
|
||||||
|
// THE VAR-TAIL WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED - the
|
||||||
|
// record is "the last set as received", exactly as set_vertex_buffers is, and
|
||||||
|
// Start + Count above the bound is Fatal{ProtocolCorruption}.
|
||||||
|
Array<MGPBoundView, kMGPipeMaxTextureUnits> BoundSamplerViews{};
|
||||||
|
Uint32 SamplerViewStart = 0;
|
||||||
|
Uint32 SamplerViewCount = 0;
|
||||||
|
Uint64 SamplerViewsSerial = 0;
|
||||||
|
|
||||||
|
Array<MGPipeHandle, kMGPipeMaxTextureUnits> BoundSamplerStates{};
|
||||||
|
Uint32 SamplerStateStart = 0;
|
||||||
|
Uint32 SamplerStateCount = 0;
|
||||||
|
Uint64 SamplerStatesSerial = 0;
|
||||||
|
|
||||||
|
Array<MGPImageView, kMGPipeMaxImageUnits> BoundShaderImages{};
|
||||||
|
Uint32 ShaderImageStart = 0;
|
||||||
|
Uint32 ShaderImageCount = 0;
|
||||||
|
Uint64 ShaderImagesSerial = 0;
|
||||||
|
|
||||||
|
// set_draw_program / set_dispatch_program are two calls because the frontend has two
|
||||||
|
// joins and two PipeInputs slots; bind_shader_state is the third, and a null handle is
|
||||||
|
// legal in all three and means "nothing bound".
|
||||||
|
MGPipeHandle DrawProgram = kMGPipeNullHandle;
|
||||||
|
MGPipeHandle DispatchProgram = kMGPipeNullHandle;
|
||||||
|
MGPipeHandle BoundShaderCso = kMGPipeNullHandle;
|
||||||
|
Uint64 ProgramBindingSerial = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The monolith's single applier. Under split there is one per served context.
|
// The monolith's single applier. Under split there is one per served context.
|
||||||
@@ -288,6 +501,20 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// vertex-input serials rather than zeroing them. It does NOT drop the resource or
|
// vertex-input serials rather than zeroing them. It does NOT drop the resource or
|
||||||
// vertex-elements records: those describe share-group objects that the switch does not
|
// vertex-elements records: those describe share-group objects that the switch does not
|
||||||
// destroy, and dropping them is a dropped write on the far side of it.
|
// destroy, and dropping them is a dropped write on the far side of it.
|
||||||
|
//
|
||||||
|
// P4a EXTENDS BOTH HALVES AND THE RULE IS UNCHANGED (D-J4). Cleared: the two framebuffer
|
||||||
|
// records, the three unit sets, DrawProgram / DispatchProgram / BoundShaderCso - all of it
|
||||||
|
// per-context working state - with their serials ADVANCED and never zeroed. Not cleared:
|
||||||
|
// texture and renderbuffer resources, sampler CSOs, sampler views, shader CSOs, and the
|
||||||
|
// texture params and pending uploads that ride on a resource record, because a texture
|
||||||
|
// lives in a share group exactly as a buffer does.
|
||||||
|
//
|
||||||
|
// AND THEREFORE NO P4a TRACKER NEEDS A RE-PUBLICATION PATH ON FreshlyPrimed, AND NONE MAY
|
||||||
|
// HAVE ONE: re-emitting create_sampler_state for a record the applier still holds would
|
||||||
|
// move its Serial for nothing. What DOES reset on a fresh context is each emitter's
|
||||||
|
// BOUND-HANDLE latch - the framebuffer and unit-set hashes through
|
||||||
|
// MGPipeSetHashSuppressor::InvalidateAll, and the program emitter's BoundShaderCso mirror -
|
||||||
|
// because those mirror working state this function just cleared.
|
||||||
void MGPipeApplierReset();
|
void MGPipeApplierReset();
|
||||||
|
|
||||||
// THE OTHER SCOPE: the served context is going away and its applier with it, so the object
|
// THE OTHER SCOPE: the served context is going away and its applier with it, so the object
|
||||||
@@ -403,6 +630,105 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// configuration. Bumps IndexBufferSerial.
|
// configuration. Bumps IndexBufferSerial.
|
||||||
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record);
|
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P4a: the fifteen object and working-state entry points (D-A1, D-B1, D-J1)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// NOT ONE OF THEM DISPATCHES TO A BACKEND FUNCTION POINTER, and that is the single most
|
||||||
|
// important structural decision in P4a rather than an omission. Nothing in these families
|
||||||
|
// reaches the backend at GL-call time today - texture storage only marks a level dirty and
|
||||||
|
// Espryt allocates lazily at sync, texture params run from SyncTextureObjectToBackend at
|
||||||
|
// draw sync, renderbuffer storage is allocated inside SyncToBackend on a four-field cache,
|
||||||
|
// a sampler twin is created lazily from the program pass, and the framebuffer, unit sets
|
||||||
|
// and program are all resolved at PrepareForDraw. So every call below is either an OBJECT
|
||||||
|
// RECORD the applier stores or WORKING STATE the applier stores, and Espryt reads the
|
||||||
|
// applier at the sync points it already has, keyed on a server-owned Serial instead of a
|
||||||
|
// frontend version. MGPipeResourceOps is therefore UNCHANGED - nine members, same
|
||||||
|
// signatures - and P4a adds no backend op table and no op-table member at all.
|
||||||
|
//
|
||||||
|
// The consequence for the four resource entry points above: they BRANCH on
|
||||||
|
// record.Desc.Target. A buffer target dispatches into MGPipeResourceOps exactly as P3a
|
||||||
|
// wrote it; every other target stores and returns. The branch is one comparison against
|
||||||
|
// kMGPipeResourceTargetBuffer and it is where a mis-typed descriptor becomes visible.
|
||||||
|
//
|
||||||
|
// AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB, exactly as P3a's nine were: the
|
||||||
|
// signatures are what the client, the backend and the gates compile against and the
|
||||||
|
// records above are what they write into; the bodies land in the three commits that
|
||||||
|
// follow this one on the same branch.
|
||||||
|
|
||||||
|
// set_framebuffer_state. Fully resolved - nothing in the record requires a lookup on the
|
||||||
|
// far side. `state.Target` says which binding it describes (Draw / Read / Both) and the
|
||||||
|
// applier keeps the two records apart; Both writes both. ContentHash covers every field
|
||||||
|
// including Fbo and DrawBuffers[8], which is what makes a suppressed record provably mean
|
||||||
|
// "the draw-buffer array did not move" and therefore "the fragColor broadcast count did
|
||||||
|
// not move".
|
||||||
|
void MGPipeApplySetFramebufferState(const MGPFramebufferState& state);
|
||||||
|
|
||||||
|
// create_sampler_state. `parameters` is the client's canonical SamplerParameters copy,
|
||||||
|
// beside the record for the one Blob rule's reason; the applier stores it by value.
|
||||||
|
void MGPipeApplyCreateSamplerState(const MGPSamplerDesc& desc, const SamplerParameters* parameters);
|
||||||
|
// delete_sampler_state: emitted by the CSO cache's LRU eviction and by the frontend
|
||||||
|
// sampler object's death helper. Clears Live and drops the record; the client frees the
|
||||||
|
// slot afterwards.
|
||||||
|
void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle);
|
||||||
|
|
||||||
|
// create_sampler_view. Re-issued on the SAME handle whenever the view restrictions move,
|
||||||
|
// which is legal because Gen increments only on slot reuse and never on a respecify.
|
||||||
|
void MGPipeApplyCreateSamplerView(const MGPSamplerView& view);
|
||||||
|
void MGPipeApplyDeleteSamplerView(const MGPHandleOnly& handle);
|
||||||
|
|
||||||
|
// set_texture_params: addressed by RESOURCE and independent of any binding, which is what
|
||||||
|
// lets a texture that is only an attachment, only an image-unit binding or only a
|
||||||
|
// glCopyImageSubData endpoint carry its parameters at all. params.BuiltinSampler may never
|
||||||
|
// be the null handle - every ITextureObject owns a sampler object - so a null is
|
||||||
|
// Fatal{ProtocolCorruption} rather than "no sampler".
|
||||||
|
void MGPipeApplySetTextureParams(const MGPTextureParams& params);
|
||||||
|
|
||||||
|
// set_sampler_views / bind_sampler_states / set_shader_images: `tail` is hdr.Count entries
|
||||||
|
// starting at hdr.Start, and hdr.Start + hdr.Count above the unit bound is
|
||||||
|
// Fatal{ProtocolCorruption}. Entries outside the declared window are NOT cleared.
|
||||||
|
void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail);
|
||||||
|
void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail);
|
||||||
|
void MGPipeApplySetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail);
|
||||||
|
|
||||||
|
// create_shader_state. THE ARTEFACTS TRAVEL BESIDE THE RECORD, by pointer: all seven of
|
||||||
|
// desc.Spirv[] and desc.Reflection are declared with Size 0 ("this record does not declare
|
||||||
|
// its blob"), which is what a monolith emission is, and the codec is NOT called - zero
|
||||||
|
// serialisation cost on the monolith path. A verify build serialises, deserialises and
|
||||||
|
// field-compares before storing, and a mismatch is Fatal{PipeVerifyDiffer, "program-archive"}.
|
||||||
|
// Splitting this record for a transport whose ring caps one record at half its capacity is
|
||||||
|
// P5's problem, not this entry point's.
|
||||||
|
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
||||||
|
const MG_State::GLState::LinkArtifacts* link,
|
||||||
|
const MG_State::GLState::SpirvArtifacts* spirv);
|
||||||
|
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle);
|
||||||
|
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle);
|
||||||
|
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle);
|
||||||
|
void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle);
|
||||||
|
|
||||||
|
// set_global_constants: the DEFAULT UNIFORM BLOCK only. Keyed (ShaderCso, Version) and
|
||||||
|
// emitted at most once per program per frame; `bytes` is MapUBO()'s image, GetUBOSize()
|
||||||
|
// long, handed over as a companion pointer with Blob.Size 0. record.Version is
|
||||||
|
// GetUBOContentVersion() and may never be ~0u, which is the backends' "never uploaded"
|
||||||
|
// sentinel.
|
||||||
|
void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P4a: the named, greppable unmigrated emulations (D-M)
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// ROADMAP.md's P4a row ends "emulation 在 split 下显式 Fatal 直到 P8". In monolith the
|
||||||
|
// code paths keep running exactly as today - the Fatal is a SPLIT-only arm - so this costs
|
||||||
|
// P4a a named call site per unmigrated emulation and nothing else. P5/P8 give it teeth: a
|
||||||
|
// split server that reaches one of these has no client address space to read and must
|
||||||
|
// abort loudly rather than degrade silently.
|
||||||
|
//
|
||||||
|
// Monolith body: (void)name;. The list of names is pinned by
|
||||||
|
// PipeCatalogueTest.EveryUnmigratedEmulationIsNamedOnce and the call count is grepped by
|
||||||
|
// the purity gate, so a site that quietly disappears is a red gate rather than a surprise
|
||||||
|
// at P8.
|
||||||
|
void MGPipeUnmigratedEmulation(const char* name);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
// The derivation step (ARCHITECTURE.md 5.3, P2 brief D5)
|
// The derivation step (ARCHITECTURE.md 5.3, P2 brief D5)
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -78,9 +78,12 @@
|
|||||||
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
||||||
F(Samples) F(FixedSampleLocations)
|
F(Samples) F(FixedSampleLocations)
|
||||||
|
|
||||||
|
// P4a, D-E1: BuiltinSampler and SamplerResync. Pad0 stays unlisted - gen_pipe.py's
|
||||||
|
// PADDING_MEMBER_RE (^Pad\d*$) excludes it, and a member that stops being called Pad<n> MUST
|
||||||
|
// gain a row here or pipe-gates goes red.
|
||||||
#define MGP_FIELDS_MGPTextureParams(F) \
|
#define MGP_FIELDS_MGPTextureParams(F) \
|
||||||
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) F(MinLod) F(MaxLod) \
|
F(Res) F(BuiltinSampler) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) \
|
||||||
F(LodBias)
|
F(SamplerResync) F(MinLod) F(MaxLod) F(LodBias)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPProgramDesc(F) \
|
#define MGP_FIELDS_MGPProgramDesc(F) \
|
||||||
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
||||||
@@ -89,9 +92,12 @@
|
|||||||
#define MGP_FIELDS_MGPSurface(F) \
|
#define MGP_FIELDS_MGPSurface(F) \
|
||||||
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
|
F(Res) F(InternalFormat) F(Kind) F(Layered) F(Level) F(Layer) F(UploadTarget)
|
||||||
|
|
||||||
|
// P4a, D-C2: Pad0 became Uint8 Target, and gen_pipe.py's PADDING_MEMBER_RE only excludes a
|
||||||
|
// member still NAMED Pad<n> - so the rename without this row is a pipe-gates failure, which
|
||||||
|
// is exactly the trip wire that makes the byte impossible to add silently.
|
||||||
#define MGP_FIELDS_MGPFramebufferState(F) \
|
#define MGP_FIELDS_MGPFramebufferState(F) \
|
||||||
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
|
F(Fbo) F(Color) F(Depth) F(Stencil) F(ReadSurface) F(DrawBuffers) F(Width) F(Height) F(Layers) \
|
||||||
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(ContentHash)
|
F(Samples) F(FixedSampleLocations) F(IsDefault) F(Complete) F(Target) F(ContentHash)
|
||||||
|
|
||||||
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
#define MGP_FIELDS_MGPVertexBuffer(F) \
|
||||||
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
||||||
@@ -254,6 +260,18 @@
|
|||||||
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
||||||
F(Alignment)
|
F(Alignment)
|
||||||
|
|
||||||
|
// P4a, D-F1: THE PADDING TRAP. SamplerParameters is sizeof == 100 with THREE BYTES OF
|
||||||
|
// TRAILING PADDING (96 bytes of members plus the 1-byte borderColorForm) and had no field
|
||||||
|
// table and no MGP_VERIFY_PAYLOAD_LIST row at all, so MGPSamplerDesc's blob was compared as
|
||||||
|
// BYTES and MOBILEGL_PIPE_VERIFY could false-differ on uninitialised padding - a coin flip
|
||||||
|
// rather than a gate. With this list the comparator sees the sixteen members and the three
|
||||||
|
// bytes can never enter the answer. The client-side CSO cache hashes and memcmp-confirms over
|
||||||
|
// a ZERO-INITIALISED canonical copy for the same reason, which is the other half of D-F1.
|
||||||
|
#define MGP_FIELDS_SamplerParameters(F) \
|
||||||
|
F(wrapS) F(wrapT) F(wrapR) F(minFilter) F(magFilter) F(mipmapMode) F(minLod) F(maxLod) \
|
||||||
|
F(lodBias) F(maxAnisotropy) F(compareFunc) F(compareMode) F(borderColor) F(borderColorI) \
|
||||||
|
F(borderColorUI) F(borderColorForm)
|
||||||
|
|
||||||
#define MGP_FIELDS_PerBufferBlendState(F) \
|
#define MGP_FIELDS_PerBufferBlendState(F) \
|
||||||
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
||||||
F(AlphaEquation)
|
F(AlphaEquation)
|
||||||
@@ -326,7 +344,8 @@
|
|||||||
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
||||||
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
||||||
P(MGPSurfaceInfo) \
|
P(MGPSurfaceInfo) \
|
||||||
P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \
|
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||||
|
P(StencilFaceState) \
|
||||||
P(DynamicBackendParameters) P(MGHostSpan) \
|
P(DynamicBackendParameters) P(MGHostSpan) \
|
||||||
P(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
|
P(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,66 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// is asked rather than assumed.
|
// is asked rather than assumed.
|
||||||
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId);
|
Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
|
||||||
|
// ---- P4a: ONE CLIENT-SIDE DEATH HELPER PER KIND P4a MINTS (brief D-I1) ----
|
||||||
|
//
|
||||||
|
// BACKEND-NEUTRAL FROM DAY ONE, and this is the P3a final-review lesson taken forward
|
||||||
|
// rather than repeated. Before it, the only thing that ever returned a VertexElementsCso
|
||||||
|
// slot was DirectGLES' StateObjectDeathOps table; under a backend that installs none -
|
||||||
|
// DirectVulkan/Magma, which keeps its own age-reclaimed identity table on purpose - every
|
||||||
|
// VAO ever created held its slot and its applier record for the life of the process, and
|
||||||
|
// past 65536 slots every create became a permanent Fatal{ProtocolCorruption}. P4a mints
|
||||||
|
// SIX kinds, so the rule is stated once and obeyed six times: whatever mints a handle owns
|
||||||
|
// the death of that handle, the client mints all six, and a backend death notice is a
|
||||||
|
// redundant SECOND path that must be idempotent - which it is, because it resolves through
|
||||||
|
// the same lifetimeId -> slot map these free, and MGPipeSlotAllocator::Free refuses a slot
|
||||||
|
// that is not live at that generation.
|
||||||
|
//
|
||||||
|
// THE ORDER INSIDE EACH IS FIXED AND IS NOT A PACKAGE'S CHOICE:
|
||||||
|
// 1. emit the wire delete FIRST - it drops the applier's record while the record still
|
||||||
|
// exists, so a recycled slot cannot inherit a field;
|
||||||
|
// 2. raise NotifyStateObjectDestroyed SECOND - it resolves the handle through the
|
||||||
|
// allocator, and a backend told after the Free could no longer find its twin, which
|
||||||
|
// moves the leak from the client to the driver object;
|
||||||
|
// 3. free the slot LAST, and a double free on a stale generation is a proven no-op
|
||||||
|
// because Free bumps no generation (the bump rides the next handout).
|
||||||
|
//
|
||||||
|
// ALL SIX TAKE THE LIFETIME ID rather than the object, for MGPipeEmitVertexElementsDestroy
|
||||||
|
// AndFree's reason: they run from a destructor, where the last SharedPtr has already
|
||||||
|
// dropped, and the lifetime id is what the slot allocator resolves the handle from. It is
|
||||||
|
// also what keeps this header a declaration-only coupling - no frontend class needs
|
||||||
|
// forward-declaring for any of them.
|
||||||
|
//
|
||||||
|
// Each returns whether its wire delete actually went out, which is the LATCH taken at the
|
||||||
|
// object's create and not a second reading of the subsystem predicate: an object born
|
||||||
|
// while a subsystem bit was clear and destroyed after it was set would otherwise free its
|
||||||
|
// slot with the applier's record still Live, on a slot about to be handed out again. The
|
||||||
|
// legacy path runs only when the answer is false.
|
||||||
|
|
||||||
|
// ResourceDestroy, and then the SamplerViewCso minted off this same lifetime id (P4a
|
||||||
|
// D-F2: one sampler view per ITextureObject). Called from TextureObjectBase's VIRTUAL
|
||||||
|
// destructor, so 2D / 3D / cube / buffer / view all announce exactly once.
|
||||||
|
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// ResourceDestroy.
|
||||||
|
Bool MGPipeEmitRenderbufferDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// NO WIRE CALL AT ALL (D-I2). PipeCalls.def has 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 - and the catalogue is closed, so P4a does not invent a
|
||||||
|
// row. The handle is minted and freed entirely client-side and this helper does steps 2
|
||||||
|
// and 3 only. A recycled framebuffer handle is distinguished by Gen, which is inside the
|
||||||
|
// record's ContentHash, so it can never be suppressed against its predecessor's record.
|
||||||
|
Bool MGPipeEmitFramebufferDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteSamplerState. Also the path the content-addressed CSO cache's LRU eviction takes,
|
||||||
|
// which is why it is addressed by lifetime id and not by "the object that owns it".
|
||||||
|
Bool MGPipeEmitSamplerCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteSamplerView. Called by the texture helper above; a sampler view has no frontend
|
||||||
|
// object of its own, so this is the only path there is.
|
||||||
|
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
// DeleteShaderState, for an ordinary program AND for a program-pipeline COMPOSITE, whose
|
||||||
|
// slot has two independent release paths - the pipeline cache's LRU eviction and the
|
||||||
|
// composite ProgramObject's own destructor. One helper for both, and the second call is a
|
||||||
|
// proven no-op.
|
||||||
|
Bool MGPipeEmitShaderCsoDestroyAndFree(Uint64 lifetimeId);
|
||||||
|
|
||||||
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
||||||
void MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
void MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
||||||
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
||||||
|
|||||||
@@ -308,8 +308,13 @@ enum class MGPipeFieldEmitter : Uint8 {
|
|||||||
BindRenderState,
|
BindRenderState,
|
||||||
BindVertexElements,
|
BindVertexElements,
|
||||||
CreateRenderState,
|
CreateRenderState,
|
||||||
|
SetDispatchProgram,
|
||||||
|
SetDrawProgram,
|
||||||
SetDynamicState,
|
SetDynamicState,
|
||||||
|
SetFramebufferState,
|
||||||
SetPatchState,
|
SetPatchState,
|
||||||
|
SetSamplerViews,
|
||||||
|
SetShaderImages,
|
||||||
SetVertexAttribDefaults,
|
SetVertexAttribDefaults,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,8 +323,13 @@ inline constexpr const char* kMGPipeFieldEmitterNames[] = {
|
|||||||
"BindRenderState",
|
"BindRenderState",
|
||||||
"BindVertexElements",
|
"BindVertexElements",
|
||||||
"CreateRenderState",
|
"CreateRenderState",
|
||||||
|
"SetDispatchProgram",
|
||||||
|
"SetDrawProgram",
|
||||||
"SetDynamicState",
|
"SetDynamicState",
|
||||||
|
"SetFramebufferState",
|
||||||
"SetPatchState",
|
"SetPatchState",
|
||||||
|
"SetSamplerViews",
|
||||||
|
"SetShaderImages",
|
||||||
"SetVertexAttribDefaults",
|
"SetVertexAttribDefaults",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -344,11 +354,11 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
|||||||
MGPipeFieldEmitter::CreateRenderState, // GetDepthFunc
|
MGPipeFieldEmitter::CreateRenderState, // GetDepthFunc
|
||||||
MGPipeFieldEmitter::CreateRenderState, // GetDepthMask
|
MGPipeFieldEmitter::CreateRenderState, // GetDepthMask
|
||||||
MGPipeFieldEmitter::SetDynamicState, // GetDepthRangeIndexed
|
MGPipeFieldEmitter::SetDynamicState, // GetDepthRangeIndexed
|
||||||
MGPipeFieldEmitter::kNone, // GetFramebufferBindingSlot
|
MGPipeFieldEmitter::SetFramebufferState, // GetFramebufferBindingSlot
|
||||||
MGPipeFieldEmitter::kNone, // GetImageTextureBinding
|
MGPipeFieldEmitter::SetShaderImages, // GetImageTextureBinding
|
||||||
MGPipeFieldEmitter::SetDynamicState, // GetLineWidth
|
MGPipeFieldEmitter::SetDynamicState, // GetLineWidth
|
||||||
MGPipeFieldEmitter::CreateRenderState, // GetLogicOp
|
MGPipeFieldEmitter::CreateRenderState, // GetLogicOp
|
||||||
MGPipeFieldEmitter::kNone, // GetMaxTouchedTextureUnit
|
MGPipeFieldEmitter::SetSamplerViews, // GetMaxTouchedTextureUnit
|
||||||
MGPipeFieldEmitter::CreateRenderState, // GetMinSampleShadingValue
|
MGPipeFieldEmitter::CreateRenderState, // GetMinSampleShadingValue
|
||||||
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultInnerLevel
|
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultInnerLevel
|
||||||
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultOuterLevel
|
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultOuterLevel
|
||||||
@@ -359,8 +369,8 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
|||||||
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetFactor
|
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetFactor
|
||||||
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetUnits
|
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetUnits
|
||||||
MGPipeFieldEmitter::SetDynamicState, // GetPrimitiveRestartIndex
|
MGPipeFieldEmitter::SetDynamicState, // GetPrimitiveRestartIndex
|
||||||
MGPipeFieldEmitter::kNone, // GetProgramForDispatch
|
MGPipeFieldEmitter::SetDispatchProgram, // GetProgramForDispatch
|
||||||
MGPipeFieldEmitter::kNone, // GetProgramForDraw
|
MGPipeFieldEmitter::SetDrawProgram, // GetProgramForDraw
|
||||||
MGPipeFieldEmitter::kNone, // GetProgramObject
|
MGPipeFieldEmitter::kNone, // GetProgramObject
|
||||||
MGPipeFieldEmitter::CreateRenderState, // GetProvokingVertexMode
|
MGPipeFieldEmitter::CreateRenderState, // GetProvokingVertexMode
|
||||||
MGPipeFieldEmitter::CreateRenderState, // GetRenderStateParameters
|
MGPipeFieldEmitter::CreateRenderState, // GetRenderStateParameters
|
||||||
@@ -371,7 +381,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
|||||||
MGPipeFieldEmitter::kNone, // GetTextureBindGeneration
|
MGPipeFieldEmitter::kNone, // GetTextureBindGeneration
|
||||||
MGPipeFieldEmitter::kNone, // GetTextureContextId
|
MGPipeFieldEmitter::kNone, // GetTextureContextId
|
||||||
MGPipeFieldEmitter::kNone, // GetTextureObject
|
MGPipeFieldEmitter::kNone, // GetTextureObject
|
||||||
MGPipeFieldEmitter::kNone, // GetTextureUnitObject
|
MGPipeFieldEmitter::SetSamplerViews, // GetTextureUnitObject
|
||||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackCapturedVertices
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackCapturedVertices
|
||||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackGeneration
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackGeneration
|
||||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackPausedPrimitiveCounter
|
MGPipeFieldEmitter::kNone, // GetTransformFeedbackPausedPrimitiveCounter
|
||||||
@@ -388,7 +398,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
|||||||
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
|
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
|
||||||
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
|
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
|
||||||
};
|
};
|
||||||
inline constexpr SizeT kMGPipeEmittedFieldCount = 34;
|
inline constexpr SizeT kMGPipeEmittedFieldCount = 40;
|
||||||
|
|
||||||
struct MGPipeFilledState {
|
struct MGPipeFilledState {
|
||||||
Uint64 CurrentVerbSerial;
|
Uint64 CurrentVerbSerial;
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ inline Bool MGPipeVerify(const MGPSwapInterval& a, const MGPSwapInterval& b, con
|
|||||||
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPSurfaceInfo& a, const MGPSurfaceInfo& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
|
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
|
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
|
||||||
@@ -247,6 +248,8 @@ struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
|||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<SamplerParameters> : std::true_type {};
|
||||||
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
||||||
@@ -629,6 +632,11 @@ inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParamete
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField) {
|
||||||
|
MGP_FIELDS_SamplerParameters(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
|
inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState& b, const char** outField) {
|
||||||
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
||||||
return true;
|
return true;
|
||||||
@@ -661,4 +669,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
|
|||||||
|
|
||||||
#undef MGP_VERIFY_FIELD
|
#undef MGP_VERIFY_FIELD
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 71;
|
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 72;
|
||||||
|
|||||||
@@ -408,6 +408,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Free constrained templates rather than members so the struct bodies above stay a verbatim
|
// Free constrained templates rather than members so the struct bodies above stay a verbatim
|
||||||
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
|
// move. The sizeof trip wires below are what keep these tables honest: a member added to a
|
||||||
// struct changes its size, trips the assertion, and the message sends the author here.
|
// struct changes its size, trips the assertion, and the message sends the author here.
|
||||||
|
//
|
||||||
|
// THE SERIALIZER NOW EXISTS (P4a): MG_State/GLState/ProgramState/ProgramArtifactsCodec.
|
||||||
|
// {h,cpp}, beside this header rather than inside it so the check_include_closure.py
|
||||||
|
// "artifacts-header" probe stays untouched. It is two visitors over the tables below - a
|
||||||
|
// writer that appends to a Vector<Uint8> and a reader that consumes one - length-prefixed,
|
||||||
|
// little-endian, with a format-version word first and a MGL_LINKARTIFACTS_SIZE echo
|
||||||
|
// second, so a struct that gained a field and a codec that did not is a mismatch at READ
|
||||||
|
// time rather than a silent truncation. Adding a member to any struct above therefore
|
||||||
|
// means: add its VisitFields row here, update the sizeof number below, and bump
|
||||||
|
// kProgramArtifactsCodecVersion. `LinkArtifacts::program` stays the one deliberate
|
||||||
|
// omission, and the codec has no arm for it.
|
||||||
template <class Self, class V>
|
template <class Self, class V>
|
||||||
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
||||||
void VisitFields(Self& a, V&& v) {
|
void VisitFields(Self& a, V&& v) {
|
||||||
@@ -549,7 +560,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ---- trip wires ----
|
// ---- trip wires ----
|
||||||
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
||||||
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
|
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44,
|
||||||
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and its serializer when one exists), then update this number");
|
"TypeFacts changed: add the field to VisitFields(TypeFacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
// The container-bearing structs have one size per standard library (std::string and
|
// The container-bearing structs have one size per standard library (std::string and
|
||||||
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
|
// std::set differ between libstdc++ and libc++), so their numbers are pinned PER STL:
|
||||||
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
|
// libstdc++ (the Linux CI toolchain) here, libc++ (the NDK) by the integrator, MSVC
|
||||||
@@ -565,12 +576,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef MGL_LINKARTIFACTS_SIZE
|
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||||
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
|
static_assert(sizeof(ResourceReflection) == MGL_RESOURCEREFLECTION_SIZE,
|
||||||
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and its serializer when one exists), then update this number");
|
"ResourceReflection changed size: add the field to VisitFields(ResourceReflection) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
|
static_assert(sizeof(XfbVarying) == MGL_XFBVARYING_SIZE,
|
||||||
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and its serializer when one exists), then update this number");
|
"XfbVarying changed size: add the field to VisitFields(XfbVarying) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
|
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
|
||||||
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and its serializer when one exists), then update this number");
|
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
|
static_assert(sizeof(SpirvArtifacts) == MGL_SPIRVARTIFACTS_SIZE,
|
||||||
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and its serializer when one exists), then update this number");
|
"SpirvArtifacts changed size: add the field to VisitFields(SpirvArtifacts) (and ProgramArtifactsCodec.cpp's serializer), then update this number");
|
||||||
#endif
|
#endif
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// ProgramArtifactsCodec.h. Compiled only under MOBILEGL_PIPE_PUSH (the root CMakeLists.txt
|
||||||
|
// appends it inside `if (MOBILEGL_PIPE_PUSH)`), so the pull build gains no symbol from it.
|
||||||
|
#include "ProgramArtifactsCodec.h"
|
||||||
|
|
||||||
|
#include <bit>
|
||||||
|
#include <cstring>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
namespace {
|
||||||
|
// Little-endian, stated rather than assumed. Every ABI MobileGL ships on is
|
||||||
|
// little-endian; the day one is not, this is a compile error and not a silently
|
||||||
|
// byte-swapped reflection table.
|
||||||
|
static_assert(std::endian::native == std::endian::little,
|
||||||
|
"the program-archive codec writes scalars in native order and MobileGL's "
|
||||||
|
"ABIs are little-endian; a big-endian target needs explicit byte order");
|
||||||
|
|
||||||
|
// ---- the four container shapes the archive is built out of ----
|
||||||
|
//
|
||||||
|
// Detected by SHAPE rather than by naming std::vector / ska::flat_hash_map, because
|
||||||
|
// MobileGL's aliases are not all std:: types (UnorderedMap is ska::flat_hash_map) and
|
||||||
|
// a codec that named them would stop compiling the day one is swapped. The order the
|
||||||
|
// arms are tested in is what makes them unambiguous: String before every container,
|
||||||
|
// maps before sets (a map has both key_type and mapped_type), fixed arrays before
|
||||||
|
// resizable ones.
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveString = std::same_as<T, String>;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveMap = requires {
|
||||||
|
typename T::key_type;
|
||||||
|
typename T::mapped_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveSet = requires { typename T::key_type; } && !ArchiveMap<T> && !ArchiveString<T>;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveFixedArray = requires { std::tuple_size<T>::value; };
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveVector = !ArchiveString<T> && !ArchiveFixedArray<T> && requires(T& t) {
|
||||||
|
t.resize(SizeT{0});
|
||||||
|
t.size();
|
||||||
|
t.begin();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveScalar = std::is_arithmetic_v<T> || std::is_enum_v<T>;
|
||||||
|
|
||||||
|
// The ONE hand-written arm, and it is hand-written because ProgramArtifacts.h gives it
|
||||||
|
// no VisitFields table: glslang::TIntermediate::TUniformInitializer is a plain
|
||||||
|
// aggregate that merely LOOKS like a glslang type (std::string + scalars + two
|
||||||
|
// std::vectors), which is exactly what ProgramTranslationCache.h audited it as when it
|
||||||
|
// decided the archive holds no glslang-owned memory. If a field is added there, this
|
||||||
|
// arm and the format version below both have to move.
|
||||||
|
using UniformInitializer = glslang::TIntermediate::TUniformInitializer;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
concept ArchiveUniformInitializer = std::same_as<T, UniformInitializer>;
|
||||||
|
|
||||||
|
// ---- the writer ----
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void PutRaw(Vector<Uint8>& out, const T& value) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>);
|
||||||
|
const SizeT at = out.size();
|
||||||
|
out.resize(at + sizeof(T));
|
||||||
|
std::memcpy(out.data() + at, &value, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PutCount(Vector<Uint8>& out, SizeT count) {
|
||||||
|
PutRaw(out, static_cast<Uint64>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteValue(Vector<Uint8>& out, const T& value);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteSequence(Vector<Uint8>& out, const T& value) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
for (const auto& element : value) WriteValue(out, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void WriteValue(Vector<Uint8>& out, const T& value) {
|
||||||
|
if constexpr (ArchiveScalar<T>) {
|
||||||
|
PutRaw(out, value);
|
||||||
|
} else if constexpr (ArchiveString<T>) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
const SizeT at = out.size();
|
||||||
|
out.resize(at + value.size());
|
||||||
|
if (!value.empty()) std::memcpy(out.data() + at, value.data(), value.size());
|
||||||
|
} else if constexpr (ArchiveMap<T>) {
|
||||||
|
PutCount(out, value.size());
|
||||||
|
for (const auto& entry : value) {
|
||||||
|
WriteValue(out, entry.first);
|
||||||
|
WriteValue(out, entry.second);
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveSet<T>) {
|
||||||
|
WriteSequence(out, value);
|
||||||
|
} else if constexpr (ArchiveFixedArray<T>) {
|
||||||
|
// No count: the width is part of the type, and writing one would let a reader
|
||||||
|
// believe a stream that disagrees with the struct.
|
||||||
|
for (const auto& element : value) WriteValue(out, element);
|
||||||
|
} else if constexpr (ArchiveVector<T>) {
|
||||||
|
WriteSequence(out, value);
|
||||||
|
} else if constexpr (ArchiveUniformInitializer<T>) {
|
||||||
|
WriteValue(out, value.name);
|
||||||
|
WriteValue(out, value.basicType);
|
||||||
|
WriteValue(out, value.vectorSize);
|
||||||
|
WriteValue(out, value.matrixCols);
|
||||||
|
WriteValue(out, value.matrixRows);
|
||||||
|
WriteValue(out, value.arraySize);
|
||||||
|
WriteValue(out, value.intValues);
|
||||||
|
WriteValue(out, value.floatValues);
|
||||||
|
} else {
|
||||||
|
// The archive's own structs: TypeFacts, ResourceReflection, XfbVarying. ONE
|
||||||
|
// table serves both directions, so a member added to any of them is carried by
|
||||||
|
// both halves of this codec the moment its VisitFields row is added - and a
|
||||||
|
// type with no table at all is a compile error here rather than a silently
|
||||||
|
// skipped field.
|
||||||
|
VisitFields(value, [&out](const char*, const auto& field) { WriteValue(out, field); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the reader ----
|
||||||
|
|
||||||
|
struct ReadCursor {
|
||||||
|
const Uint8* Bytes = nullptr;
|
||||||
|
SizeT Size = 0;
|
||||||
|
SizeT Pos = 0;
|
||||||
|
Bool Ok = true;
|
||||||
|
|
||||||
|
SizeT Remaining() const { return Size - Pos; }
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
Bool TakeRaw(ReadCursor& in, T& value) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>);
|
||||||
|
if (!in.Ok || in.Remaining() < sizeof(T)) {
|
||||||
|
in.Ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(&value, in.Bytes + in.Pos, sizeof(T));
|
||||||
|
in.Pos += sizeof(T);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A COUNT IS CHECKED AGAINST THE BYTES THAT REMAIN BEFORE ANYTHING IS ALLOCATED. Every
|
||||||
|
// element this codec writes costs at least one byte, so a count larger than the
|
||||||
|
// remaining bytes cannot describe this stream - and refusing it here is what stops a
|
||||||
|
// corrupt or truncated archive from turning into a multi-gigabyte resize before the
|
||||||
|
// element loop notices it has run out.
|
||||||
|
Bool TakeCount(ReadCursor& in, SizeT& count) {
|
||||||
|
Uint64 raw = 0;
|
||||||
|
if (!TakeRaw(in, raw)) return false;
|
||||||
|
if (raw > static_cast<Uint64>(in.Remaining())) {
|
||||||
|
in.Ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
count = static_cast<SizeT>(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ReadValue(ReadCursor& in, T& value);
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ReadValue(ReadCursor& in, T& value) {
|
||||||
|
if constexpr (ArchiveScalar<T>) {
|
||||||
|
TakeRaw(in, value);
|
||||||
|
} else if constexpr (ArchiveString<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.assign(reinterpret_cast<const char*>(in.Bytes + in.Pos), count);
|
||||||
|
in.Pos += count;
|
||||||
|
} else if constexpr (ArchiveMap<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
for (SizeT i = 0; i < count && in.Ok; ++i) {
|
||||||
|
typename T::key_type key{};
|
||||||
|
typename T::mapped_type mapped{};
|
||||||
|
ReadValue(in, key);
|
||||||
|
ReadValue(in, mapped);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
value.emplace(Move(key), Move(mapped));
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveSet<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
for (SizeT i = 0; i < count && in.Ok; ++i) {
|
||||||
|
typename T::key_type key{};
|
||||||
|
ReadValue(in, key);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
value.insert(Move(key));
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveFixedArray<T>) {
|
||||||
|
for (auto& element : value) {
|
||||||
|
ReadValue(in, element);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveVector<T>) {
|
||||||
|
SizeT count = 0;
|
||||||
|
if (!TakeCount(in, count)) return;
|
||||||
|
value.clear();
|
||||||
|
value.resize(count);
|
||||||
|
for (auto& element : value) {
|
||||||
|
ReadValue(in, element);
|
||||||
|
if (!in.Ok) return;
|
||||||
|
}
|
||||||
|
} else if constexpr (ArchiveUniformInitializer<T>) {
|
||||||
|
ReadValue(in, value.name);
|
||||||
|
ReadValue(in, value.basicType);
|
||||||
|
ReadValue(in, value.vectorSize);
|
||||||
|
ReadValue(in, value.matrixCols);
|
||||||
|
ReadValue(in, value.matrixRows);
|
||||||
|
ReadValue(in, value.arraySize);
|
||||||
|
ReadValue(in, value.intValues);
|
||||||
|
ReadValue(in, value.floatValues);
|
||||||
|
} else {
|
||||||
|
VisitFields(value, [&in](const char*, auto& field) {
|
||||||
|
if (in.Ok) ReadValue(in, field);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The struct-size echo. Under a toolchain whose sizes are not pinned yet
|
||||||
|
// (ProgramArtifacts.h's libc++ branch until the integrator fills it in) this is 0,
|
||||||
|
// which still round-trips within one build - the echo compares what THIS build wrote
|
||||||
|
// against what THIS build expects - and stops mattering the moment the pin lands.
|
||||||
|
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||||
|
inline constexpr Uint64 kLinkArtifactsSizeEcho = MGL_LINKARTIFACTS_SIZE;
|
||||||
|
#else
|
||||||
|
inline constexpr Uint64 kLinkArtifactsSizeEcho = 0;
|
||||||
|
#endif
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EncodeProgramArtifacts(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||||
|
Vector<Uint8>& out) {
|
||||||
|
PutRaw(out, kProgramArtifactsCodecVersion);
|
||||||
|
PutRaw(out, kLinkArtifactsSizeEcho);
|
||||||
|
// `link` is walked through its own VisitFields table, which omits the live
|
||||||
|
// SharedPtr<glslang::TProgram>: 57 of the 58 members. There is no arm here for it and
|
||||||
|
// there must not be one - it points into a glslang arena that no archived instance
|
||||||
|
// owns, and ProgramTranslationCache asserts it is null at insert.
|
||||||
|
WriteValue(out, link);
|
||||||
|
WriteValue(out, spirv);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
|
||||||
|
SpirvArtifacts& spirv) {
|
||||||
|
// Both outputs are left in a DEFINED state on every exit, including every failure:
|
||||||
|
// a caller that ignores the return value gets an empty archive rather than half of a
|
||||||
|
// truncated one.
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
if (bytes == nullptr) return false;
|
||||||
|
|
||||||
|
ReadCursor in{bytes, size, 0, true};
|
||||||
|
Uint32 version = 0;
|
||||||
|
Uint64 sizeEcho = 0;
|
||||||
|
if (!TakeRaw(in, version) || !TakeRaw(in, sizeEcho)) return false;
|
||||||
|
// REFUSED, NOT GUESSED. A different version word or a struct that changed width means
|
||||||
|
// the bytes describe a layout this build does not have; deserialising them anyway
|
||||||
|
// writes garbage into the tail of a reflection table, which is exactly the failure the
|
||||||
|
// two words exist to turn into a clean false.
|
||||||
|
if (version != kProgramArtifactsCodecVersion) return false;
|
||||||
|
if (sizeEcho != kLinkArtifactsSizeEcho) return false;
|
||||||
|
|
||||||
|
ReadValue(in, link);
|
||||||
|
ReadValue(in, spirv);
|
||||||
|
if (!in.Ok) {
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Trailing bytes are a mismatch too: the format accounts for every byte it writes, so
|
||||||
|
// anything left over means the reader and the writer disagree about the shape and the
|
||||||
|
// agreement so far was luck.
|
||||||
|
if (in.Pos != in.Size) {
|
||||||
|
link = LinkArtifacts{};
|
||||||
|
spirv = SpirvArtifacts{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Never written, never read, and stated here so it cannot be added by reflex.
|
||||||
|
link.program = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.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 "ProgramArtifacts.h"
|
||||||
|
|
||||||
|
// The reflection ARCHIVE's serializer (P4a, D-H2): create_shader_state's payload is per-stage
|
||||||
|
// SPIR-V plus LinkArtifacts + SpirvArtifacts, whole structs, and until now nothing could turn
|
||||||
|
// those into bytes. Every VisitFields comment in ProgramArtifacts.h said "(and its serializer
|
||||||
|
// when one exists)"; this is it.
|
||||||
|
//
|
||||||
|
// IT LIVES BESIDE THE HEADER RATHER THAN INSIDE IT, deliberately: ProgramArtifacts.h carries
|
||||||
|
// the check_include_closure.py "artifacts-header" probe, which pins that the header is
|
||||||
|
// glslang-free by symbol and reaches no ShaderObject, no SpvcSession, no Config and no
|
||||||
|
// MG_Backend. A codec inside it would have to be inspected against that probe on every edit;
|
||||||
|
// a codec beside it leaves the probe untouched, and this file is compiled only in a push
|
||||||
|
// build (the root CMakeLists.txt appends it inside `if (MOBILEGL_PIPE_PUSH)`).
|
||||||
|
//
|
||||||
|
// WHEN IT ACTUALLY RUNS, and the answer is "not on the monolith hot path at all". In monolith
|
||||||
|
// the archive does not travel: MGPProgramDesc's seven blob refs are declared with Size 0 -
|
||||||
|
// "this record does not declare its blob" - and MGPipeApplyCreateShaderState takes the two
|
||||||
|
// structs by pointer beside the record, so the applier reads the frontend's own archive and
|
||||||
|
// this codec is never called. The VERIFY build is where it is exercised, and it is exercised
|
||||||
|
// as LIVE CODE WITH A GATE rather than as dead code with a unit test: the applier serialises,
|
||||||
|
// deserialises and field-compares before storing, and a mismatch is
|
||||||
|
// Fatal{PipeVerifyDiffer, "program-archive"}. Under split, P5 is what makes it the transport's
|
||||||
|
// path.
|
||||||
|
//
|
||||||
|
// THE FORMAT, and every part of it is a refusal rather than a guess:
|
||||||
|
// * a VERSION word first, and a MGL_LINKARTIFACTS_SIZE echo second, so a struct that gained
|
||||||
|
// a field and a codec that did not is a MISMATCH AT READ TIME rather than a silent
|
||||||
|
// truncation that deserialises garbage into the tail of a reflection table;
|
||||||
|
// * length-prefixed everything - strings, vectors, maps, sets - with the count checked
|
||||||
|
// against the bytes that remain before a single element is allocated, so a corrupt count
|
||||||
|
// cannot turn into a four-billion-element resize;
|
||||||
|
// * little-endian, which is asserted rather than assumed;
|
||||||
|
// * and `LinkArtifacts::program` is NEVER visited. It is the live glslang TProgram, it is
|
||||||
|
// null for every archived instance by construction, and VisitFields deliberately omits it
|
||||||
|
// (57 of the 58 members). Decode leaves it null.
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
|
||||||
|
// Bumped whenever the byte format changes in a way a previous reader would misread. A
|
||||||
|
// reader that sees a different word REFUSES; it never tries to guess a layout.
|
||||||
|
inline constexpr Uint32 kProgramArtifactsCodecVersion = 1;
|
||||||
|
|
||||||
|
// Appends the archive to `out` (which is not cleared, so a caller may frame it). Never
|
||||||
|
// fails: everything it walks is owned plain data.
|
||||||
|
void EncodeProgramArtifacts(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||||
|
Vector<Uint8>& out);
|
||||||
|
|
||||||
|
// Replaces `link` and `spirv` with what `bytes` describes. Returns false - with both
|
||||||
|
// outputs left in a defined, default state - for a truncated stream, a version mismatch, a
|
||||||
|
// struct-size mismatch, or trailing bytes the format does not account for. `link.program`
|
||||||
|
// is always null on return.
|
||||||
|
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
|
||||||
|
SpirvArtifacts& spirv);
|
||||||
|
|
||||||
|
// How many fields a type's VisitFields table actually visits. The codec walks exactly that
|
||||||
|
// table, so this is what pins "the codec did not quietly grow an arm of its own" - most of
|
||||||
|
// all for LinkArtifacts, whose 58th member is the live TProgram the table omits. It is a
|
||||||
|
// runtime count rather than a static_assert because VisitFields needs an INSTANCE and
|
||||||
|
// these structs carry strings, vectors and maps: none of them is a constant expression.
|
||||||
|
// ProgramArtifactsCodecTest is where it is asserted.
|
||||||
|
template <class T>
|
||||||
|
inline SizeT ProgramArtifactsVisitedFieldCount() {
|
||||||
|
T probe{};
|
||||||
|
SizeT count = 0;
|
||||||
|
VisitFields(probe, [&count](const char*, auto&) { ++count; });
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -135,16 +135,25 @@ if (MSVC)
|
|||||||
target_compile_options(MagmaPipeIdentityTest PRIVATE /Zc:preprocessor)
|
target_compile_options(MagmaPipeIdentityTest PRIVATE /Zc:preprocessor)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# P3a's two suites. Their targets and this registration are the CONTRACT commit's, for the
|
# P3a's two suites AND P4a's six, together because they take exactly the same shape. Their
|
||||||
# same reason the four P2 suites' are: their CONTENTS belong to two later packages each, and
|
# targets and this registration are the CONTRACT commit's, for the same reason the four P2
|
||||||
# neither of them should have to come back to this file to add a case.
|
# suites' are: their CONTENTS belong to two later packages each, and neither of them should
|
||||||
|
# have to come back to this file to add a case.
|
||||||
|
#
|
||||||
|
# P4a's six are FramebufferEmitTest, TextureEmitTest, SamplerEmitTest, ImageEmitTest,
|
||||||
|
# ProgramEmitTest and CompositeResolverTest. Each lands from the contract commit with one case
|
||||||
|
# that pins the shape its later cases depend on - the emitter is one never-destroyed process
|
||||||
|
# singleton, or the composite band has exactly one door - so none of them is an empty file
|
||||||
|
# waiting for a package, and none of them can be registered wrongly without a red test.
|
||||||
#
|
#
|
||||||
# They link gtest rather than gtest_main and carry their own main(), like PipeInputsTest and
|
# They link gtest rather than gtest_main and carry their own main(), like PipeInputsTest and
|
||||||
# RenderStateSpansTest: the applier's bounds and protocol trip wires report through a log line
|
# RenderStateSpansTest: the applier's bounds and protocol trip wires report through a log line
|
||||||
# in a shipped push build and std::abort() in a poison or verify one, so a case that drives
|
# in a shipped push build and std::abort() in a poison or verify one, so a case that drives
|
||||||
# one reads the line back out of a file the process names before anything logs. Deciding that
|
# one reads the line back out of a file the process names before anything logs. Deciding that
|
||||||
# HERE is what keeps the later packages out of this file.
|
# HERE is what keeps the later packages out of this file.
|
||||||
foreach(pipeTest ResourceEmitTest VertexInputEmitTest)
|
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
||||||
|
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
|
||||||
|
ProgramEmitTest CompositeResolverTest)
|
||||||
add_executable(${pipeTest} ${pipeTest}.cpp)
|
add_executable(${pipeTest} ${pipeTest}.cpp)
|
||||||
|
|
||||||
target_include_directories(${pipeTest} PRIVATE
|
target_include_directories(${pipeTest} PRIVATE
|
||||||
@@ -168,7 +177,9 @@ endforeach()
|
|||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
foreach(pipeTest ResourceEmitTest VertexInputEmitTest)
|
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
||||||
|
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
|
||||||
|
ProgramEmitTest CompositeResolverTest)
|
||||||
gtest_discover_tests(${pipeTest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(${pipeTest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
endforeach()
|
endforeach()
|
||||||
gtest_discover_tests(MagmaPipeIdentityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(MagmaPipeIdentityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/CompositeResolverTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's program-pipeline COMPOSITE: GLContext::GetProgramForDraw() already flattens a pipeline
|
||||||
|
// into one hidden composite ProgramObject entirely in the frontend, so the client pushes ONE
|
||||||
|
// handle for it, allocated out of the ShaderCso reserved high band, and the server never
|
||||||
|
// learns it is a composite - it needs no "resolved draw program" hook at all.
|
||||||
|
//
|
||||||
|
// WHAT THIS SUITE IS ACTUALLY FOR: the composite's slot has TWO INDEPENDENT RELEASE PATHS -
|
||||||
|
// the pipeline cache's LRU eviction and the composite ProgramObject's own destructor - and
|
||||||
|
// both go through one client-side death helper. Either order has to free the slot exactly
|
||||||
|
// once, and the second call has to be a proven no-op rather than a lucky one. That is what the
|
||||||
|
// eviction-then-destruction pair and its mirror pin, and it is why the composite gets a leak
|
||||||
|
// case of its own beside the five ordinary kinds.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `CompositeResolver`, not `CompositeResolverTest`: the file is XTest.cpp and the
|
||||||
|
// suite is X, this directory's convention.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT - the
|
||||||
|
// resolver itself, its signature-keyed cache and the two release orders are the client
|
||||||
|
// package's, and it never has to come back to MG_Test/Pipe/CMakeLists.txt.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||||
|
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||||
|
// pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The contract commit's one case, and it pins the property everything else in this suite is
|
||||||
|
// built on: the composite band has EXACTLY ONE DOOR. The ordinary allocator refuses the band
|
||||||
|
// for kind ShaderCso, AllocateComposite is the only way in, and a slot from one can never be
|
||||||
|
// mistaken for a slot from the other - which is what reserving a band rather than setting a
|
||||||
|
// flag on the handle buys, and what keeps the resolver's lifetime bookkeeping out of the
|
||||||
|
// ordinary program allocator.
|
||||||
|
TEST(CompositeResolver, TheCompositeBandHasExactlyOneDoor) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
MGPipeSlotAllocator slots;
|
||||||
|
|
||||||
|
// The ordinary door never opens onto the band, however many times it is used.
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
const MGPipeHandle ordinary = slots.Allocate(MGPipeKind::ShaderCso);
|
||||||
|
EXPECT_FALSE(MGPipeHandleIsNull(ordinary));
|
||||||
|
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(ordinary.Slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The composite door only ever opens onto it, and the handle it hands out is an ORDINARY
|
||||||
|
// ShaderCso handle in every other respect - the same kind, the same {slot, gen} rules, the
|
||||||
|
// same Free. The server cannot tell the difference and must not be able to.
|
||||||
|
const MGPipeHandle composite = slots.AllocateComposite(4242);
|
||||||
|
EXPECT_FALSE(MGPipeHandleIsNull(composite));
|
||||||
|
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||||
|
EXPECT_TRUE(slots.IsLive(MGPipeKind::ShaderCso, composite));
|
||||||
|
EXPECT_EQ(slots.FindByLifetimeId(MGPipeKind::ShaderCso, 4242), composite);
|
||||||
|
|
||||||
|
// TWO RELEASE PATHS, ONE FREE. The second call resolves the same handle at a generation
|
||||||
|
// the slot no longer has, and Free refuses it - which is what makes "the pipeline cache
|
||||||
|
// evicted it and then the composite's destructor ran" safe in either order rather than a
|
||||||
|
// double free that only shows up as slot theft much later.
|
||||||
|
const Uint32 liveBefore = slots.LiveCount(MGPipeKind::ShaderCso);
|
||||||
|
slots.Free(MGPipeKind::ShaderCso, composite);
|
||||||
|
slots.Free(MGPipeKind::ShaderCso, composite);
|
||||||
|
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), liveBefore - 1);
|
||||||
|
EXPECT_FALSE(slots.IsLive(MGPipeKind::ShaderCso, composite));
|
||||||
|
|
||||||
|
// And the slot really goes back to the band rather than to the ordinary free list: the
|
||||||
|
// next composite reuses it with a bumped generation, and no ordinary program can be handed
|
||||||
|
// it.
|
||||||
|
const MGPipeHandle recycled = slots.AllocateComposite(4343);
|
||||||
|
EXPECT_EQ(recycled.Slot, composite.Slot);
|
||||||
|
EXPECT_NE(recycled.Gen, composite.Gen);
|
||||||
|
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(slots.Allocate(MGPipeKind::ShaderCso).Slot));
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client slot allocator in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-compositeresolver-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's framebuffer family: set_framebuffer_state, emitted per bound target, on both sides of
|
||||||
|
// the call.
|
||||||
|
//
|
||||||
|
// THIS SUITE IS A NAMED GATE. The phase's descriptor-consistency gate is "for every framebuffer
|
||||||
|
// configuration the emitted MGPFramebufferState reproduces exactly the values the backend's
|
||||||
|
// SyncToBackend family reads from the frontend today, field by field", and it is spelled
|
||||||
|
// `ctest -R 'FramebufferEmit\.'`; its negative control is a script that stops the conversion
|
||||||
|
// copying ONE field (MGPSurface::Layered) and expects this suite to go red NAMING that field.
|
||||||
|
// So a case here must fail by field name, never by a bare count, or the control cannot answer.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `FramebufferEmit`, not `FramebufferEmitTest`: the file is XTest.cpp and the
|
||||||
|
// suite is X, this directory's convention, and it is what the gates grep for.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT. The
|
||||||
|
// applier-side cases (a record's lifecycle, the per-target storage, what a make-current does
|
||||||
|
// and does not clear) are the wire commits'; the emitter-side cases (the resolved read
|
||||||
|
// surface, the draw-buffer array in the content hash, a recycled handle never suppressed
|
||||||
|
// against its predecessor, every attachment field surviving the surface conversion, an
|
||||||
|
// attachment point above the wire width refused rather than truncated, a re-storaged attached
|
||||||
|
// renderbuffer publishing its new extent) are the client package's - and neither of them has
|
||||||
|
// to come back to MG_Test/Pipe/CMakeLists.txt to add one.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for the same reason ResourceEmitTest and VertexInputEmitTest do: the
|
||||||
|
// applier's bounds and protocol trip wires report through a log line in a shipped push build
|
||||||
|
// and std::abort() in a poison or verify one, so a case that drives one reads the line back
|
||||||
|
// out of a file this process names before anything logs.
|
||||||
|
//
|
||||||
|
// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N`
|
||||||
|
// stays name-for-name identical between the pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The one case the contract commit lands, and it is not a placeholder: it pins the SHAPE every
|
||||||
|
// later case depends on. The emitter is a process singleton that is heap-constructed and
|
||||||
|
// intentionally leaked, because a static holding client state whose destructor an exit handler
|
||||||
|
// can run is the exit-order use-after-free this design closed once already - `exit` runs the
|
||||||
|
// frontend's own teardown into a pipe whose allocator has already been destroyed. One
|
||||||
|
// allocation for the life of the process, no destructor to lose.
|
||||||
|
TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(&MGPipeFramebufferEmitterInstance(), &MGPipeFramebufferEmitterInstance());
|
||||||
|
// And the family's wired-subsystem constant is either 0 or its own bit and nothing else.
|
||||||
|
// It is 0 until this family's emitter has a body; the OR in PipeFill.cpp is what turns it
|
||||||
|
// into the switch, so a header that set the wrong bit would switch the wrong family on.
|
||||||
|
EXPECT_TRUE(kMGPipeWiredFramebufferSubsystem == 0 ||
|
||||||
|
kMGPipeWiredFramebufferSubsystem == kMGPipeSubsystemFramebuffer);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
// Before anything logs: the logger reads this variable once, on its first write, and
|
||||||
|
// caches the handle. The name carries this process's pid, and the file is removed on the
|
||||||
|
// way out.
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-framebufferemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/ImageEmitTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's image-unit set: set_shader_images, the third of the three kVarTail unit sets. It rides
|
||||||
|
// the sampler family's subsystem bit - one family, one A/B - and has its own suite because its
|
||||||
|
// content hash has to cover two fields the other two sets do not carry.
|
||||||
|
//
|
||||||
|
// THE TWO CASES THIS SUITE EXISTS FOR: an ACCESS-mode change alone, and an INTERNAL-FORMAT
|
||||||
|
// change alone, each has to move the hash and emit the set. Both are live glBindImageTexture
|
||||||
|
// state, the format-less image bake keys on the format the shader was built against, and a
|
||||||
|
// hash over the bindings alone would suppress exactly the record that says the bake is stale.
|
||||||
|
// The behavioural gates beside them are the format-less bake and non-core-format scenarios,
|
||||||
|
// and the photon fixture on desktop retrace - the only fixture that has ever caught an
|
||||||
|
// image-binding-semantics regression, and one that must never be run on the Adreno.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `ImageEmit`, not `ImageEmitTest`: the file is XTest.cpp and the suite is X,
|
||||||
|
// this directory's convention, and it is what the gates grep for.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||||
|
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||||
|
// pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/ImageEmit.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||||
|
TEST(ImageEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(&MGPipeImageEmitterInstance(), &MGPipeImageEmitterInstance());
|
||||||
|
// The image set's window is bounded by the same merged unit space the other two sets use;
|
||||||
|
// there is no separate image-unit capacity and there must not be one, because a record
|
||||||
|
// whose window is checked against a different bound from the array it indexes is the shape
|
||||||
|
// the applier's Fatal{ProtocolCorruption} exists to make impossible.
|
||||||
|
EXPECT_EQ(kMGPipeMaxImageUnits, kMGPipeMaxTextureUnits);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-imageemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -13,10 +13,18 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
// P4a: MGPipeUnmigratedEmulation's declaration, and the applier's records the catalogue's size
|
||||||
|
// pins now reach. Push-only, like the translation unit that defines them - in a pull build the
|
||||||
|
// symbol does not exist and the one case that calls it is compiled out.
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
using namespace MobileGL::MG_Pipe;
|
using namespace MobileGL::MG_Pipe;
|
||||||
@@ -157,6 +165,76 @@ TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
|
|||||||
EXPECT_EQ(sizeof(MGPBindRenderState), 12u);
|
EXPECT_EQ(sizeof(MGPBindRenderState), 12u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P4a's two payload edits, which are the only two the phase makes, and both are the kind a
|
||||||
|
// compiler catches only where somebody asked it to. MGP_ASSERT_POD already pins both sizes in
|
||||||
|
// MGPipeTypes.h; what is pinned HERE is the SHAPE the two edits were made for, because that is
|
||||||
|
// what a later phase would silently undo.
|
||||||
|
TEST(PipeCatalogue, TextureParamsNameTheirBuiltinSamplerAndFramebufferStateNamesItsTarget) {
|
||||||
|
// 32 -> 40: the CSO handle carrying the SamplerParameters of the SamplerObject every
|
||||||
|
// ITextureObject owns, plus the second resync bit. Naming the CSO rather than widening
|
||||||
|
// this payload with a filter/wrap/border block is what keeps ONE authority for one value -
|
||||||
|
// duplicating SamplerParameters on the wire would give two.
|
||||||
|
EXPECT_EQ(sizeof(MGPTextureParams), 40u);
|
||||||
|
EXPECT_EQ(offsetof(MGPTextureParams, Res), 0u);
|
||||||
|
EXPECT_EQ(offsetof(MGPTextureParams, BuiltinSampler), 8u);
|
||||||
|
EXPECT_EQ(offsetof(MGPTextureParams, SamplerResync), 26u);
|
||||||
|
// The two resync bits are SEPARATE bytes and must stay so: ForceResync guards a swizzle
|
||||||
|
// override the frontend params version does not move for, SamplerResync guards an
|
||||||
|
// incomplete texture sampling (0,0,0,1) after a driver re-mint. Different failures,
|
||||||
|
// different owners, one byte each.
|
||||||
|
EXPECT_NE(offsetof(MGPTextureParams, ForceResync), offsetof(MGPTextureParams, SamplerResync));
|
||||||
|
|
||||||
|
// Pad0 -> Uint8 Target, and the SIZE DID NOT MOVE, which is the whole point: the record is
|
||||||
|
// emitted once per bound target that moved, or once with Both, and that costs a byte the
|
||||||
|
// struct already had.
|
||||||
|
EXPECT_EQ(sizeof(MGPFramebufferState), 304u);
|
||||||
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Draw), 0u);
|
||||||
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Read), 1u);
|
||||||
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Both), 2u);
|
||||||
|
// The wire's colour-attachment width is ONE width, and it is the wire's rather than the
|
||||||
|
// driver's: a driver reporting more attachments than this is refused at bring-up, never
|
||||||
|
// truncated into the record.
|
||||||
|
EXPECT_EQ(kMGPipeMaxColorAttachments, 8u);
|
||||||
|
EXPECT_EQ(std::extent_v<decltype(MGPFramebufferState::Color)>, kMGPipeMaxColorAttachments);
|
||||||
|
EXPECT_EQ(std::extent_v<decltype(MGPFramebufferState::DrawBuffers)>, kMGPipeMaxColorAttachments);
|
||||||
|
|
||||||
|
// And the two unit bounds, which bound all three var-tail sets. One merged unit space, no
|
||||||
|
// stage dimension.
|
||||||
|
EXPECT_EQ(kMGPipeMaxTextureUnits, 192u);
|
||||||
|
EXPECT_EQ(kMGPipeMaxImageUnits, 192u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// D-A3: the resource-target enum minted beside the field, and the property that makes it worth
|
||||||
|
// minting - EVERY TextureTarget has a row, checked at compile time by a table with no
|
||||||
|
// `default:` arm, so adding a target is a build break rather than a descriptor that silently
|
||||||
|
// describes the wrong kind of storage.
|
||||||
|
TEST(PipeCatalogue, EveryTextureTargetMapsToItsOwnResourceTarget) {
|
||||||
|
// The compile-time half is MGPipeEveryTextureTargetIsMapped's static_assert; this is the
|
||||||
|
// same walk at runtime, so the case names the offender instead of the build naming a line.
|
||||||
|
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
||||||
|
const auto target = static_cast<TextureTarget>(i);
|
||||||
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(target), kMGPipeResourceTargetUnmapped)
|
||||||
|
<< "TextureTarget " << i << " has no MGPResourceDesc::Target row";
|
||||||
|
EXPECT_LT(MGPipeResourceTargetForTextureTarget(target),
|
||||||
|
static_cast<Uint32>(MGPipeResourceTarget::Count));
|
||||||
|
}
|
||||||
|
// Buffer is 0 and stays 0: P3a's constant is what a zero-initialised record already says,
|
||||||
|
// and the narrowed ack predicate below compares against it.
|
||||||
|
EXPECT_EQ(static_cast<Uint32>(MGPipeResourceTarget::Buffer), 0u);
|
||||||
|
EXPECT_EQ(kMGPipeResourceTargetBuffer, 0u);
|
||||||
|
// No texture target may collide with the buffer target, or a texture descriptor would ask
|
||||||
|
// for a synchronous acknowledgement.
|
||||||
|
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
||||||
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)),
|
||||||
|
static_cast<Uint32>(kMGPipeResourceTargetBuffer));
|
||||||
|
}
|
||||||
|
// A rectangle texture is NOT a 2D texture on the wire. Espryt lowers both to GL_TEXTURE_2D
|
||||||
|
// at bind time and lowers Texture1D the same way, and Tex1D still has an enumerator of its
|
||||||
|
// own; folding rectangle onto Tex2D here would erase a distinction both backends switch on.
|
||||||
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture2D),
|
||||||
|
MGPipeResourceTargetForTextureTarget(TextureTarget::TextureRectangle));
|
||||||
|
}
|
||||||
|
|
||||||
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
|
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
|
||||||
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
|
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
|
||||||
@@ -352,9 +430,19 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
|||||||
// the struct that used to memcmp is compared member by member. P3a added the two vertex wire
|
// the struct that used to memcmp is compared member by member. P3a added the two vertex wire
|
||||||
// views as a seventh and eighth non-payload entry (63 + 8), for the same reason: they are the
|
// views as a seventh and eighth non-payload entry (63 + 8), for the same reason: they are the
|
||||||
// elements of create_vertex_elements' blob, and a memcmp over that blob would false-differ on
|
// elements of create_vertex_elements' blob, and a memcmp over that blob would false-differ on
|
||||||
// MGPVertexAttribWire::Pad0.
|
// MGPVertexAttribWire::Pad0. P4a adds SamplerParameters as a ninth (63 + 9 = 72), and the name
|
||||||
|
// of this case stays what it was, because a removed test name is a gate failure of its own.
|
||||||
|
//
|
||||||
|
// SamplerParameters IS THE SHARPEST OF THE NINE. It is 100 bytes with THREE BYTES OF TRAILING
|
||||||
|
// PADDING (96 bytes of members plus the one-byte borderColorForm), it rides
|
||||||
|
// MGPSamplerDesc::Parameters as a blob, and until P4a it had no field list and no verify-list
|
||||||
|
// row at all - so the comparator fell back to comparing the blob as BYTES and could
|
||||||
|
// false-differ on padding nobody writes. That is not a theoretical hazard for this struct:
|
||||||
|
// the client's CSO cache confirms a hash hit with a memcmp over the same bytes, so a codec or
|
||||||
|
// a cache that read the padding would mint a fresh CSO per call and the verify lane would
|
||||||
|
// abort at random.
|
||||||
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 71u);
|
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 72u);
|
||||||
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||||
@@ -363,6 +451,7 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
|||||||
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<MGPVertexAttribWire>::value);
|
static_assert(MGPipeHasFieldVerifier<MGPVertexAttribWire>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<MGPVertexBindingPointWire>::value);
|
static_assert(MGPipeHasFieldVerifier<MGPVertexBindingPointWire>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<SamplerParameters>::value);
|
||||||
PixelStoreParameters p{};
|
PixelStoreParameters p{};
|
||||||
PixelStoreParameters q{};
|
PixelStoreParameters q{};
|
||||||
const char* field = nullptr;
|
const char* field = nullptr;
|
||||||
@@ -377,6 +466,38 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
|||||||
t.Offset = 8;
|
t.Offset = 8;
|
||||||
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
||||||
EXPECT_STREQ(field, "Offset");
|
EXPECT_STREQ(field, "Offset");
|
||||||
|
|
||||||
|
// P4a's ninth, and its two halves. First: the comparator sees the members, INCLUDING
|
||||||
|
// borderColorForm - which is the field a backend picks glSamplerParameterIiv over fv by,
|
||||||
|
// and which no value comparison can infer because all three border representations are
|
||||||
|
// always numerically populated.
|
||||||
|
SamplerParameters left{};
|
||||||
|
SamplerParameters right{};
|
||||||
|
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
||||||
|
right.borderColorForm = BorderColorForm::Int;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
||||||
|
EXPECT_STREQ(field, "borderColorForm");
|
||||||
|
right = left;
|
||||||
|
right.borderColorI = IntVec4{1, 0, 0, 0};
|
||||||
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
||||||
|
EXPECT_STREQ(field, "borderColorI");
|
||||||
|
right = left;
|
||||||
|
right.maxAnisotropy = 4.0f;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
||||||
|
EXPECT_STREQ(field, "maxAnisotropy");
|
||||||
|
|
||||||
|
// Second, and this is the one a byte comparison gets wrong: the THREE TRAILING PADDING
|
||||||
|
// BYTES are not fields, so garbage in them cannot make two equal sampler states differ.
|
||||||
|
// Written through a byte pointer, because that is the only way to reach a byte the struct
|
||||||
|
// does not name.
|
||||||
|
static_assert(sizeof(SamplerParameters) == 100);
|
||||||
|
right = left;
|
||||||
|
auto* rightBytes = reinterpret_cast<unsigned char*>(&right);
|
||||||
|
for (SizeT i = sizeof(SamplerParameters) - 3; i < sizeof(SamplerParameters); ++i) {
|
||||||
|
rightBytes[i] = 0x5A;
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(MGPipeVerify(left, right, &field))
|
||||||
|
<< "the comparator read a padding byte: field=" << (field != nullptr ? field : "(none)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// G7 pins the member list the pipeline/dynamic split is derived from.
|
// G7 pins the member list the pipeline/dynamic split is derived from.
|
||||||
@@ -580,6 +701,81 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
|
|||||||
mutableStore.Width = 64u * 1024u;
|
mutableStore.Width = 64u * 1024u;
|
||||||
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore));
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore));
|
||||||
|
|
||||||
|
// P4a: THE TWO IDIOMS THAT MADE THE PREDICATE HAVE TO NARROW. Textures travel on the same
|
||||||
|
// resource_respecify row as buffers, and glTexStorage* sets Immutable for a real reason -
|
||||||
|
// it is a descriptor fact the backend reads - so an Immutable-only predicate would have
|
||||||
|
// started acknowledging every immutable texture allocation the moment P4a's texture family
|
||||||
|
// landed. Texture allocation is already deferred to sync time in monolith (glTexImage* and
|
||||||
|
// glTexStorage* only mark the storage dirty, and even glRenderbufferStorage* allocates
|
||||||
|
// lazily inside SyncToBackend), so splitting changes no observable behaviour and this batch
|
||||||
|
// must not ack. glBufferStorage stays the only entry point allowed a synchronous one.
|
||||||
|
//
|
||||||
|
// This is the negative control for a future widening, in both directions: a predicate that
|
||||||
|
// stopped naming the buffer target would turn these two green-and-wrong.
|
||||||
|
MGPResourceDesc immutableTexture{};
|
||||||
|
immutableTexture.Immutable = 1; // glTexStorage2D
|
||||||
|
immutableTexture.Target =
|
||||||
|
static_cast<Uint8>(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture2D));
|
||||||
|
immutableTexture.Width = 256;
|
||||||
|
immutableTexture.Height = 256;
|
||||||
|
immutableTexture.Levels = 9;
|
||||||
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(immutableTexture));
|
||||||
|
|
||||||
|
MGPResourceDesc renderbuffer{};
|
||||||
|
renderbuffer.Immutable = 1; // glRenderbufferStorage: one shot, and still lazy in the backend
|
||||||
|
renderbuffer.Target = static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer);
|
||||||
|
renderbuffer.Width = 1920;
|
||||||
|
renderbuffer.Height = 1080;
|
||||||
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(renderbuffer));
|
||||||
|
|
||||||
|
// And the buffer half still answers true with the target spelled explicitly rather than
|
||||||
|
// relying on a zero-initialised record to mean "buffer".
|
||||||
|
MGPResourceDesc immutableBuffer{};
|
||||||
|
immutableBuffer.Immutable = 1;
|
||||||
|
immutableBuffer.Target = kMGPipeResourceTargetBuffer;
|
||||||
|
EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutableBuffer));
|
||||||
|
|
||||||
// And the opcode did not move: a flag-word edit is not a catalogue edit.
|
// And the opcode did not move: a flag-word edit is not a catalogue edit.
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ResourceRespecify), 3);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ResourceRespecify), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// G13b, D-M: "emulation 在 split 下显式 Fatal 直到 P8" costs P4a a NAMED, GREPPABLE call site
|
||||||
|
// per unmigrated emulation and nothing else - in monolith MGPipeUnmigratedEmulation is a no-op
|
||||||
|
// and the emulation still runs on exactly the code path it runs on today. What this pins is
|
||||||
|
// the LIST, because the whole value of the mechanism is that P5 and P8 edit one function
|
||||||
|
// instead of rediscovering five call sites, and a site that quietly disappears has to be a red
|
||||||
|
// gate rather than a surprise three phases later.
|
||||||
|
//
|
||||||
|
// The names are pinned here rather than counted in the backend, because the count alone cannot
|
||||||
|
// say WHICH one was lost. The purity gate greps the count; this says what the count is of.
|
||||||
|
TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) {
|
||||||
|
// Every one of these is an emulation that reads or writes CLIENT memory a split server
|
||||||
|
// would not have: a CPU shadow mirror, a CPU mipmap fallback, a shadow-conversion readback,
|
||||||
|
// and the re-dirty of already-uploaded levels that a texture re-mint performs.
|
||||||
|
const char* const kNames[] = {
|
||||||
|
"copy-image-shadow-mirror", // the glCopyImageSubData CPU-shadow mirror
|
||||||
|
"generate-mipmap-storage", // EnsureGenerateMipmapStorageAllocated
|
||||||
|
"generate-mipmap-cpu-fallback", // GenerateThreeChannelFloatMipmapOnCpu
|
||||||
|
"get-tex-image-shadow", // GetTexImageViaShadowConversion
|
||||||
|
"texture-remint-pull", // RequireImageBindableStorage's re-dirty
|
||||||
|
};
|
||||||
|
EXPECT_EQ(std::size(kNames), 5u);
|
||||||
|
// No duplicates: two sites sharing a name would make the grepped count and this list
|
||||||
|
// disagree in the one direction nobody would notice.
|
||||||
|
for (SizeT i = 0; i < std::size(kNames); ++i) {
|
||||||
|
for (SizeT j = i + 1; j < std::size(kNames); ++j) {
|
||||||
|
EXPECT_STRNE(kNames[i], kNames[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The last one is the head of the only NEW stall class the design admits, and P4a supplies
|
||||||
|
// exactly one of its four mitigations - prevention, through ImageBindableHint on every
|
||||||
|
// create and respecify. The async pull, the bounded retention and the
|
||||||
|
// ResourceSubDataComplete terminator are a later phase's, and P4a must not build half a
|
||||||
|
// terminator.
|
||||||
|
EXPECT_STREQ(kNames[4], "texture-remint-pull");
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// In monolith it really is a no-op: calling it changes nothing and returns nothing. The
|
||||||
|
// teeth are a split server's, and the call site is what P8 gives them to.
|
||||||
|
for (const char* name : kNames) MGPipeUnmigratedEmulation(name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's program family: create/bind/delete_shader_state, set_draw_program,
|
||||||
|
// set_dispatch_program and set_global_constants.
|
||||||
|
//
|
||||||
|
// THE ONE PIN THAT IS EASIEST TO LOSE AND WORST TO LOSE: set_global_constants' Version is
|
||||||
|
// GetUBOContentVersion(), and ~0u is the BACKENDS' "never uploaded" sentinel - the wrap skips
|
||||||
|
// it - so the client must never emit it. A record carrying the sentinel would tell a backend
|
||||||
|
// that a block it has just been handed was never uploaded.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `ProgramEmit`, not `ProgramEmitTest`: the file is XTest.cpp and the suite is X,
|
||||||
|
// this directory's convention, and it is what the gates grep for.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT: the
|
||||||
|
// applier-side cases are the wire commits' and the emitter-side cases are the client
|
||||||
|
// package's, and neither has to come back to MG_Test/Pipe/CMakeLists.txt to add one.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||||
|
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||||
|
// pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||||
|
TEST(ProgramEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(&MGPipeProgramEmitterInstance(), &MGPipeProgramEmitterInstance());
|
||||||
|
EXPECT_TRUE(kMGPipeWiredProgramSubsystem == 0 ||
|
||||||
|
kMGPipeWiredProgramSubsystem == kMGPipeSubsystemPrograms);
|
||||||
|
// The record the applier starts from carries the sentinel, not 0: a program that has never
|
||||||
|
// published a default-uniform-block image must not look like one that published version 0.
|
||||||
|
const MGPipeShaderCsoRecord fresh{};
|
||||||
|
EXPECT_EQ(fresh.GlobalConstantsVersion, ~Uint32{0});
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-programemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/SamplerEmitTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's sampler family: the content-addressed sampler CSO, the identity-addressed sampler view
|
||||||
|
// per texture object, and the set_sampler_views / bind_sampler_states unit sets.
|
||||||
|
//
|
||||||
|
// THIS SUITE IS A NAMED GATE (`ctest -R 'SamplerEmit\.'`), and its negative control is a
|
||||||
|
// script that stops the conversion copying SamplerParameters::borderColorForm and expects this
|
||||||
|
// suite to go red NAMING that field - which it must, because all three border-colour
|
||||||
|
// representations are always numerically populated and the value alone cannot say which driver
|
||||||
|
// entry point to use.
|
||||||
|
//
|
||||||
|
// THE ONE CASE THAT LOOKS LIKE PARANOIA AND IS NOT: SamplerParameters is 100 bytes with THREE
|
||||||
|
// BYTES OF TRAILING PADDING, so a cache that hashes or memcmps the object's own bytes reads
|
||||||
|
// uninitialised memory and mints a fresh CSO per call - a 256-entry cache with a hit rate of
|
||||||
|
// zero, and nobody notices, because the pixels are right. The case that writes garbage into
|
||||||
|
// the padding through a byte pointer is what turns that into a red gate.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `SamplerEmit`, not `SamplerEmitTest`: the file is XTest.cpp and the suite is X,
|
||||||
|
// this directory's convention, and it is what the gates grep for.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT: the
|
||||||
|
// applier-side cases are the wire commits' and the emitter-side cases are the client
|
||||||
|
// package's, and neither has to come back to MG_Test/Pipe/CMakeLists.txt to add one.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||||
|
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||||
|
// pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||||
|
TEST(SamplerEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(&MGPipeSamplerEmitterInstance(), &MGPipeSamplerEmitterInstance());
|
||||||
|
// One bit for the whole sampler family - the CSO, the view and all three unit sets,
|
||||||
|
// including set_shader_images, whose emitter lives in ImageEmit.h. An operator switching
|
||||||
|
// samplers off has to get the whole family's legacy arm, not two thirds of it.
|
||||||
|
EXPECT_TRUE(kMGPipeWiredSamplerSubsystem == 0 ||
|
||||||
|
kMGPipeWiredSamplerSubsystem == kMGPipeSubsystemSamplers);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-sampleremit-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/TextureEmitTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P4a's texture and renderbuffer family: resource_create from the constructor,
|
||||||
|
// resource_respecify from every storage definition, set_texture_params from the parameter
|
||||||
|
// mutators, and resource_subdata from the drain list at the validate point.
|
||||||
|
//
|
||||||
|
// THIS SUITE IS A NAMED GATE (`ctest -R 'TextureEmit\.'`), and one of its invariants is the
|
||||||
|
// one nothing else in the tree can see: the union box and the region list have to describe the
|
||||||
|
// SAME texels, because the server picks the upload shape from them and SSIM is completely
|
||||||
|
// blind to which one it picked. The Mali cliff behind that choice is ~+6 ms/frame for a
|
||||||
|
// hundred one-rect jobs against one union box.
|
||||||
|
//
|
||||||
|
// THE SUITE IS `TextureEmit`, not `TextureEmitTest`: the file is XTest.cpp and the suite is X,
|
||||||
|
// this directory's convention, and it is what the gates grep for.
|
||||||
|
//
|
||||||
|
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT. The
|
||||||
|
// applier-side cases are the wire commits'; the emitter-side cases (every texture target
|
||||||
|
// mapping to its own resource target, every bind kind setting its mask bit and the bit being
|
||||||
|
// sticky across a respecify, the image-bindable hint being forever, the box/rect invariant,
|
||||||
|
// the level shadow's strides, the collapse to the box past the rect cap, an upload through a
|
||||||
|
// view keying on the storage owner, every texture's params naming its built-in sampler CSO and
|
||||||
|
// two identical samplers sharing one, and a destroyed texture releasing its resource, view and
|
||||||
|
// sampler slots) are the client package's - and neither has to come back to
|
||||||
|
// MG_Test/Pipe/CMakeLists.txt to add one.
|
||||||
|
//
|
||||||
|
// IT HAS ITS OWN main() for ResourceEmitTest's reason: the applier's bounds and protocol trip
|
||||||
|
// wires report through a log line in a shipped push build and std::abort() in a poison or
|
||||||
|
// verify one.
|
||||||
|
//
|
||||||
|
// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N`
|
||||||
|
// stays name-for-name identical between the pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <process.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
String g_logPath;
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _getpid();
|
||||||
|
#else
|
||||||
|
return static_cast<int>(getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder: a
|
||||||
|
// static holding client state whose destructor an exit handler can run is the exit-order
|
||||||
|
// use-after-free this design closed once already.
|
||||||
|
TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(&MGPipeTextureEmitterInstance(), &MGPipeTextureEmitterInstance());
|
||||||
|
EXPECT_TRUE(kMGPipeWiredTextureSubsystem == 0 ||
|
||||||
|
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-textureemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -320,15 +320,29 @@ namespace {
|
|||||||
// keeps the name it was born with and follows the phase constant instead of a literal
|
// keeps the name it was born with and follows the phase constant instead of a literal
|
||||||
// five: what it has always asserted is "a bit names a subsystem if and only if this build
|
// five: what it has always asserted is "a bit names a subsystem if and only if this build
|
||||||
// emits a call for it", which is the property the emission gate and the residual-fill
|
// emits a call for it", which is the property the emission gate and the residual-fill
|
||||||
// skip both rest on. P3a took the vertex-input family over, so the set it compares
|
// skip both rest on. P3a took the vertex-input family over and P4a takes seven more bits
|
||||||
// against is now kMGPipeDirtyEmittedAtP3a - and a bit that gained an arm without gaining
|
// across four subsystems, so the set it compares against is now kMGPipeDirtyEmittedAtP4a -
|
||||||
// an emitter, or the reverse, still fails here.
|
// and a bit that gained an arm without gaining an emitter, or the reverse, still fails
|
||||||
|
// here.
|
||||||
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
|
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
|
||||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||||
const auto bit = static_cast<MGPipeDirty>(i);
|
const auto bit = static_cast<MGPipeDirty>(i);
|
||||||
const Bool emitted = (kMGPipeDirtyEmittedAtP3a & MGPipeDirtyBit(bit)) != 0;
|
const Bool emitted = (kMGPipeDirtyEmittedAtP4a & MGPipeDirtyBit(bit)) != 0;
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
|
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
|
||||||
}
|
}
|
||||||
|
// Each phase's constant SURVIVES as the next phase's A/B control, so the three are
|
||||||
|
// pinned as a chain rather than one being edited into the next: 0x1ff is P4a's "T2"
|
||||||
|
// arm and 0x7f is P3a's, and an operator's recorded mask has to keep meaning what it
|
||||||
|
// meant.
|
||||||
|
EXPECT_EQ(kMGPipeDirtyEmittedAtP4a & kMGPipeDirtyEmittedAtP3a, kMGPipeDirtyEmittedAtP3a);
|
||||||
|
EXPECT_EQ(kMGPipeDirtyEmittedAtP3a & kMGPipeDirtyEmittedAtP2, kMGPipeDirtyEmittedAtP2);
|
||||||
|
// The three bits P4a still does not emit for - the const-buffer, shader-buffer and
|
||||||
|
// stream-output sets - name no subsystem, so their fields keep going through the
|
||||||
|
// residual fill. Stated positively as well as through the loop above, because "only
|
||||||
|
// these three are left" is the phase's own scope statement.
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewConstBuffers), 0u);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers), 0u);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSoTargets), 0u);
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack);
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
|
||||||
@@ -339,6 +353,26 @@ namespace {
|
|||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements), kMGPipeSubsystemVertexInput);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements), kMGPipeSubsystemVertexInput);
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers), kMGPipeSubsystemVertexInput);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers), kMGPipeSubsystemVertexInput);
|
||||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer), kMGPipeSubsystemVertexInput);
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer), kMGPipeSubsystemVertexInput);
|
||||||
|
// P4a's seven, across FOUR subsystems, and the grouping is the whole point: the three
|
||||||
|
// program bits are one family because an operator switching programs off has to get
|
||||||
|
// the whole legacy arm, and so are the three unit-set bits.
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShader), kMGPipeSubsystemPrograms);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBindings), kMGPipeSubsystemPrograms);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewGlobalConstants), kMGPipeSubsystemPrograms);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewFramebuffer), kMGPipeSubsystemFramebuffer);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplerViews), kMGPipeSubsystemSamplers);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplers), kMGPipeSubsystemSamplers);
|
||||||
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderImages), kMGPipeSubsystemSamplers);
|
||||||
|
// AND NO BIT NAMES THE TEXTURE-RESOURCE SUBSYSTEM. Its calls 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, so a bit
|
||||||
|
// that started naming it would gate the emission twice and the two gates would
|
||||||
|
// disagree the first time one of them was edited.
|
||||||
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||||
|
EXPECT_NE(MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(i)),
|
||||||
|
kMGPipeSubsystemTextureResources)
|
||||||
|
<< kMGPipeDirtyNames[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(TrackerWalk, TheFirstWalkOnAFreshContextPublishesEverything) {
|
TEST_F(TrackerWalk, TheFirstWalkOnAFreshContextPublishesEverything) {
|
||||||
|
|||||||
@@ -296,3 +296,26 @@ target_link_libraries(
|
|||||||
)
|
)
|
||||||
|
|
||||||
gtest_discover_tests(ProgramArtifactsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramArtifactsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
# P4a: the archive SERIALIZER over the same VisitFields tables, beside the header's own suite.
|
||||||
|
# Its target and this registration are the CONTRACT commit's, like every other suite whose
|
||||||
|
# contents a later package extends. Every case is a visible SKIP in a pull build, because the
|
||||||
|
# codec is push-only - the root CMakeLists.txt appends it inside `if (MOBILEGL_PIPE_PUSH)` -
|
||||||
|
# and a vanishing test would make `ctest -N` differ between the pull and push trees.
|
||||||
|
add_executable(
|
||||||
|
ProgramArtifactsCodecTest
|
||||||
|
ProgramArtifactsCodecTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ProgramArtifactsCodecTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
ProgramArtifactsCodecTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
gtest_discover_tests(ProgramArtifactsCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Program/ProgramArtifactsCodecTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// The reflection ARCHIVE's serializer (P4a): create_shader_state's payload is per-stage SPIR-V
|
||||||
|
// plus LinkArtifacts + SpirvArtifacts, whole structs, and the codec is what turns them into
|
||||||
|
// bytes. In monolith it is never called on the hot path - the two structs ride beside the
|
||||||
|
// record through the apply entry point's companion pointers - so THIS SUITE plus the verify
|
||||||
|
// lane's round trip are the only things that exercise it until a transport exists.
|
||||||
|
//
|
||||||
|
// WHAT IT HAS TO PIN, and each of the three is a different failure:
|
||||||
|
// * a fully populated archive survives a round trip FIELD BY FIELD, including both of
|
||||||
|
// XfbVarying's spellings (the GL name AND the block instance / member / element triple)
|
||||||
|
// and every one of TypeFacts' twenty members - a codec that dropped one would be invisible
|
||||||
|
// until a backend read a reflection answer that had quietly become zero;
|
||||||
|
// * a TRUNCATED stream is refused rather than guessed at;
|
||||||
|
// * a VERSION or struct-size mismatch is refused rather than deserialised into a layout this
|
||||||
|
// build does not have.
|
||||||
|
//
|
||||||
|
// AND ONE PROPERTY THAT IS NOT ABOUT BYTES AT ALL: LinkArtifacts has 58 members and its
|
||||||
|
// VisitFields table visits 57. The 58th is the live SharedPtr<glslang::TProgram>, which is
|
||||||
|
// null for every archived instance by construction and points into an arena no archive owns.
|
||||||
|
// The codec has no arm for it, and the count is asserted here because that is the only place
|
||||||
|
// it can be: VisitFields needs an instance, and these structs carry strings, vectors and maps,
|
||||||
|
// so no static_assert can walk them.
|
||||||
|
//
|
||||||
|
// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N`
|
||||||
|
// stays name-for-name identical between the pull and the push trees.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
using namespace MobileGL::MG_State::GLState;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
TypeFacts MakeTypeFacts() {
|
||||||
|
// Every member set to something that is NOT its default, so a field the codec skips
|
||||||
|
// reads back as the default and the comparison names it.
|
||||||
|
TypeFacts facts{};
|
||||||
|
facts.isArray = true;
|
||||||
|
facts.isSizedArray = true;
|
||||||
|
facts.isMatrix = true;
|
||||||
|
facts.isVector = true;
|
||||||
|
facts.isOpaque = true;
|
||||||
|
facts.isTexture = true;
|
||||||
|
facts.isImage = true;
|
||||||
|
facts.isDouble = true;
|
||||||
|
facts.isVoid = true;
|
||||||
|
facts.isBuffer = true;
|
||||||
|
facts.isPatch = true;
|
||||||
|
facts.hasIndex = true;
|
||||||
|
facts.hasFormat = true;
|
||||||
|
facts.vectorSize = 3;
|
||||||
|
facts.matrixCols = 4;
|
||||||
|
facts.matrixRows = 2;
|
||||||
|
facts.layoutIndex = 7;
|
||||||
|
facts.layoutFormat = 0x8814u;
|
||||||
|
facts.layoutMatrix = 1;
|
||||||
|
facts.basicType = 11;
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceReflection MakeReflection(const char* name, Int location) {
|
||||||
|
ResourceReflection reflection{};
|
||||||
|
reflection.name = name;
|
||||||
|
reflection.glDefineType = GL_FLOAT_VEC4;
|
||||||
|
reflection.offset = 16;
|
||||||
|
reflection.size = 4;
|
||||||
|
reflection.index = 2;
|
||||||
|
reflection.counterIndex = 3;
|
||||||
|
reflection.arrayStride = 16;
|
||||||
|
reflection.topLevelArraySize = 5;
|
||||||
|
reflection.topLevelArrayStride = 32;
|
||||||
|
reflection.binding = 6;
|
||||||
|
reflection.location = location;
|
||||||
|
reflection.stages = 0x3u;
|
||||||
|
reflection.arraySize = 8;
|
||||||
|
reflection.type = MakeTypeFacts();
|
||||||
|
return reflection;
|
||||||
|
}
|
||||||
|
|
||||||
|
XfbVarying MakeXfbVarying() {
|
||||||
|
XfbVarying varying{};
|
||||||
|
// BOTH SPELLINGS. `name` is the GL one ("Block.member"), which the interface queries
|
||||||
|
// and the ESSL driver-side capture list need; the triple below is what a SPIR-V
|
||||||
|
// backend needs instead, because the decoration target is the block's instance
|
||||||
|
// variable and the member index inside it.
|
||||||
|
varying.name = "Captured.position";
|
||||||
|
varying.type = GL_FLOAT_VEC3;
|
||||||
|
varying.size = 2;
|
||||||
|
varying.bufferIndex = 1;
|
||||||
|
varying.offsetBytes = 12;
|
||||||
|
varying.byteSize = 24;
|
||||||
|
varying.packedOffsetBytes = 8;
|
||||||
|
varying.blockInstanceName = "capturedInstance";
|
||||||
|
varying.blockName = "Captured";
|
||||||
|
varying.blockMemberIndex = 1;
|
||||||
|
varying.blockMemberElement = 3;
|
||||||
|
return varying;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkArtifacts MakeLinkArtifacts() {
|
||||||
|
LinkArtifacts link{};
|
||||||
|
link.uniformReflection = {MakeReflection("uColour", 0), MakeReflection("uMatrix", 1)};
|
||||||
|
link.blockReflection = {MakeReflection("Block", -1)};
|
||||||
|
link.pipeInputReflection = {MakeReflection("inPosition", 0)};
|
||||||
|
link.pipeOutputReflection = {MakeReflection("outColour", 0)};
|
||||||
|
link.lastStageIsFragment = true;
|
||||||
|
link.computeLocalSize = {8u, 4u, 2u};
|
||||||
|
link.uniformIndexByName = {{"uColour", 0}, {"uMatrix", 1}};
|
||||||
|
link.attribs = {"inPosition", "inNormal"};
|
||||||
|
link.attribTypes = {GL_FLOAT_VEC3, GL_FLOAT_VEC3};
|
||||||
|
link.linkedFragDataLocation = {{"outColour", 0u}};
|
||||||
|
link.linkedFragDataIndex = {{"outColour", 1u}};
|
||||||
|
link.glUniformIndexToTProgram = {0, 1};
|
||||||
|
link.tProgramUniformIndexToGl = {0, 1};
|
||||||
|
link.glBlockIndexToTProgram = {0};
|
||||||
|
link.tProgramBlockIndexToGl = {-1};
|
||||||
|
link.glUniformBlockIndexToBlock = {0};
|
||||||
|
link.blockIndexToGlUniformBlock = {0};
|
||||||
|
link.linkedExplicitUniformLocations = {{"uColour", 3}};
|
||||||
|
link.uniformLocations = {{"uColour", 0u}, {"uMatrix", 4u}};
|
||||||
|
link.writtenUniformLocationBits = {0x5ull};
|
||||||
|
link.writtenUniformIndexBits = {0x3ull};
|
||||||
|
link.writtenUniformIndices = {0u, 1u};
|
||||||
|
link.uniformIndexInTProgram = {0, 1};
|
||||||
|
link.uniformSamplerOrImageUnitIndex = {-1, 2};
|
||||||
|
link.explicitOpaqueUniformBindings = {{"uSampler", 5u}};
|
||||||
|
link.uniformBlockIndexByName = {{"Block", 0u}};
|
||||||
|
link.uniformBlockBinding = {2};
|
||||||
|
link.shaderStorageBlockBinding = {{"Storage", 1}};
|
||||||
|
link.storageBlocksWithoutBinding = {"Storage"};
|
||||||
|
link.uniformBlocksWithoutBinding = {"Block"};
|
||||||
|
link.activeUniformCount = 2u;
|
||||||
|
link.usesReservedNumSamples = true;
|
||||||
|
link.maxUniformLocation = 4u;
|
||||||
|
link.uniformNameMaxLength = 9;
|
||||||
|
link.attribInNameMaxLength = 11;
|
||||||
|
link.uniformBlockNameMaxLength = 6;
|
||||||
|
link.infoLog = "linked with warnings";
|
||||||
|
link.linkStatus = true;
|
||||||
|
link.xfbVaryings = {MakeXfbVarying()};
|
||||||
|
link.xfbInterfaceNames = {"gl_NextBuffer", "Captured.position"};
|
||||||
|
link.xfbStrides = {32u, 0u};
|
||||||
|
link.gsStripTriangles = {3u, 5u};
|
||||||
|
link.gsStripCaptureFixup = true;
|
||||||
|
link.gsInputPrimitive = GL_TRIANGLES;
|
||||||
|
link.tcsOutputVertices = 3;
|
||||||
|
link.gsOutputPrimitive = GL_TRIANGLE_STRIP;
|
||||||
|
link.gsMaxVertices = 12;
|
||||||
|
link.gsInvocations = 2;
|
||||||
|
link.tessGenMode = GL_QUADS;
|
||||||
|
link.tessGenSpacing = GL_FRACTIONAL_ODD;
|
||||||
|
link.tessGenVertexOrder = GL_CW;
|
||||||
|
link.tessGenPointMode = true;
|
||||||
|
link.xfbBufferMode = GL_SEPARATE_ATTRIBS;
|
||||||
|
link.xfbVaryingNameMaxLength = 18;
|
||||||
|
link.xfbNeedsScatteredCapture = true;
|
||||||
|
link.xfbPackedStride = 24u;
|
||||||
|
|
||||||
|
// The one glslang-typed member that DOES travel: a plain aggregate of a string,
|
||||||
|
// scalars and two vectors. The codec has a hand-written arm for it because
|
||||||
|
// ProgramArtifacts.h gives it no VisitFields table.
|
||||||
|
glslang::TIntermediate::TUniformInitializer initializer;
|
||||||
|
initializer.name = "uInitialised";
|
||||||
|
initializer.basicType = glslang::EbtInt;
|
||||||
|
initializer.vectorSize = 2;
|
||||||
|
initializer.matrixCols = 0;
|
||||||
|
initializer.matrixRows = 0;
|
||||||
|
initializer.arraySize = 3;
|
||||||
|
initializer.intValues = {1, 2, 3, 4, 5, 6};
|
||||||
|
initializer.floatValues = {};
|
||||||
|
link.uniformInitialValues.push_back(initializer);
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpirvArtifacts MakeSpirvArtifacts() {
|
||||||
|
SpirvArtifacts spirv{};
|
||||||
|
spirv.generatedSpirv = {{0x07230203u, 0x00010300u, 0u}, {0x07230203u, 0x00010300u, 1u}};
|
||||||
|
spirv.enableSpirvValidation = true;
|
||||||
|
spirv.uniformOffsets = {0u, 16u, kInvalidUniformOffset};
|
||||||
|
spirv.globalUboScratch = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||||
|
spirv.reservedNumSamplesOffset = 32u;
|
||||||
|
spirv.spirvStatus = true;
|
||||||
|
spirv.nativeFloat64 = true;
|
||||||
|
spirv.pointSizeDemoted = true;
|
||||||
|
return spirv;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
// The round trip, field by field. A re-encode equality alone would prove the codec is
|
||||||
|
// self-consistent and nothing else - a field it skips in BOTH directions round-trips
|
||||||
|
// perfectly - so the members are read back explicitly first, and the byte comparison is the
|
||||||
|
// catch-all underneath them.
|
||||||
|
TEST(ProgramArtifactsCodec, RoundTripsAFullyPopulatedArchive) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
const LinkArtifacts link = MakeLinkArtifacts();
|
||||||
|
const SpirvArtifacts spirv = MakeSpirvArtifacts();
|
||||||
|
|
||||||
|
Vector<Uint8> bytes;
|
||||||
|
EncodeProgramArtifacts(link, spirv, bytes);
|
||||||
|
ASSERT_FALSE(bytes.empty());
|
||||||
|
|
||||||
|
LinkArtifacts decodedLink;
|
||||||
|
SpirvArtifacts decodedSpirv;
|
||||||
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), decodedLink, decodedSpirv));
|
||||||
|
|
||||||
|
// The four reflection vectors, with their TypeFacts.
|
||||||
|
ASSERT_EQ(decodedLink.uniformReflection.size(), 2u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].name, "uColour");
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[1].location, 1);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].arrayStride, 16);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].stages, 0x3u);
|
||||||
|
EXPECT_TRUE(decodedLink.uniformReflection[0].type.isSizedArray);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.matrixCols, 4);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.layoutFormat, 0x8814u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.basicType, 11);
|
||||||
|
ASSERT_EQ(decodedLink.blockReflection.size(), 1u);
|
||||||
|
ASSERT_EQ(decodedLink.pipeInputReflection.size(), 1u);
|
||||||
|
ASSERT_EQ(decodedLink.pipeOutputReflection.size(), 1u);
|
||||||
|
|
||||||
|
// BOTH XfbVarying SPELLINGS.
|
||||||
|
ASSERT_EQ(decodedLink.xfbVaryings.size(), 1u);
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].name, "Captured.position");
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockInstanceName, "capturedInstance");
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockName, "Captured");
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockMemberIndex, 1);
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockMemberElement, 3);
|
||||||
|
EXPECT_EQ(decodedLink.xfbVaryings[0].packedOffsetBytes, 8u);
|
||||||
|
|
||||||
|
// The maps, the set and the fixed array - the four container shapes the archive is made
|
||||||
|
// of, each with a reader that has to agree with its writer about the length prefix.
|
||||||
|
EXPECT_EQ(decodedLink.uniformIndexByName.size(), 2u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformIndexByName.at("uMatrix"), 1);
|
||||||
|
EXPECT_EQ(decodedLink.shaderStorageBlockBinding.at("Storage"), 1);
|
||||||
|
EXPECT_EQ(decodedLink.storageBlocksWithoutBinding.count("Storage"), 1u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformBlocksWithoutBinding.count("Block"), 1u);
|
||||||
|
EXPECT_EQ(decodedLink.computeLocalSize[0], 8u);
|
||||||
|
EXPECT_EQ(decodedLink.computeLocalSize[2], 2u);
|
||||||
|
|
||||||
|
// The glslang-typed aggregate, through the codec's one hand-written arm.
|
||||||
|
ASSERT_EQ(decodedLink.uniformInitialValues.size(), 1u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].name, "uInitialised");
|
||||||
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].basicType, glslang::EbtInt);
|
||||||
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].arraySize, 3);
|
||||||
|
ASSERT_EQ(decodedLink.uniformInitialValues[0].intValues.size(), 6u);
|
||||||
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].intValues[5], 6);
|
||||||
|
EXPECT_TRUE(decodedLink.uniformInitialValues[0].floatValues.empty());
|
||||||
|
|
||||||
|
// The scalars at the tail, which is where a length-prefix that drifted by one would first
|
||||||
|
// read as garbage rather than as a short read.
|
||||||
|
EXPECT_EQ(decodedLink.infoLog, "linked with warnings");
|
||||||
|
EXPECT_TRUE(decodedLink.linkStatus);
|
||||||
|
EXPECT_EQ(decodedLink.tessGenSpacing, static_cast<GLenum>(GL_FRACTIONAL_ODD));
|
||||||
|
EXPECT_TRUE(decodedLink.tessGenPointMode);
|
||||||
|
EXPECT_EQ(decodedLink.xfbPackedStride, 24u);
|
||||||
|
|
||||||
|
// SpirvArtifacts, including the nested vector of SPIR-V words and the sentinel offset.
|
||||||
|
ASSERT_EQ(decodedSpirv.generatedSpirv.size(), 2u);
|
||||||
|
ASSERT_EQ(decodedSpirv.generatedSpirv[0].size(), 3u);
|
||||||
|
EXPECT_EQ(decodedSpirv.generatedSpirv[1][2], 1u);
|
||||||
|
ASSERT_EQ(decodedSpirv.uniformOffsets.size(), 3u);
|
||||||
|
EXPECT_EQ(decodedSpirv.uniformOffsets[2], kInvalidUniformOffset);
|
||||||
|
EXPECT_EQ(decodedSpirv.globalUboScratch.size(), 8u);
|
||||||
|
EXPECT_EQ(decodedSpirv.reservedNumSamplesOffset, 32u);
|
||||||
|
EXPECT_TRUE(decodedSpirv.nativeFloat64);
|
||||||
|
EXPECT_TRUE(decodedSpirv.pointSizeDemoted);
|
||||||
|
|
||||||
|
// THE LIVE TProgram IS NEVER CARRIED and never reconstructed.
|
||||||
|
EXPECT_EQ(decodedLink.program, nullptr);
|
||||||
|
|
||||||
|
// The catch-all: re-encoding what came back has to produce the same bytes, which covers
|
||||||
|
// every member the explicit reads above do not name.
|
||||||
|
Vector<Uint8> reencoded;
|
||||||
|
EncodeProgramArtifacts(decodedLink, decodedSpirv, reencoded);
|
||||||
|
EXPECT_EQ(reencoded, bytes);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: the archive codec is push-only";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control 1. Every prefix length is checked against the bytes that REMAIN, so a
|
||||||
|
// stream cut anywhere has to come back false with both outputs defaulted - never a partially
|
||||||
|
// filled archive, and never a resize driven by a count the stream cannot back.
|
||||||
|
TEST(ProgramArtifactsCodec, ATruncatedStreamIsRefusedNotGuessed) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Vector<Uint8> bytes;
|
||||||
|
EncodeProgramArtifacts(MakeLinkArtifacts(), MakeSpirvArtifacts(), bytes);
|
||||||
|
ASSERT_GT(bytes.size(), 64u);
|
||||||
|
|
||||||
|
// Cut at a spread of points rather than one: the header, a length prefix, the middle of a
|
||||||
|
// string and the last byte all fail through different branches.
|
||||||
|
for (const SizeT cut : {SizeT{0}, SizeT{4}, SizeT{9}, bytes.size() / 3, bytes.size() / 2,
|
||||||
|
bytes.size() - 1}) {
|
||||||
|
LinkArtifacts link;
|
||||||
|
SpirvArtifacts spirv;
|
||||||
|
link.infoLog = "must be cleared";
|
||||||
|
EXPECT_FALSE(DecodeProgramArtifacts(bytes.data(), cut, link, spirv))
|
||||||
|
<< "a stream truncated at " << cut << " was accepted";
|
||||||
|
EXPECT_TRUE(link.infoLog.empty()) << "a refused decode left the output half-filled";
|
||||||
|
EXPECT_TRUE(link.uniformReflection.empty());
|
||||||
|
EXPECT_TRUE(spirv.generatedSpirv.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// And TRAILING bytes are a mismatch too: the format accounts for every byte it writes, so
|
||||||
|
// anything left over means the reader and the writer disagree about the shape.
|
||||||
|
Vector<Uint8> withTail = bytes;
|
||||||
|
withTail.push_back(0);
|
||||||
|
LinkArtifacts link;
|
||||||
|
SpirvArtifacts spirv;
|
||||||
|
EXPECT_FALSE(DecodeProgramArtifacts(withTail.data(), withTail.size(), link, spirv));
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: the archive codec is push-only";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control 2. The version word and the struct-size echo are the two things a compiler
|
||||||
|
// cannot check: a struct that gained a field and a codec that did not would otherwise
|
||||||
|
// deserialise garbage into the tail of a reflection table. Both have to REFUSE.
|
||||||
|
TEST(ProgramArtifactsCodec, AVersionMismatchIsRefused) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
Vector<Uint8> bytes;
|
||||||
|
EncodeProgramArtifacts(MakeLinkArtifacts(), MakeSpirvArtifacts(), bytes);
|
||||||
|
ASSERT_GT(bytes.size(), 12u);
|
||||||
|
|
||||||
|
LinkArtifacts link;
|
||||||
|
SpirvArtifacts spirv;
|
||||||
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), link, spirv));
|
||||||
|
|
||||||
|
// The version word first: a reader that saw a format it does not know must not try to
|
||||||
|
// guess the layout.
|
||||||
|
Vector<Uint8> wrongVersion = bytes;
|
||||||
|
++wrongVersion[0];
|
||||||
|
EXPECT_FALSE(DecodeProgramArtifacts(wrongVersion.data(), wrongVersion.size(), link, spirv));
|
||||||
|
|
||||||
|
// Then the MGL_LINKARTIFACTS_SIZE echo, which is the half that catches a struct that grew
|
||||||
|
// under a codec that did not - the failure the four sizeof trip wires in
|
||||||
|
// ProgramArtifacts.h send an author here to fix.
|
||||||
|
Vector<Uint8> wrongSize = bytes;
|
||||||
|
++wrongSize[4];
|
||||||
|
EXPECT_FALSE(DecodeProgramArtifacts(wrongSize.data(), wrongSize.size(), link, spirv));
|
||||||
|
|
||||||
|
// A null pointer is refused rather than dereferenced.
|
||||||
|
EXPECT_FALSE(DecodeProgramArtifacts(nullptr, 0, link, spirv));
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: the archive codec is push-only";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// The codec walks the VisitFields tables and nothing else, so what those tables visit IS the
|
||||||
|
// archive. LinkArtifacts has 58 members and its table visits 57: the 58th is the live glslang
|
||||||
|
// TProgram, which is null for every archived instance by construction and points into an arena
|
||||||
|
// no archive owns. A codec arm for it would be a use-after-free waiting for a cache hit.
|
||||||
|
TEST(ProgramArtifactsCodec, TheTablesVisitEveryMemberExceptTheLiveProgram) {
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<LinkArtifacts>(), 57u);
|
||||||
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<SpirvArtifacts>(), 8u);
|
||||||
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<ResourceReflection>(), 14u);
|
||||||
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<XfbVarying>(), 11u);
|
||||||
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<TypeFacts>(), 20u);
|
||||||
|
#else
|
||||||
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: the archive codec is push-only";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -160,6 +160,17 @@ namespace {
|
|||||||
// the storage-regrow gate reads, so its short name is pinned where an operator's
|
// the storage-regrow gate reads, so its short name is pinned where an operator's
|
||||||
// grep would break.
|
// grep would break.
|
||||||
EXPECT_NE(line.find("mpr="), String::npos) << line;
|
EXPECT_NE(line.find("mpr="), String::npos) << line;
|
||||||
|
// P4a's emission bracket, and its short names are pinned for exactly the same reason:
|
||||||
|
// fbe/sve/sse/sie are the four suppressors' hit rates and ctu is the client half of
|
||||||
|
// the upload-shape comparison, so a rename breaks every recorded reading of them.
|
||||||
|
EXPECT_NE(line.find("emit[fbe="), String::npos) << line;
|
||||||
|
EXPECT_NE(line.find("sve="), String::npos) << line;
|
||||||
|
EXPECT_NE(line.find("sse="), String::npos) << line;
|
||||||
|
EXPECT_NE(line.find("sie="), String::npos) << line;
|
||||||
|
EXPECT_NE(line.find("ctu="), String::npos) << line;
|
||||||
|
// And the new ByteClass rides the ordinary bytes[] bracket under a short name that is
|
||||||
|
// NOT "csob": the cso[] bracket above already prints csob= for the CSO bind count.
|
||||||
|
EXPECT_NE(line.find("csob-blob="), String::npos) << line;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +292,18 @@ namespace {
|
|||||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints");
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints");
|
||||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds");
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds");
|
||||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::MapPersistentRoundtrips), "map-persistent-roundtrips");
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::MapPersistentRoundtrips), "map-persistent-roundtrips");
|
||||||
|
// P4a's six. The four set counters are how the suppressors' hit rates are read, ctu is
|
||||||
|
// the client-side twin of Espryt's tex-upload-emissions - a divergence between the two
|
||||||
|
// is the only way an upload-SHAPE regression becomes visible, because SSIM cannot see
|
||||||
|
// the box/rect split at all - and cso-blob-bytes is the ByteClass that discharges the
|
||||||
|
// summary line's missing CSO-blob row.
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::FramebufferEmissions), "framebuffer-emissions");
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::SamplerViewEmissions), "sampler-view-emissions");
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::SamplerStateEmissions), "sampler-state-emissions");
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::ShaderImageEmissions), "shader-image-emissions");
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::ClientTextureUploadEmissions),
|
||||||
|
"client-tex-upload-emissions");
|
||||||
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::CsoBlobBytes), "cso-blob-bytes");
|
||||||
#endif
|
#endif
|
||||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
|
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
|
||||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
|
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
|
||||||
|
|||||||
@@ -169,12 +169,17 @@ namespace MobileGL::MG_Util::PipeStats {
|
|||||||
"stage-buffer", "stage-texture", "stage-ubo-global",
|
"stage-buffer", "stage-texture", "stage-ubo-global",
|
||||||
"stage-ubo-named", "stage-vertex-client", "stage-index-client",
|
"stage-ubo-named", "stage-vertex-client", "stage-index-client",
|
||||||
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
|
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
"cso-blob-bytes",
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
const char* const kCallClassNames[kCallClassCount] = {
|
const char* const kCallClassNames[kCallClassCount] = {
|
||||||
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
|
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
|
||||||
"tex-upload-jobs",
|
"tex-upload-jobs",
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
|
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
|
||||||
|
"framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions",
|
||||||
|
"shader-image-emissions", "client-tex-upload-emissions",
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
const char* const kGateNames[kGateCount] = {
|
const char* const kGateNames[kGateCount] = {
|
||||||
@@ -193,8 +198,15 @@ namespace MobileGL::MG_Util::PipeStats {
|
|||||||
"magma-draw-fastpath-miss", "magma-pipeline-memo-miss", "magma-dynamic-tail-miss",
|
"magma-draw-fastpath-miss", "magma-pipeline-memo-miss", "magma-dynamic-tail-miss",
|
||||||
};
|
};
|
||||||
// Short forms, so the per-120-frame line stays one terminal line wide.
|
// Short forms, so the per-120-frame line stays one terminal line wide.
|
||||||
|
// "csob-blob" and not "csob": the cso[] bracket below already prints csob= for the
|
||||||
|
// render-state CSO BIND count, and two different numbers under one grep is how a
|
||||||
|
// recorded baseline stops meaning anything.
|
||||||
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
|
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
|
||||||
"idxc", "icmd", "pmap", "resid"};
|
"idxc", "icmd", "pmap", "resid",
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
"csob-blob",
|
||||||
|
#endif
|
||||||
|
};
|
||||||
const char* const kGateShort[kGateCount] = {"ers", "etl", "eub", "mfp", "mpm", "mdt"};
|
const char* const kGateShort[kGateCount] = {"ers", "etl", "eub", "mfp", "mpm", "mdt"};
|
||||||
|
|
||||||
void ResetCounters() {
|
void ResetCounters() {
|
||||||
@@ -429,6 +441,18 @@ namespace MobileGL::MG_Util::PipeStats {
|
|||||||
// reason: it is push-only, and a window with an unexpected mpr= is the one number
|
// reason: it is push-only, and a window with an unexpected mpr= is the one number
|
||||||
// that says an adoption is happening per draw rather than per storage definition.
|
// that says an adoption is happening per draw rather than per storage definition.
|
||||||
line += " mpr=" + std::to_string(calls[static_cast<Uint32>(CallClass::MapPersistentRoundtrips)]);
|
line += " mpr=" + std::to_string(calls[static_cast<Uint32>(CallClass::MapPersistentRoundtrips)]);
|
||||||
|
// P4a's four suppressor-visible emission counts and the client-side upload twin, on a
|
||||||
|
// bracket of their own so one grep reads the whole family. Every one of them is
|
||||||
|
// post-suppressor: a set that was resolved and then not sent does not appear here, and
|
||||||
|
// that is what makes fbe/sve/sse/sie the suppressors' hit rates rather than their call
|
||||||
|
// rates. ctu is the CLIENT's count of the same texture records Espryt's tex[emit=]
|
||||||
|
// counts on the server - the two agreeing is the whole reason both are printed.
|
||||||
|
line += "] emit[fbe=" + std::to_string(calls[static_cast<Uint32>(CallClass::FramebufferEmissions)]);
|
||||||
|
line += " sve=" + std::to_string(calls[static_cast<Uint32>(CallClass::SamplerViewEmissions)]);
|
||||||
|
line += " sse=" + std::to_string(calls[static_cast<Uint32>(CallClass::SamplerStateEmissions)]);
|
||||||
|
line += " sie=" + std::to_string(calls[static_cast<Uint32>(CallClass::ShaderImageEmissions)]);
|
||||||
|
line += " ctu=" +
|
||||||
|
std::to_string(calls[static_cast<Uint32>(CallClass::ClientTextureUploadEmissions)]);
|
||||||
#endif
|
#endif
|
||||||
line += "] gates[";
|
line += "] gates[";
|
||||||
for (Uint32 i = 0; i < kGateCount; ++i) {
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ namespace MobileGL::MG_Util::PipeStats {
|
|||||||
// PLACEHOLDER (plan section 6.3): the residual value block does not exist yet. The
|
// PLACEHOLDER (plan section 6.3): the residual value block does not exist yet. The
|
||||||
// class is minted now so the counter names never churn; it stays at 0 until P2.
|
// class is minted now so the counter names never churn; it stays at 0 until P2.
|
||||||
ResidualValueBlock,
|
ResidualValueBlock,
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
// P4a's, and THE PUSH GUARD IS NEW ON THIS ENUM: CallClass has had one since P2 and
|
||||||
|
// ByteClass has never had one, so the block is opened here rather than the member
|
||||||
|
// simply appended. Without it the pull build's two counter arrays, the name table, the
|
||||||
|
// short-name table and FormatWindowLine all resize for a class that could never leave
|
||||||
|
// zero - and the pull build has to stay symbol-identical.
|
||||||
|
//
|
||||||
|
// The bytes of every CSO BLOB the client declares in a frame: P3a's vertex-elements
|
||||||
|
// blobs (which MEASUREMENTS.md recorded as a client-side array that was never
|
||||||
|
// measured, and left to P4a to give the summary line a class for), P4a's sampler
|
||||||
|
// parameter blobs, and P4a's program archives. It is the number that says what a
|
||||||
|
// transport would actually have to move for the CSO families, as opposed to what the
|
||||||
|
// records themselves cost.
|
||||||
|
CsoBlobBytes,
|
||||||
|
#endif
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +139,23 @@ namespace MobileGL::MG_Util::PipeStats {
|
|||||||
// first window. Counted at the client emitter, behind the usual Enabled() predicate;
|
// first window. Counted at the client emitter, behind the usual Enabled() predicate;
|
||||||
// no timer anywhere.
|
// no timer anywhere.
|
||||||
MapPersistentRoundtrips,
|
MapPersistentRoundtrips,
|
||||||
|
// P4a's five, push-only for the same reason as the three above, and every one of them
|
||||||
|
// counts a record that ACTUALLY WENT OUT - post-suppressor - because the number an
|
||||||
|
// operator needs is the traffic, not the number of times the emitter was asked.
|
||||||
|
//
|
||||||
|
// The four set counters are how the suppressors' hit rates become readable at all: a
|
||||||
|
// suppressor that stopped suppressing is invisible in the pixels and shows up here as
|
||||||
|
// a per-frame count that tracks the draw count instead of the state changes.
|
||||||
|
FramebufferEmissions,
|
||||||
|
SamplerViewEmissions,
|
||||||
|
SamplerStateEmissions,
|
||||||
|
ShaderImageEmissions,
|
||||||
|
// The CLIENT-side twin of Espryt's TextureUploadEmissions, which counts the same
|
||||||
|
// records on the server. Two published numbers rather than one is the whole point:
|
||||||
|
// SSIM is completely blind to the box-versus-rect upload shape, and the Mali cliff it
|
||||||
|
// hides is ~+6 ms/frame, so an emission-shape divergence has to be a difference of two
|
||||||
|
// numbers rather than something only a GPU can see.
|
||||||
|
ClientTextureUploadEmissions,
|
||||||
#endif
|
#endif
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,8 +71,21 @@ SCAN_ROOTS = (os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "GLImpl"),
|
|||||||
os.path.join(REPO_ROOT, "MobileGL", "MG_State", "GLState"))
|
os.path.join(REPO_ROOT, "MobileGL", "MG_State", "GLState"))
|
||||||
|
|
||||||
# The mutating half of GLContext's surface. Prefix-matched, per the plan's list.
|
# The mutating half of GLContext's surface. Prefix-matched, per the plan's list.
|
||||||
|
#
|
||||||
|
# P4a WIDENS IT BY EXACTLY TWO WORDS, `Use` and `Bind`, and the hole they close is a coverage
|
||||||
|
# hole in this heuristic rather than a red gate that was being ignored: `UseProgram` begins
|
||||||
|
# with "Use" and `BindVertexArray`, `BindProgramPipelineObject` and `BindTransformFeedbackObject`
|
||||||
|
# begin with "Bind", so none of the four was ever visible to this scan - and each of them moves
|
||||||
|
# a field P3a or P4a pushes. The complete set the widening surfaces was enumerated by grep at
|
||||||
|
# the phase's base ref before the change landed, so it is four names on seven call sites and
|
||||||
|
# not a discovery.
|
||||||
|
#
|
||||||
|
# `Create` and `Pop` are DELIBERATELY NOT ADDED; DirtySurface.def's header carries the reason,
|
||||||
|
# which is that they create or destroy objects rather than move a pushed field, and each
|
||||||
|
# object class's creation and destruction is already answered by its own Mark*ForDeletion row
|
||||||
|
# plus the constructor-time resource_create.
|
||||||
MUTATOR_PREFIXES = ("Add", "Set", "Mark", "Bump", "Allocate", "Truncate", "Record", "Notify",
|
MUTATOR_PREFIXES = ("Add", "Set", "Mark", "Bump", "Allocate", "Truncate", "Record", "Notify",
|
||||||
"Begin", "End")
|
"Begin", "End", "Use", "Bind")
|
||||||
|
|
||||||
MUTATOR_RE = re.compile(r"pGLContext->\s*((?:%s)\w*)\s*\(" % "|".join(MUTATOR_PREFIXES))
|
MUTATOR_RE = re.compile(r"pGLContext->\s*((?:%s)\w*)\s*\(" % "|".join(MUTATOR_PREFIXES))
|
||||||
# The SECOND publish mechanism (MG_Pipe/PipeMutation.h). It carries the FIELD, not a mutator
|
# The SECOND publish mechanism (MG_Pipe/PipeMutation.h). It carries the FIELD, not a mutator
|
||||||
@@ -1769,6 +1782,47 @@ def self_test(scanned, bits, publishers, movers, moved, outside=None, undecided_
|
|||||||
tripped(any(p.startswith("STALE undecided mark NEW_PIXEL_PACK for SetPixelStoreParam")
|
tripped(any(p.startswith("STALE undecided mark NEW_PIXEL_PACK for SetPixelStoreParam")
|
||||||
for p in problems), "18 (a stale undecided mark)")
|
for p in problems), "18 (a stale undecided mark)")
|
||||||
|
|
||||||
|
# 19. THE PREFIX WIDENING ITSELF (P4a). `Use` and `Bind` are what make the four new rows
|
||||||
|
# visible at all, and the control asserts BOTH halves of that - P3a's ten-word set
|
||||||
|
# matches none of the four, and the current set matches exactly the four - because
|
||||||
|
# "the pattern matches now" and "the pattern did not match before" are different
|
||||||
|
# claims, and only the pair says the widening bought anything.
|
||||||
|
p3a_prefixes = ("Add", "Set", "Mark", "Bump", "Allocate", "Truncate", "Record", "Notify",
|
||||||
|
"Begin", "End")
|
||||||
|
p3a_re = re.compile(r"pGLContext->\s*((?:%s)\w*)\s*\(" % "|".join(p3a_prefixes))
|
||||||
|
widened_names = ("UseProgram", "BindVertexArray", "BindProgramPipelineObject",
|
||||||
|
"BindTransformFeedbackObject")
|
||||||
|
sample = " ".join("pGLContext->%s(x);" % name for name in widened_names)
|
||||||
|
tripped(not p3a_re.findall(sample)
|
||||||
|
and sorted(MUTATOR_RE.findall(sample)) == sorted(widened_names),
|
||||||
|
"19 (P3a's prefix set is blind to the four mutators `Use` and `Bind` add)")
|
||||||
|
|
||||||
|
# 20a-20d. ONE CONTROL PER NEW ROW, and each is the row's own: with that ONE mutator gone
|
||||||
|
# from what the scan finds - which is what a narrowed prefix set, a renamed entry point
|
||||||
|
# or a deleted call site would produce - its row has to come out as a STALE row rather
|
||||||
|
# than sitting in the file describing a mutator that no longer exists. The other three
|
||||||
|
# rows must not trip on it, or one control would be standing in for four.
|
||||||
|
for index, name in enumerate(widened_names):
|
||||||
|
without = {m: c for m, c in scanned.items() if m != name} if isinstance(scanned, dict) \
|
||||||
|
else set(scanned) - {name}
|
||||||
|
problems = check_mapping(real, real_duplicates, without, bits)
|
||||||
|
stale = [p for p in problems if p.startswith("STALE row")]
|
||||||
|
tripped(len(stale) == 1 and name in stale[0],
|
||||||
|
"20%s (the %s row is STALE the moment the scan stops finding it)"
|
||||||
|
% ("abcd"[index], name))
|
||||||
|
|
||||||
|
# 21. THE TWO UNDECIDED MARKS ARE STILL LOAD-BEARING. Dropping them has to make --check
|
||||||
|
# refuse both rows as unmarked UNDECIDED - which is what says the marks are covering a
|
||||||
|
# real blind spot rather than a verdict the analysis could give today. Control 18 is
|
||||||
|
# the other direction: a mark the derivation DOES decide is itself a problem, so
|
||||||
|
# neither of these can outlive its reason.
|
||||||
|
problems, _, _, undecided_rows = object_class_problems(real, bits, movers, moved, outside, {})
|
||||||
|
tripped(any(p.startswith("UNDECIDED answer NEW_SHADER for UseProgram") for p in problems)
|
||||||
|
and any(p.startswith("UNDECIDED answer NEW_VERTEX_ELEMENTS for BindVertexArray")
|
||||||
|
for p in problems)
|
||||||
|
and len(undecided_rows) == 2,
|
||||||
|
"21 (the two P4a undecided marks are still needed)")
|
||||||
|
|
||||||
# THE POSITIVE CONTROLS. (a) The row that was wrong in round 3: SetPixelStoreParam writes
|
# THE POSITIVE CONTROLS. (a) The row that was wrong in round 3: SetPixelStoreParam writes
|
||||||
# NEW_PIXEL_PACK's shutter member sixteen times, through a token-pasting macro; it has
|
# NEW_PIXEL_PACK's shutter member sixteen times, through a token-pasting macro; it has
|
||||||
# to be SUPPORTED at field level. (b) The seven setters round 4's review named, which
|
# to be SUPPORTED at field level. (b) The seven setters round 4's review named, which
|
||||||
|
|||||||
Reference in New Issue
Block a user