mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
Compare commits
23
Commits
c20e2f2b67
...
712c946744
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
712c946744 | ||
|
|
8f1eaafa79 | ||
|
|
08922b6f78 | ||
|
|
711d5c61ba | ||
|
|
5ab90dec24 | ||
|
|
4299acd9c1 | ||
|
|
953d73f111 | ||
|
|
d01439d846 | ||
|
|
c9958075e8 | ||
|
|
ae1a1c503f | ||
|
|
a02f1571f5 | ||
|
|
dca3eb868e | ||
|
|
7408bad9fd | ||
|
|
e8502a6100 | ||
|
|
9ea44389e7 | ||
|
|
17db759891 | ||
|
|
2cb44039b5 | ||
|
|
32033d6993 | ||
|
|
08192d7266 | ||
|
|
37da3c3a07 | ||
|
|
fde5fda3b5 | ||
|
|
6515c8e6ae | ||
|
|
d54ec57a5d |
@@ -491,6 +491,12 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.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()
|
||||
|
||||
|
||||
+14
-5
@@ -322,16 +322,25 @@ namespace MobileGL::MG_Config {
|
||||
// 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
|
||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP3a), so MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-pull control and 0x7f (kMGPipeSubsystemsMigratedAtP2) is
|
||||
// the "P2 only" control P3a's A/B is run against. Accepts decimal or 0x-prefixed
|
||||
// hex, and operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h
|
||||
// owns them):
|
||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a), so MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-pull control and 0x1ff (kMGPipeSubsystemsMigratedAtP3a) is
|
||||
// the "everything before P4a" control P4a's A/B is run against - each phase's
|
||||
// constant survives as the next phase's control, which is why none of them is ever
|
||||
// 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)
|
||||
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
#include "Config.h"
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// For kMGPipeSubsystemsMigratedAtP3a, the push build's PipePush default (the P2 constant
|
||||
// beside it is the phase-by-phase control, not the default). Push-only, so the pull
|
||||
// build's translation unit is unchanged.
|
||||
// For kMGPipeSubsystemsMigratedAtP4a, the push build's PipePush default (the P2 and P3a
|
||||
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||
// pull build's translation unit is unchanged.
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#endif
|
||||
|
||||
@@ -252,9 +252,9 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP2 (0x7f) is the phase-by-phase control - P3a's two
|
||||
// subsystems off, everything P2 landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP3a);
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
|
||||
#else
|
||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||
// "pull everything" and that stays literally true.
|
||||
|
||||
@@ -688,7 +688,22 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
|
||||
// variable: no .cpp is needed for the definition.
|
||||
inline PipeInputs gPipeInputs{};
|
||||
//
|
||||
// LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for
|
||||
// pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply
|
||||
// leaks the global singletons to the OS instead of running destructors during static
|
||||
// teardown". This block breaks that rule if it is a value, because its O-class members
|
||||
// are SharedPtrs to FRONTEND objects: a VertexArrayObject that the application deleted
|
||||
// while it was bound has its last reference here, and destroying this block from
|
||||
// __run_exit_handlers therefore runs ~VertexArrayObject -> ~BufferObject at exit. Those
|
||||
// destructors are not exit-safe and cannot be made so - they reach the client's slot
|
||||
// allocator, the resource tracker, the vertex-input emitter, the applier AND, through
|
||||
// MGPipeApplyResourceDestroy, the backend's own twin tables, deferred-release queue,
|
||||
// buffer pool and driver entry points, every one of which is either already destroyed or
|
||||
// about to be. So the reference is never dropped: nothing here can start such a chain.
|
||||
// A live context releases these SharedPtrs the ordinary way, at the fill point.
|
||||
// (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.)
|
||||
inline PipeInputs& gPipeInputs = *new PipeInputs();
|
||||
|
||||
// Every field has storage or is forwarded, and nothing else.
|
||||
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
|
||||
|
||||
@@ -195,8 +195,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// when the pipeline version moved, and it keeps the eviction order in the same array as
|
||||
// the content - a map would need a second structure to answer "which is oldest".
|
||||
inline MGPipeCsoCache& MGPipeCsoCacheInstance() {
|
||||
static MGPipeCsoCache cache;
|
||||
return cache;
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||
// rule covers every MGPipe process singleton, not only the ones on today's death
|
||||
// paths.
|
||||
static MGPipeCsoCache* cache = new MGPipeCsoCache();
|
||||
return *cache;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/FramebufferEmit.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P4a's framebuffer family: set_framebuffer_state, emitted at the validate
|
||||
// point once per bound TARGET that moved, or once with Target = Both when the two bindings
|
||||
// name the same object.
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT, and the
|
||||
// split is the whole reason it exists this early. MG_Impl/Pipe/PipeFill.cpp is the contract
|
||||
// package's for the entire phase - it carries Coverage.def's enum-coupled block, the validate
|
||||
// point and the death helpers - so the emitter package must not edit it. What it edits instead
|
||||
// is this header: the emitter's BODY, and the value of kMGPipeWiredFramebufferSubsystem below.
|
||||
// That is what makes "no file is touched twice by two packages" structural rather than a
|
||||
// convention, and it is what the bb2a236d semantic-merge trap taught (two branches green
|
||||
// separately, the integrated tree not compiling).
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state: the root
|
||||
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
|
||||
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR, and it is 0 until the emitter below
|
||||
// has a body. PipeFill.cpp ORs the four per-family constants into kMGPipeWiredSubsystems,
|
||||
// so the bit is added by the commit that gives the emitters their bodies, with no file
|
||||
// touched twice - and a Coverage.def row can never silently drop a field on the floor
|
||||
// before the call that carries it exists.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
||||
|
||||
// set_framebuffer_state. STUB AT THE CONTRACT COMMIT: it emits nothing and returns 0
|
||||
// payload bytes, so the validate point's ladder has its final shape and the package that
|
||||
// fills this in never edits PipeFill.cpp.
|
||||
class MGPipeFramebufferEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A fresh context: what the server has is no longer what this emitter last sent. Only
|
||||
// LATCHES reset here - the applier's object records survive a make-current and
|
||||
// re-publishing them would move their serials for nothing.
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeFramebufferEmitter& MGPipeFramebufferEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||
// rule covers every MGPipe process singleton, not only the ones a frontend destructor
|
||||
// reaches today, and it is what keeps exit() out of a torn-down pipe.
|
||||
static MGPipeFramebufferEmitter* emitter = new MGPipeFramebufferEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -0,0 +1,62 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/ImageEmit.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of set_shader_images, the third of P4a's kVarTail unit sets. It rides
|
||||
// SamplerEmit.h's subsystem bit (kMGPipeWiredSamplerSubsystem): one family, one A/B.
|
||||
//
|
||||
// TWO INVARIANTS THAT MUST SURVIVE INTO THE BODY, and they are the kind an optimisation
|
||||
// deletes:
|
||||
// 1. THE HIGH-WATER-ZERO EARLY-OUT. An image high-water mark of 0 emits nothing, BEFORE any
|
||||
// hash - that is what makes every Minecraft draw pay one integer test for a feature it
|
||||
// does not use.
|
||||
// 2. THE SWEEP'S GATE IS KEYED ON FRONTEND GENERATIONS AND DELIBERATELY NOT ON A BACKEND
|
||||
// RE-MINT COUNTER. A texture bound ONLY to an image unit is re-minted INSIDE the sweep,
|
||||
// so a server-side epoch would be bumped after the gate had already declined. The
|
||||
// client's bit-14 shutter is Mix(Mix(textureContent, textureParams), programImageUnitVersion)
|
||||
// - all three FRONTEND counters - so the property is preserved by construction, and it is
|
||||
// written here because it is invisible from the shutter itself.
|
||||
//
|
||||
// The record carries the APPLICATION's format and access; the bind-format recast (a GL_RG32F
|
||||
// bind is INVALID_VALUE on 19 of 26 non-core formats on Adreno) and the buffer-texture split
|
||||
// view stay SERVER-side and unchanged. ContentHash therefore has to cover InternalFormat and
|
||||
// Access as well as the binding, because the format the shader was built against is live
|
||||
// glBindImageTexture state and the format-less image bake keys on it.
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeImageEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeImageEmitter& MGPipeImageEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||
// intentionally leaked at exit, like every other MGPipe process singleton.
|
||||
static MGPipeImageEmitter* emitter = new MGPipeImageEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -19,9 +19,19 @@
|
||||
#include <MG_State/GLState/StateObjectDeathNotice.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/CsoCache.h>
|
||||
// P4a's five client emitters. This translation unit is the ONLY one that includes them in the
|
||||
// library, exactly as it is for Tracker.h, CsoCache.h, ResourceTracker.h and VertexInputEmit.h
|
||||
// - all of them header-only for the same ownership reason. Each carries its family's
|
||||
// kMGPipeWired*Subsystem constant, so the bit that switches a family on is added by the commit
|
||||
// that gives that family's emitters their bodies, and no two packages ever edit one file.
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#include <MG_Impl/Pipe/ImageEmit.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/ResourceTracker.h>
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
@@ -363,8 +373,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// COMPARE-AT-READ in every accessor (the stored value against a fresh read of the
|
||||
// live context at the moment the backend reads it - the arm that is real in P1: it
|
||||
// catches a value that changed between the verb boundary and the read).
|
||||
PipeInputs g_snapshot{}; // the second arm
|
||||
PipeInputs g_readScratch{}; // where the compare-at-read re-read lands
|
||||
// LEAK-AT-EXIT STORAGE, for gPipeInputs' reason (MG_Backend/MGPipe/PipeInputs.h): a
|
||||
// PipeInputs holds SharedPtrs to frontend objects in its O class, and these two are
|
||||
// filled from the live context, so either can hold the LAST reference to a
|
||||
// VertexArrayObject or a ProgramObject. Destroying them from __run_exit_handlers
|
||||
// would run ~VertexArrayObject / ~BufferObject at exit, into a pipe and a backend
|
||||
// that are already being torn down. References so the ~50 uses below need no edit.
|
||||
PipeInputs& g_snapshot = *new PipeInputs(); // the second arm
|
||||
PipeInputs& g_readScratch = *new PipeInputs(); // where the compare-at-read re-read lands
|
||||
|
||||
// The read hook arms at the first fill (ArmVerify below), so it cannot see a read
|
||||
// made before that. That window is covered by the poison instead: MGP_INPUT_CHECK
|
||||
@@ -843,6 +859,434 @@ namespace MobileGL::MG_Pipe {
|
||||
return published;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// P4a: the BIRTH half - the gate, the four mints, the publication latch and the seam
|
||||
// ================================================================================
|
||||
//
|
||||
// Declared in MG_Pipe/PipeMutation.h, which is the one door MG_State has into the client
|
||||
// (the closure gate's mutation-header probe keeps it a declaration), and defined here for
|
||||
// the reason every other client-side emission point is: this file is package A's for the
|
||||
// whole phase, so the gate is written ONCE and the packages that own the emitters never
|
||||
// edit it.
|
||||
namespace {
|
||||
using MG_State::GLState::FramebufferObject;
|
||||
using MG_State::GLState::ITextureObject;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::SamplerObject;
|
||||
|
||||
// THE SAME PAIR `wants()` APPLIES TO EVERY EMISSION at the validate point, and it is
|
||||
// deliberately the same predicate rather than a second copy of it: the operator's
|
||||
// per-subsystem A/B bit in MOBILEGL_PIPE_PUSH, AND this build having WIRED the family
|
||||
// at all. The second half is the family's own kMGPipeWired*Subsystem constant, which
|
||||
// lives in the family's emit header and is 0 until the commit that gives the emitter
|
||||
// its body - so a client path that lands before its emitter does is inert by
|
||||
// construction rather than by everyone remembering to check.
|
||||
Bool FamilyIsLive(Uint64 subsystem, Uint64 wired) {
|
||||
return (MG_Config::Features.PipePush & subsystem) != 0 && (wired & subsystem) != 0;
|
||||
}
|
||||
|
||||
// ---- THE FAMILY SEAM ----
|
||||
//
|
||||
// The forwarding from a birth hook to its family's emitter has to be written HERE,
|
||||
// once, against an emitter whose entry point does not exist yet: A owns this file for
|
||||
// the whole phase and B/C own the five emit headers, and neither may edit the other's.
|
||||
// A plain call would not compile against the stub emitter and a runtime `if` would not
|
||||
// link. So the call is made from a TEMPLATE whose `if constexpr` condition is the
|
||||
// family's own wired constant, passed as a template ARGUMENT so the condition is
|
||||
// value-dependent: while the constant is 0 the statement is discarded and never
|
||||
// instantiated, so this tree compiles against the stubs; the moment a family sets its
|
||||
// constant the statement instantiates and a missing or misspelled entry point is a
|
||||
// COMPILE ERROR in that family's own commit rather than a surprise at the merge. That
|
||||
// is the same property the four `kMGPipeWired*Subsystem == 0 || == its own bit`
|
||||
// asserts below give, one level further in.
|
||||
//
|
||||
// `call` must be a GENERIC lambda - `[&](auto& emitter) { ... }` - so its body is
|
||||
// checked at instantiation and not at definition. A non-generic one would be checked
|
||||
// here and would defeat the whole seam.
|
||||
template <Uint64 kWired, class Emitter, class Fn>
|
||||
constexpr void ForwardWhenWired(Emitter& emitter, Fn&& call) {
|
||||
if constexpr (kWired != 0) {
|
||||
call(emitter);
|
||||
} else {
|
||||
(void)emitter;
|
||||
(void)call;
|
||||
}
|
||||
}
|
||||
|
||||
// THE SEAM'S POSITIVE CONTROL, and it is not decoration: every use of it in this tree
|
||||
// passes a constant that is 0, so the TAKEN arm is never instantiated here and a seam
|
||||
// that failed to compile or failed to call would be discovered by package B or C
|
||||
// rather than by the commit that wrote it. This drives both arms against a probe
|
||||
// emitter shaped like the ones the emit headers will carry, and asserts that exactly
|
||||
// one call happened - so "discarded when 0, called when set" is a checked property of
|
||||
// this build rather than a claim in the paragraph above.
|
||||
struct SeamProbeEmitter {
|
||||
Uint32 Calls = 0;
|
||||
constexpr void Probe() { ++Calls; }
|
||||
};
|
||||
|
||||
constexpr Bool SeamForwardsExactlyWhenWired() {
|
||||
SeamProbeEmitter probe{};
|
||||
ForwardWhenWired<1ull>(probe, [](auto& emitter) { emitter.Probe(); });
|
||||
ForwardWhenWired<0ull>(probe, [](auto& emitter) { emitter.Probe(); });
|
||||
return probe.Calls == 1;
|
||||
}
|
||||
|
||||
static_assert(SeamForwardsExactlyWhenWired(),
|
||||
"the family seam must forward exactly when its wired constant is non-zero");
|
||||
|
||||
// ---- THE PUBLICATION LATCH (D-I1) ----
|
||||
//
|
||||
// "Did a create for exactly this handle actually go out?" - asked by the six death
|
||||
// helpers below and answered by whatever emitted the create. It exists because the
|
||||
// create is gated at its call site and the destroy inside the helper, so the two ask
|
||||
// the same question at two different moments; and because 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 anything, 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 asserts on in a verify build.
|
||||
//
|
||||
// KEYED BY {kind, slot, gen}, so a recycled slot cannot inherit its predecessor's
|
||||
// answer - the same reason the identity carries a generation at all.
|
||||
//
|
||||
// THE ShaderCso COMPOSITE BAND GETS A TABLE OF ITS OWN, exactly as the allocator's
|
||||
// does and for the same arithmetic: the band's base is 983040, so a single composite
|
||||
// in a slot-indexed vector would allocate ~983k entries. Anything that indexes a
|
||||
// ShaderCso slot must test MGPipeIsCompositeShaderSlot(slot) FIRST; this is the
|
||||
// client-side worked example of that rule.
|
||||
class MGPipePublicationLatch {
|
||||
public:
|
||||
void NotePublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
Entry* entry = Grow(kind, handle.Slot);
|
||||
if (entry == nullptr) return;
|
||||
entry->Gen = handle.Gen;
|
||||
entry->Published = true;
|
||||
}
|
||||
|
||||
Bool IsPublished(MGPipeKind kind, MGPipeHandle handle) const {
|
||||
const Entry* entry = Find(kind, handle.Slot);
|
||||
return entry != nullptr && entry->Published && entry->Gen == handle.Gen;
|
||||
}
|
||||
|
||||
void NoteUnpublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
Entry* entry = const_cast<Entry*>(Find(kind, handle.Slot));
|
||||
if (entry == nullptr || entry->Gen != handle.Gen) return;
|
||||
*entry = Entry{};
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Uint32 Gen = 0;
|
||||
Bool Published = false;
|
||||
};
|
||||
|
||||
static constexpr SizeT kKindCount = static_cast<SizeT>(MGPipeKind::KindCount);
|
||||
|
||||
Bool IsBand(MGPipeKind kind, Uint32 slot) const {
|
||||
return kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(slot);
|
||||
}
|
||||
|
||||
Entry* Grow(MGPipeKind kind, Uint32 slot) {
|
||||
const SizeT index = static_cast<SizeT>(kind);
|
||||
if (index >= kKindCount) return nullptr;
|
||||
if (IsBand(kind, slot)) {
|
||||
const SizeT banded = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
if (banded >= m_band.size()) m_band.resize(banded + 1);
|
||||
return &m_band[banded];
|
||||
}
|
||||
Vector<Entry>& table = m_kinds[index];
|
||||
if (slot >= table.size()) table.resize(static_cast<SizeT>(slot) + 1);
|
||||
return &table[slot];
|
||||
}
|
||||
|
||||
const Entry* Find(MGPipeKind kind, Uint32 slot) const {
|
||||
const SizeT index = static_cast<SizeT>(kind);
|
||||
if (index >= kKindCount) return nullptr;
|
||||
if (IsBand(kind, slot)) {
|
||||
const SizeT banded = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
return banded < m_band.size() ? &m_band[banded] : nullptr;
|
||||
}
|
||||
const Vector<Entry>& table = m_kinds[index];
|
||||
return slot < table.size() ? &table[slot] : nullptr;
|
||||
}
|
||||
|
||||
Array<Vector<Entry>, kKindCount> m_kinds{};
|
||||
Vector<Entry> m_band{};
|
||||
};
|
||||
|
||||
MGPipePublicationLatch& PublicationLatch() {
|
||||
// NEVER DESTROYED, for MGPipeSlots()' reason: the six death helpers reach this
|
||||
// from frontend destructors that __run_exit_handlers drives AFTER a function-local
|
||||
// static would have gone, and a destroyed latch answers out of freed vectors.
|
||||
static MGPipePublicationLatch* latch = new MGPipePublicationLatch();
|
||||
return *latch;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
PublicationLatch().NotePublished(kind, handle);
|
||||
}
|
||||
|
||||
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return false;
|
||||
return PublicationLatch().IsPublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
PublicationLatch().NoteUnpublished(kind, handle);
|
||||
}
|
||||
|
||||
void MGPipeMintTextureHandle(ITextureObject& texture) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Texture, texture.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintRenderbufferHandle(RenderbufferObject& renderbuffer) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Renderbuffer, renderbuffer.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintFramebufferHandle(FramebufferObject& framebuffer) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::Framebuffer, framebuffer.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeMintShaderCsoHandle(ProgramObject& program) {
|
||||
MGPipeSlots().Acquire(MGPipeKind::ShaderCso, program.GetLifetimeId());
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureResourceCreate(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceCreate(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceRespecify(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitTextureParams(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitTextureParams(texture); });
|
||||
}
|
||||
|
||||
void MGPipeNoteTextureLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.NoteLevelDirty(storageOwner, uploadTarget, level); });
|
||||
}
|
||||
|
||||
void MGPipeEmitRenderbufferResourceCreate(RenderbufferObject& renderbuffer) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.EmitRenderbufferCreate(renderbuffer); });
|
||||
}
|
||||
|
||||
void MGPipeEmitRenderbufferResourceRespecify(RenderbufferObject& renderbuffer) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(),
|
||||
[&](auto& emitter) { emitter.EmitRenderbufferRespecify(renderbuffer); });
|
||||
}
|
||||
|
||||
void MGPipeEmitSamplerCsoCreate(SamplerObject& sampler) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerCso(sampler); });
|
||||
}
|
||||
|
||||
void MGPipeEmitSamplerViewCreate(ITextureObject& texture) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.EmitSamplerView(texture); });
|
||||
}
|
||||
|
||||
void MGPipeEmitShaderCsoCreate(ProgramObject& program) {
|
||||
if (!FamilyIsLive(kMGPipeSubsystemPrograms, kMGPipeWiredProgramSubsystem)) return;
|
||||
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.EmitShaderCso(program); });
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
// 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 PUBLICATION LATCH above is
|
||||
// asked before any delete goes out, and it is the SAME latch whatever emitted the create
|
||||
// wrote - one answer per {kind, slot, gen}, not a second reading of a live predicate.
|
||||
//
|
||||
// THE LATCH RATHER THAN A PER-EMITTER RecordIsPublished(handle), deliberately, and it is
|
||||
// the one place P4a's shape differs from P3a's: P3a had one kind and one emitter, so the
|
||||
// emitter could hold the latch. P4a has six kinds behind FOUR emitters and one kind -
|
||||
// SamplerViewCso - with no frontend object at all, and a ShaderCso whose composite band
|
||||
// has two independent release paths. A latch this file owns is then the only thing all
|
||||
// six can read, and it keeps the answer out of the emit headers B and C are writing.
|
||||
//
|
||||
// AT THE CONTRACT COMMIT nothing latches a publication, because every family emitter is a
|
||||
// stub, 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);
|
||||
}
|
||||
|
||||
MGPHandleOnly HandleOnly(MGPipeKind kind, MGPipeHandle handle) {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(kind);
|
||||
return only;
|
||||
}
|
||||
|
||||
// Step 1, shared: the wire delete goes out FIRST and only for a PUBLISHED handle, and
|
||||
// the latch is cleared with it so a second death path - a composite's two, a backend's
|
||||
// redundant notice - cannot emit a second delete for a record that is already gone.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle, void (*apply)(const MGPHandleOnly&)) {
|
||||
if (!MGPipeHandleIsPublished(kind, handle)) return false;
|
||||
apply(HandleOnly(kind, handle));
|
||||
MGPipeNoteHandleUnpublished(kind, handle);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
|
||||
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
|
||||
// per kind), MGPipeKind has SamplerViewCso, and the view IS keyed in that kind's
|
||||
// ByLifetimeId map under the texture's id - which is exactly what the FindByLifetimeId
|
||||
// above just resolved. "It has no frontend object of its own" is why it takes the
|
||||
// lifetime id; it is not a reason to drop step 2. A backend that holds a twin per
|
||||
// SamplerViewCso slot - which is the shape both backends' slot tables take - would
|
||||
// otherwise never be told to drop it, and under a backend with no other per-kind free
|
||||
// path never drop it at all: the C-1 leak, one kind later, and invisible to
|
||||
// PipeSlotPeek because the SLOT was returned correctly.
|
||||
NotifyAndFree(MGPipeKind::SamplerViewCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||
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). Its release therefore rides
|
||||
// ~SamplerObject and MGPipeEmitSamplerCsoDestroyAndFree below - the same helper, the
|
||||
// same three-step order, idempotent.
|
||||
//
|
||||
// WHEN that runs is NOT ordered against this body and nothing here may assume it is.
|
||||
// m_sampler is a SharedPtr, so a texture unit slot or a sampler-view resolution that
|
||||
// took a reference delays ~SamplerObject arbitrarily; "a member's destructor follows
|
||||
// its owner's body" would be true of a by-value member and is not true of this one.
|
||||
// The conclusion above does not depend on the timing - the two ids are different, so
|
||||
// the two releases are independent whichever order they happen in - but a package must
|
||||
// not build an ordering on it.
|
||||
//
|
||||
// AND THE VIEW'S ANSWER IS OR-ED IN, not dropped: a texture whose ResourceDestroy was
|
||||
// suppressed (nothing ever published it) but whose DeleteSamplerView did go out has
|
||||
// already spoken on the wire for this object, and reporting false would run the legacy
|
||||
// path for both halves.
|
||||
const Bool viewPublished = MGPipeEmitSamplerViewCsoDestroyAndFree(lifetimeId);
|
||||
return published || viewPublished;
|
||||
}
|
||||
|
||||
Bool MGPipeEmitRenderbufferDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||
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.
|
||||
//
|
||||
// NOTHING EVER TAKES THE PUBLICATION LATCH FOR THIS KIND, by contract and not by
|
||||
// omission: with no create there is nothing to latch, and with no delete there is
|
||||
// nothing for a latch to gate. The answer is therefore the literal false rather than a
|
||||
// latch read, and false is the right one - it means "the legacy path still owes
|
||||
// whatever it owed", which for a framebuffer is the death notice this just raised.
|
||||
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 =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||
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 =
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
return published;
|
||||
}
|
||||
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||
if (verb == nullptr || field == nullptr) {
|
||||
g_omission = PoisonOmission{};
|
||||
@@ -964,6 +1408,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// causes them rather than filled into a PipeInputs field.
|
||||
case MGPipeFieldEmitter::BindVertexElements:
|
||||
return kMGPipeSubsystemVertexInput;
|
||||
// P4a's six emitted rows, across three of its four subsystems. The fourth,
|
||||
// kMGPipeSubsystemTextureResources, names NO emitted field and cannot: the texture
|
||||
// and renderbuffer resource_* calls and set_texture_params are dispatched at the
|
||||
// GL call that causes them rather than filled into a PipeInputs field, exactly as
|
||||
// P3a's buffer family is, so there is no Coverage.def emitted row for them and
|
||||
// there must not be one.
|
||||
case MGPipeFieldEmitter::SetFramebufferState:
|
||||
return kMGPipeSubsystemFramebuffer;
|
||||
case MGPipeFieldEmitter::SetSamplerViews:
|
||||
case MGPipeFieldEmitter::SetShaderImages:
|
||||
return kMGPipeSubsystemSamplers;
|
||||
case MGPipeFieldEmitter::SetDrawProgram:
|
||||
case MGPipeFieldEmitter::SetDispatchProgram:
|
||||
return kMGPipeSubsystemPrograms;
|
||||
case MGPipeFieldEmitter::kNone:
|
||||
break;
|
||||
}
|
||||
@@ -1032,6 +1490,72 @@ namespace MobileGL::MG_Pipe {
|
||||
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||
"the MGPipe vertex-attribute capacity and the frontend's have drifted");
|
||||
|
||||
// ---- P4a's SEVEN pairings, and EVERY ONE OF THEM COMPARES AGAINST
|
||||
// SubsystemForEmitter RATHER THAN AGAINST A CONSTANT. That is the lesson written out
|
||||
// twenty lines above and it is not a style preference: naming the subsystem constant
|
||||
// directly pins the dirty half to a constant instead of pinning the two MAPS to each
|
||||
// other, so an emitter row moved onto another subsystem would still satisfy the
|
||||
// assertion while the emission gate and the residual-fill skip had begun to disagree.
|
||||
//
|
||||
// One emitter row stands for each family: set_framebuffer_state for the framebuffer,
|
||||
// set_sampler_views for the sampler family (set_shader_images is the same subsystem
|
||||
// and is pinned to it below), and set_draw_program for the program family.
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewFramebuffer) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetFramebufferState),
|
||||
"set_framebuffer_state and NEW_FRAMEBUFFER must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplerViews) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetSamplerViews),
|
||||
"set_sampler_views and NEW_SAMPLER_VIEWS must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplers) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetSamplerViews),
|
||||
"bind_sampler_states and NEW_SAMPLERS must name the sampler subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderImages) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetShaderImages),
|
||||
"set_shader_images and NEW_SHADER_IMAGES must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShader) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram),
|
||||
"create/bind_shader_state and NEW_SHADER must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBindings) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram),
|
||||
"the program family and NEW_SHADER_BINDINGS must name one subsystem");
|
||||
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewGlobalConstants) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDispatchProgram),
|
||||
"set_global_constants and NEW_GLOBAL_CONSTANTS must name one subsystem");
|
||||
// And the two program emitters really are one subsystem, which is what makes the two
|
||||
// assertions above a statement about the family rather than about one call.
|
||||
static_assert(SubsystemForEmitter(MGPipeFieldEmitter::SetDrawProgram) ==
|
||||
SubsystemForEmitter(MGPipeFieldEmitter::SetDispatchProgram),
|
||||
"set_draw_program and set_dispatch_program are one family and one A/B");
|
||||
|
||||
// THE TEXTURE-RESOURCE SUBSYSTEM HAS NO DIRTY BIT, and that has to be asserted rather
|
||||
// than left as an absence: its calls are dispatched from the GL entry points that
|
||||
// cause them, so a bit that started naming it would gate the emission twice - once at
|
||||
// the dispatch site and once in the walk - and the two would disagree the first time
|
||||
// one of them was edited. Exactly the shape NoDirtyBitOwnsTheResidualSubsystem uses.
|
||||
constexpr Bool NoDirtyBitOwnsTheTextureResourceSubsystem() {
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||
if (MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(i)) ==
|
||||
kMGPipeSubsystemTextureResources) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(NoDirtyBitOwnsTheTextureResourceSubsystem(),
|
||||
"a MGPipeDirty bit now owns kMGPipeSubsystemTextureResources: the texture "
|
||||
"and renderbuffer resource_* calls are dispatched at the GL call that "
|
||||
"causes them, so a dirty bit would gate them a second time");
|
||||
|
||||
// The two texture-unit capacities are one number on both sides of the boundary, and
|
||||
// this is the one translation unit that sees the frontend constant and the MG_Pipe
|
||||
// one - the same pinning kMGPipeMaxVertexAttribs gets, for the same reason.
|
||||
static_assert(kMGPipeMaxTextureUnits ==
|
||||
static_cast<Uint32>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS),
|
||||
"the MGPipe texture-unit capacity and the frontend's have drifted");
|
||||
static_assert(kMGPipeMaxImageUnits ==
|
||||
static_cast<Uint32>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS),
|
||||
"the MGPipe image-unit capacity and the frontend's have drifted");
|
||||
|
||||
// Which of those subsystems THIS BUILD actually emits for. It grows one commit at a
|
||||
// time, and a field whose emitter is not wired here keeps being pulled - so adding a
|
||||
// row to Coverage.def can never silently drop a field on the floor before the call
|
||||
@@ -1062,12 +1586,43 @@ namespace MobileGL::MG_Pipe {
|
||||
// and every one of those calls lands in RefusedResourceCalls. Bit 7 without bit 8 is
|
||||
// fine. Neither the P3a default (0x1ff, both on) nor G12's control (0x7f, both off)
|
||||
// is in that arm, which is why nothing in the phase trips over it.
|
||||
// P4a's FOUR ARE NOT WRITTEN HERE AT ALL, and that is the structural half of the
|
||||
// ownership rule rather than a stylistic choice. This file is the contract package's
|
||||
// for the entire phase: it carries Coverage.def's enum-coupled switch, the validate
|
||||
// point and the death helpers, so the packages that fill the emitters in must never
|
||||
// edit it - which is exactly the merge trap that produced a push and verify build that
|
||||
// did not compile on the integrated tree while both branches were green apart. So each
|
||||
// family's bit is the value of a constant DEFINED IN THAT FAMILY'S OWN EMIT HEADER,
|
||||
// initialised to 0 there and set to the subsystem constant by the commit that gives
|
||||
// those emitters their bodies. A mistake is then a compile error at the contract
|
||||
// commit, not at the merge, and no file is touched twice.
|
||||
//
|
||||
// The sampler bit covers SamplerEmit.h AND ImageEmit.h: one family, one A/B.
|
||||
constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState |
|
||||
kMGPipeSubsystemPixelPack |
|
||||
kMGPipeSubsystemPatchState |
|
||||
kMGPipeSubsystemVertexAttribDefaults |
|
||||
kMGPipeSubsystemResources |
|
||||
kMGPipeSubsystemVertexInput;
|
||||
kMGPipeSubsystemVertexInput |
|
||||
kMGPipeWiredFramebufferSubsystem |
|
||||
kMGPipeWiredTextureSubsystem |
|
||||
kMGPipeWiredSamplerSubsystem |
|
||||
kMGPipeWiredProgramSubsystem;
|
||||
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
|
||||
// this a header that set the wrong constant - the sampler bit in the program header,
|
||||
// say - would switch the wrong family on and every gate would still pass.
|
||||
static_assert(kMGPipeWiredFramebufferSubsystem == 0 ||
|
||||
kMGPipeWiredFramebufferSubsystem == kMGPipeSubsystemFramebuffer,
|
||||
"FramebufferEmit.h's wired constant must be 0 or the framebuffer bit");
|
||||
static_assert(kMGPipeWiredTextureSubsystem == 0 ||
|
||||
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources,
|
||||
"TextureEmit.h's wired constant must be 0 or the texture-resource bit");
|
||||
static_assert(kMGPipeWiredSamplerSubsystem == 0 ||
|
||||
kMGPipeWiredSamplerSubsystem == kMGPipeSubsystemSamplers,
|
||||
"SamplerEmit.h's wired constant must be 0 or the sampler bit");
|
||||
static_assert(kMGPipeWiredProgramSubsystem == 0 ||
|
||||
kMGPipeWiredProgramSubsystem == kMGPipeSubsystemPrograms,
|
||||
"ProgramEmit.h's wired constant must be 0 or the program bit");
|
||||
|
||||
// A field an emitted call supplies COMPLETELY, so the residual fill may stop pulling
|
||||
// it. Two rows of Coverage.def's emitted list do not qualify and each has its reason
|
||||
@@ -1107,11 +1662,36 @@ namespace MobileGL::MG_Pipe {
|
||||
// coming through the residual fill because the mirror is a pointer only the
|
||||
// client can hold. What retires the pull is not a better applier - it is P8,
|
||||
// where the backend stops reading a frontend VAO at all.
|
||||
// P4a's SIX ROWS ARE ALL FALSE, and five of them for GetBoundVertexArray's exact
|
||||
// reason: the field's storage is a frontend heap reference - a
|
||||
// BindingSlot<FramebufferObject>, an ImageTextureBinding, a TextureUnit, two
|
||||
// SharedPtr<ProgramObject> - and the calls that supply them carry eight-byte
|
||||
// {slot, gen} handles and fully resolved descriptors. The applier has no way to
|
||||
// produce a pointer and P4a deliberately does not give it one: a payload never
|
||||
// contains a pointer, and the whole point of the conversion is that the server
|
||||
// stops holding frontend references. Skipping the pull would leave those mirrors
|
||||
// null on every draw of every push build. What retires them is not a better
|
||||
// applier, it is the phase where the backend stops reading a frontend object.
|
||||
//
|
||||
// GetMaxTouchedTextureUnit is the sixth and its argument is different, which is
|
||||
// why it is written out: it is a plain Int, and set_sampler_views' Count IS that
|
||||
// value plus one. But the set is SUPPRESSED on an unchanged content hash and is
|
||||
// emitted only when NEW_SAMPLER_VIEWS fires, and that bit's shutter -
|
||||
// Mix(textureContent, GetTextureBindGeneration()) - does NOT move on a redundant
|
||||
// re-bind of the object a unit already holds, while the high-water mark DOES. So
|
||||
// the applier's Count can lag the frontend's mark by exactly the case the
|
||||
// suppressor exists to swallow, and the field keeps being pulled.
|
||||
constexpr Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field) {
|
||||
switch (field) {
|
||||
case MGPipeInputField::GetPixelStoreParameters:
|
||||
case MGPipeInputField::GetCurrentVertexAttribute:
|
||||
case MGPipeInputField::GetBoundVertexArray:
|
||||
case MGPipeInputField::GetFramebufferBindingSlot:
|
||||
case MGPipeInputField::GetImageTextureBinding:
|
||||
case MGPipeInputField::GetTextureUnitObject:
|
||||
case MGPipeInputField::GetProgramForDraw:
|
||||
case MGPipeInputField::GetProgramForDispatch:
|
||||
case MGPipeInputField::GetMaxTouchedTextureUnit:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
@@ -1163,7 +1743,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// Widening the probe to all 29 would re-implement the derivation to check it.
|
||||
Bool ApplierDerivesRenderStateFields() {
|
||||
static const Bool answer = [] {
|
||||
static PipeInputs probe;
|
||||
// Leak-at-exit, for gPipeInputs' reason: a PipeInputs is never destroyed by
|
||||
// an exit handler. This one only ever carries render state, but the rule is
|
||||
// stated over the TYPE rather than over each instance's current contents -
|
||||
// an instance that grows an O-class write later must not become the next
|
||||
// exit-time chain starter.
|
||||
static PipeInputs& probe = *new PipeInputs();
|
||||
constexpr Uint32 kSentinel = 0x5a5a5a5au;
|
||||
MGPipeFillAccess::RenderStateOf(probe).ClearStencil = kSentinel;
|
||||
MGPipeFillAccess::ClearStencilOf(probe) = 0u;
|
||||
@@ -1485,6 +2070,53 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 EmitIndexBuffer(GLContext& ctx) {
|
||||
return MGPipeVertexInputEmitterInstance().EmitIndexBuffer(ctx);
|
||||
}
|
||||
|
||||
// ---- P4a's seven emitters (D-C, D-D, D-F, D-G, D-H) ----
|
||||
//
|
||||
// THE SHAPE IS THE CONTRACT COMMIT'S, exactly as P3a's three were: seven adapters
|
||||
// whose bodies live in the five family headers, so the commits that fill those
|
||||
// emitters in never touch this file. Every one of them returns 0 today.
|
||||
//
|
||||
// THE ORDER IS ARCHITECTURE.md 5.4's RECOMMENDED ONE - framebuffer, then program, then
|
||||
// textures/sampler/image/global constants - and that document is explicit that the
|
||||
// order is code organisation and NOT a contract: all of a verb's set_*/bind_* must
|
||||
// complete before the verb, and apart from "a resource create precedes a bind to it"
|
||||
// there is no ordering requirement between them. The server specialises the shader and
|
||||
// the pipeline lazily at the verb, from everything it holds at that moment, which is
|
||||
// what makes deriving the fragColor broadcast count from the framebuffer record legal
|
||||
// at the verb rather than at the FBO sync.
|
||||
Uint64 EmitFramebufferState(GLContext& ctx) {
|
||||
return MGPipeFramebufferEmitterInstance().EmitFramebufferState(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitShaderState(GLContext& ctx) {
|
||||
return MGPipeProgramEmitterInstance().EmitShaderState(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||
return MGPipeProgramEmitterInstance().EmitGlobalConstants(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitSamplerViews(GLContext& ctx) {
|
||||
return MGPipeSamplerEmitterInstance().EmitSamplerViews(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitSamplerStates(GLContext& ctx) {
|
||||
return MGPipeSamplerEmitterInstance().EmitSamplerStates(ctx);
|
||||
}
|
||||
|
||||
Uint64 EmitShaderImages(GLContext& ctx) {
|
||||
return MGPipeImageEmitterInstance().EmitShaderImages(ctx);
|
||||
}
|
||||
|
||||
// The texture sub-data DRAIN, and it is the one P4a emitter with no dirty bit over it.
|
||||
// Its calls are dispatched from the GL entry points that cause them (a constructor, a
|
||||
// storage definition, a glTexParameter) and the only thing that has to wait for the
|
||||
// validate point is the accumulated upload, so the gate is the subsystem bit alone.
|
||||
// With nothing dirty the drain list is empty and this is one test.
|
||||
Uint64 DrainTextureSubData(GLContext& ctx) {
|
||||
return MGPipeTextureEmitterInstance().DrainTextureSubData(ctx);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint64 MGPipeVertexAttribDefaultRepairCount() { return g_attribDefaultRepairs; }
|
||||
@@ -1538,10 +2170,22 @@ namespace MobileGL::MG_Pipe {
|
||||
// to the runtime subsystem that owns it. Naming the subsystem constants here instead
|
||||
// would be a second copy of that map in the only path that runs, and mis-gating a bit
|
||||
// in it would pass every test the map has.
|
||||
//
|
||||
// FOUR CONDITIONS, AND THE WIRED MASK IS ONE OF THEM. `kMGPipeWiredSubsystems` is the
|
||||
// OR of the per-family constants each emit header defines, and the whole ownership
|
||||
// design rests on it MEANING what the headers, this file and the result files all say
|
||||
// it means: an emitter runs only once the commit that gave it a body set its family's
|
||||
// constant. Without this condition a family whose header still says 0 would be CALLED
|
||||
// at every verb whose bit fires under the shipped default mask, so the commit that
|
||||
// lands the body would go live one commit early and every gate run in between would
|
||||
// measure an arm nobody thinks is on - and the mirror error is worse: a family that
|
||||
// lands its body and forgets the constant would emit nothing and look broken. The
|
||||
// P2/P3a bits are all in the mask, so nothing that emits today changes.
|
||||
const Uint64 pushMask = MG_Config::Features.PipePush;
|
||||
const auto wants = [&](MGPipeDirty bit) {
|
||||
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
|
||||
return subsystem != 0 && (pushMask & subsystem) != 0 &&
|
||||
(kMGPipeWiredSubsystems & subsystem) != 0 &&
|
||||
(dirty & MGPipeDirtyBit(bit)) != 0;
|
||||
};
|
||||
Uint64 payloadBytes = 0;
|
||||
@@ -1570,9 +2214,61 @@ namespace MobileGL::MG_Pipe {
|
||||
// is not. The resource tracker is deliberately NOT reset here for the same
|
||||
// reason its records survive: see ResourceTracker.h's ResetForTest.
|
||||
MGPipeVertexInputEmitterInstance().Reset();
|
||||
// P4a's five, and ONLY their latches: MGPipeApplierReset clears the framebuffer
|
||||
// records, the three unit sets and the three program handles, so the emitters'
|
||||
// mirrors of those must go with them or the first emission after a make-current
|
||||
// would be suppressed as unchanged and the server would draw with the previous
|
||||
// context's bindings. What must NOT reset is the RECORD half - the applier keeps
|
||||
// its texture, sampler, view and shader-CSO records across a make-current, because
|
||||
// a GL object lives in a share group, and re-publishing one would move its Serial
|
||||
// for nothing.
|
||||
MGPipeFramebufferEmitterInstance().Reset();
|
||||
MGPipeTextureEmitterInstance().Reset();
|
||||
MGPipeSamplerEmitterInstance().Reset();
|
||||
MGPipeImageEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
g_residualDue = true;
|
||||
}
|
||||
|
||||
// P4a's segment, in ARCHITECTURE.md 5.4's RECOMMENDED order - framebuffer, then
|
||||
// program, then textures / sampler / image / global constants - which is why it stands
|
||||
// before the render-state block rather than after it. That order is explicitly code
|
||||
// organisation and not a contract (all of a verb's set_*/bind_* complete before the
|
||||
// verb, and the server specialises lazily AT the verb from everything it then holds),
|
||||
// so nothing about the P2 and P3a emissions changes by standing after it; what it buys
|
||||
// is that the file reads in the order the design states.
|
||||
//
|
||||
// ALL SEVEN ARE STUBS AT THE CONTRACT COMMIT and all four family bits are absent from
|
||||
// kMGPipeWiredSubsystems, so `wants()` is false for every one of them - it tests that
|
||||
// mask as its third condition, which is what makes the sentence true rather than
|
||||
// merely intended - and this whole block is dead until the packages that own the
|
||||
// emitters land. Placing it here, once, is what keeps those packages out of this file.
|
||||
if (wants(MGPipeDirty::NewFramebuffer)) {
|
||||
payloadBytes += EmitFramebufferState(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewShader) || wants(MGPipeDirty::NewShaderBindings)) {
|
||||
payloadBytes += EmitShaderState(*ctx);
|
||||
}
|
||||
// The texture drain has no dirty bit over it (see its definition); it is gated on the
|
||||
// subsystem bit and on this build having wired the family at all, which is the same
|
||||
// pair `wants()` applies to every other emission.
|
||||
if ((pushMask & kMGPipeSubsystemTextureResources) != 0 &&
|
||||
(kMGPipeWiredSubsystems & kMGPipeSubsystemTextureResources) != 0) {
|
||||
payloadBytes += DrainTextureSubData(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewSamplerViews)) {
|
||||
payloadBytes += EmitSamplerViews(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewSamplers)) {
|
||||
payloadBytes += EmitSamplerStates(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewShaderImages)) {
|
||||
payloadBytes += EmitShaderImages(*ctx);
|
||||
}
|
||||
if (wants(MGPipeDirty::NewGlobalConstants)) {
|
||||
payloadBytes += EmitGlobalConstants(*ctx);
|
||||
}
|
||||
|
||||
if (wants(MGPipeDirty::NewPipelineState) || wants(MGPipeDirty::NewRenderState)) {
|
||||
payloadBytes += EmitRenderState(*ctx, dirty, tracker.FreshlyPrimed());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/ProgramEmit.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P4a's program family: create/bind/delete_shader_state,
|
||||
// set_draw_program, set_dispatch_program and set_global_constants.
|
||||
//
|
||||
// WHERE create_shader_state IS EMITTED FROM, and why it is not the tracker's business: the
|
||||
// tracker's bit-6 shutter reads GetCurrentProgram() and DELIBERATELY NOT GetProgramForDraw(),
|
||||
// because the tracker must not force a compile just to answer "did the shader move". So the
|
||||
// tracker keeps its shutter and the EMITTER joins - from the same GetProgramForDraw() /
|
||||
// GetProgramForDispatch() call the verb is about to make anyway, so no join happens that would
|
||||
// not have happened. Emitting from the compile pool's terminal continuation is a real
|
||||
// asynchronous win and is a LATER phase's: in monolith the applier is one function call away,
|
||||
// so it is unmeasurable here.
|
||||
//
|
||||
// WHAT THE SERVER STILL SPECIALISES, so nobody reads create_shader_state as self-contained
|
||||
// and produces a per-draw rebuild: the draw-FBO clamp masks, the fragColor broadcast count,
|
||||
// the storage-block binding signature, the atomic-counter set, the live image formats and the
|
||||
// patch parameters are all inputs a backend program depends on BEYOND the artefacts. This call
|
||||
// publishes the ARTEFACTS; the server specialises at the verb from the state it holds. The
|
||||
// clause count does not shrink - its inputs move.
|
||||
//
|
||||
// THE ARTEFACTS DO NOT TRAVEL IN MONOLITH. All seven of MGPProgramDesc's blob refs are
|
||||
// declared with Size 0 and the LinkArtifacts / SpirvArtifacts ride beside the record through
|
||||
// MGPipeApplyCreateShaderState's companion pointers, so the codec is never called on the hot
|
||||
// path; the verify build is where it is exercised.
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeProgramEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// create_shader_state (re-issued on the SAME handle whenever the link version moves -
|
||||
// Gen moves only on slot reuse), then bind_shader_state and set_draw_program /
|
||||
// set_dispatch_program. Two program calls because the frontend has two joins and two
|
||||
// PipeInputs slots.
|
||||
Uint64 EmitShaderState(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// set_global_constants: the DEFAULT UNIFORM BLOCK only, keyed (ShaderCso, Version) and
|
||||
// at most once per program per frame. Version is GetUBOContentVersion() and must never
|
||||
// be ~0u, which is the backends' "never uploaded" sentinel - the wrap skips it.
|
||||
Uint64 EmitGlobalConstants(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeProgramEmitter& MGPipeProgramEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason; heap-constructed and
|
||||
// intentionally leaked at exit, and it MUST NOT hold a frontend SharedPtr - that is
|
||||
// the exit-order rule, stated over every MGPipe process singleton rather than over the
|
||||
// ones a destructor reaches today.
|
||||
static MGPipeProgramEmitter* emitter = new MGPipeProgramEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -57,33 +57,16 @@ namespace MobileGL::MG_Pipe {
|
||||
// D-A3: BindMask
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// MGPResourceDesc::BindMask's twelve bits, in the order MGPipeTypes.h names them:
|
||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY.
|
||||
// MGPResourceDesc::BindMask's twelve bits MOVED TO MG_Pipe/MGPipeTypes.h AT P4a, beside
|
||||
// the field, exactly as the note that stood here said they would when a second producer
|
||||
// appeared: P4a's texture family sets kMGPipeBindSampler / kMGPipeBindShaderImage /
|
||||
// kMGPipeBindRenderTarget / kMGPipeBindDepthStencil, the four bits nothing set before.
|
||||
// No alias is written for them because none is possible or needed - both files are
|
||||
// namespace MobileGL::MG_Pipe and this one includes that header, so every spelling below
|
||||
// and in package B's code is unchanged.
|
||||
//
|
||||
// They are spelled HERE rather than in MGPipeTypes.h because that header is the contract
|
||||
// package's and the mask has, so far, exactly one producer: this file. The integrator
|
||||
// moves them beside the field when a second producer appears (P4a's texture family).
|
||||
enum MGPipeBindBit : Uint16 {
|
||||
kMGPipeBindNone = 0,
|
||||
kMGPipeBindVertex = 1u << 0,
|
||||
kMGPipeBindIndex = 1u << 1,
|
||||
kMGPipeBindConstant = 1u << 2,
|
||||
kMGPipeBindShaderBuffer = 1u << 3,
|
||||
kMGPipeBindIndirect = 1u << 4,
|
||||
kMGPipeBindSampler = 1u << 5,
|
||||
kMGPipeBindShaderImage = 1u << 6,
|
||||
kMGPipeBindRenderTarget = 1u << 7,
|
||||
kMGPipeBindDepthStencil = 1u << 8,
|
||||
kMGPipeBindStreamOutput = 1u << 9,
|
||||
kMGPipeBindAtomic = 1u << 10,
|
||||
// THE D-B7 SWITCH. With kCapNeedsHostIndexBytes set the server mirrors this
|
||||
// resource's bytes so it can rewrite restart indices and flatten multi-draws
|
||||
// (ARCHITECTURE.md 10.3). Getting it wrong is invisible in monolith and silently
|
||||
// disables both under split, which is why it is set from the same table as every
|
||||
// other bit rather than from a special case at the emission site.
|
||||
kMGPipeBindElementArray = 1u << 11,
|
||||
};
|
||||
// What stays here is the BUFFER half of the mapping, which is this file's own: the
|
||||
// BufferTarget table, its sentinel and its completeness assert.
|
||||
|
||||
// A sentinel the table below returns for an enumerator it does not name. It is NOT a
|
||||
// legal mask value: every enumerator must be listed, including the ones that map to no
|
||||
@@ -152,12 +135,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// The discriminators MGPResourceDesc / MGPSubData carry for a BUFFER
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MGPipeTypes.h documents Target as "Buffer | Tex1D..TexCubeArray | Renderbuffer |
|
||||
// TexBuffer" and StorageKind as "== TextureStorageType", but P3a is buffer-only and the
|
||||
// contract package minted no enum for the first list. Buffer is its leading member and
|
||||
// is therefore 0, which is also what a zero-initialised record already says; the second
|
||||
// is the frontend enum, named rather than open-coded.
|
||||
inline constexpr Uint16 kMGPipeResourceTargetBuffer = 0;
|
||||
// P4a MINTED THE FIRST LIST: MGPipeTypes.h now carries enum MGPipeResourceTarget beside
|
||||
// the field, and kMGPipeResourceTargetBuffer moved there with it - the narrowed
|
||||
// resource_respecify ack predicate lives in that header and has to name the buffer target
|
||||
// explicitly, and it may not reach into MG_Impl to do so. The second discriminator is the
|
||||
// frontend enum, named rather than open-coded, and stays here because only this file
|
||||
// produces it.
|
||||
inline constexpr Uint8 kMGPipeResourceStorageKindBuffer =
|
||||
static_cast<Uint8>(MobileGL::TextureStorageType::Buffer);
|
||||
|
||||
@@ -552,8 +535,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// The monolith's one resource tracker, beside the state tracker, the CSO cache and the
|
||||
// set-hash suppressor.
|
||||
inline MGPipeResourceTracker& MGPipeResourceTrackerInstance() {
|
||||
static MGPipeResourceTracker tracker;
|
||||
return tracker;
|
||||
// NEVER DESTROYED, for MGPipeSlots()' reason (SlotAllocator.cpp): ~BufferObject reads
|
||||
// and writes this tracker, and the objects that own the last reference to a
|
||||
// BufferObject outlive every function-local static.
|
||||
static MGPipeResourceTracker* tracker = new MGPipeResourceTracker();
|
||||
return *tracker;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/SamplerEmit.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P4a's sampler family: the content-addressed sampler CSO cache, the
|
||||
// identity-addressed sampler view per texture object, and the two unit sets
|
||||
// set_sampler_views and bind_sampler_states. The third unit set, set_shader_images, is
|
||||
// ImageEmit.h's - the same subsystem bit, a different resolution.
|
||||
//
|
||||
// TWO THINGS THIS FILE OWNS THAT ARE EASY TO GET WRONG, both stated where the body will go:
|
||||
// * SamplerParameters is 100 bytes with THREE BYTES OF TRAILING PADDING, so the CSO cache
|
||||
// hashes and memcmp-confirms over a ZERO-INITIALISED canonical copy built field by field,
|
||||
// never over the object's own bytes. Without that the 256-entry cache's hit rate is zero
|
||||
// and nobody notices, because the pixels are right.
|
||||
// * every emission goes through a VERSION-FIRST SKIP before it hashes anything: the sampler
|
||||
// view latches (params version, shape version) per handle, and the two sets latch their
|
||||
// SetHashSuppressor slots. A 192-entry walk per verb without a latch is not affordable.
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full. kMGPipeWiredSamplerSubsystem below covers this file AND
|
||||
// ImageEmit.h: the three unit sets, the sampler CSO and the sampler view are ONE family and
|
||||
// one subsystem bit, because an operator switching samplers off has to get the whole family's
|
||||
// legacy arm rather than two thirds of it.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below and in ImageEmit.h have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeSamplerEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// set_sampler_views: the PROGRAM-RESOLVED set only, one entry per unit, no stage
|
||||
// dimension. Start is 0 and Count is GetMaxTouchedTextureUnit() + 1 clamped to the
|
||||
// wire bound - the high-water mark is directly the count argument and is not
|
||||
// re-derived.
|
||||
Uint64 EmitSamplerViews(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// bind_sampler_states: the unit's sampler CSO, or the null handle when the unit has no
|
||||
// sampler object - the texture's built-in sampler then applies, exactly as today.
|
||||
Uint64 EmitSamplerStates(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeSamplerEmitter& MGPipeSamplerEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason - and this one is named in the
|
||||
// phase's own risk list: a new client singleton that held a frontend SharedPtr, or
|
||||
// that had a destructor an exit handler could run into a torn-down pipe, is the
|
||||
// exit-order UAF P3a closed. Heap-constructed and intentionally leaked at exit.
|
||||
static MGPipeSamplerEmitter* emitter = new MGPipeSamplerEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -18,7 +18,9 @@
|
||||
// them answers it against a shape the backend rediscovered. P2 lands the MECHANISM and ONE
|
||||
// real consumer (SetVertexAttribDefaults) so the shape is pinned by a test rather than by a
|
||||
// plan; the other six slots exist, are unit-tested, and are wired by the phase that moves
|
||||
// the set they name. P3a wires the second, SetVertexBuffers.
|
||||
// the set they name. P3a wires the second, SetVertexBuffers. P4a wires SetSamplerViews,
|
||||
// BindSamplerStates and SetShaderImages, and APPENDS an eighth slot, SetFramebufferState -
|
||||
// which leaves only SetShaderBuffers and SetStreamOutputTargets unwired, both P4b's.
|
||||
//
|
||||
// A WIRED SLOT PUTS A REQUIREMENT ON ITS HASH, and SetVertexBuffers is where that first
|
||||
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
|
||||
@@ -40,15 +42,28 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list).
|
||||
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list), PLUS
|
||||
// SetFramebufferState, which is not kVarTail at all: MGPFramebufferState carries a
|
||||
// ContentHash for TWO jobs - the server's render-pass memo key and the client's emission
|
||||
// suppressor - and the second one needs a slot here like any other. The enum is
|
||||
// CLIENT-ONLY and is not a wire opcode, so appending before Count is safe.
|
||||
enum class MGPipeSuppressorSlot : Uint32 {
|
||||
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
|
||||
SetSamplerViews, // P3b
|
||||
BindSamplerStates, // P3b
|
||||
SetShaderImages, // P4b
|
||||
// P4a - WIRED. The three unit sets' suppressors are not optional and were never a
|
||||
// later phase's: MGPipeTypes.h makes the pattern mandatory for every kVarTail set_*,
|
||||
// because GetTextureBindGeneration() bumps on a REDUNDANT rebind - MC 26.2 rebinds the
|
||||
// same sampler at every texture-unit switch - so an unsuppressed set is a
|
||||
// several-hundred-byte variable-length record per batch, which is the exact regression
|
||||
// the design names. What P3b/P4b owns is the ~175-line BACKEND debounce these replace
|
||||
// (UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and the two
|
||||
// g_*SyncList tables); P4a wires the carrier, P3b/P4b deletes the backend copy.
|
||||
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
|
||||
SetShaderBuffers, // P4b
|
||||
SetStreamOutputTargets, // P4b
|
||||
SetVertexAttribDefaults, // P2 - the one consumer that is wired
|
||||
SetFramebufferState, // P4a - wired
|
||||
Count,
|
||||
};
|
||||
|
||||
@@ -86,8 +101,11 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// The monolith's one suppressor, beside the tracker and the CSO cache.
|
||||
inline MGPipeSetHashSuppressor& MGPipeSetHashSuppressorInstance() {
|
||||
static MGPipeSetHashSuppressor suppressor;
|
||||
return suppressor;
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
|
||||
// rule covers every MGPipe process singleton, not only the ones on today's death
|
||||
// paths.
|
||||
static MGPipeSetHashSuppressor* suppressor = new MGPipeSetHashSuppressor();
|
||||
return *suppressor;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
Executable → Regular
+131
-25
@@ -35,6 +35,22 @@ namespace MobileGL::MG_Pipe {
|
||||
return m_kinds[index < kKindCount ? index : 0];
|
||||
}
|
||||
|
||||
MGPipeSlotAllocator::SlotState* MGPipeSlotAllocator::EntryOf(KindState& state, MGPipeKind kind,
|
||||
Uint32 slot) {
|
||||
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(slot)) {
|
||||
const SizeT index = slot - kMGPipeShaderCsoCompositeSlotBase;
|
||||
if (index >= state.BandSlots.size()) return nullptr;
|
||||
return &state.BandSlots[index];
|
||||
}
|
||||
if (slot >= state.Slots.size()) return nullptr;
|
||||
return &state.Slots[slot];
|
||||
}
|
||||
|
||||
const MGPipeSlotAllocator::SlotState*
|
||||
MGPipeSlotAllocator::EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot) {
|
||||
return EntryOf(const_cast<KindState&>(state), kind, slot);
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::Allocate(MGPipeKind kind) {
|
||||
KindState& state = StateOf(kind);
|
||||
if (state.Slots.empty()) {
|
||||
@@ -94,14 +110,69 @@ namespace MobileGL::MG_Pipe {
|
||||
return handle;
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::AllocateComposite(Uint64 lifetimeId) {
|
||||
// P4a, D-H7. The mirror image of Allocate() above, restricted to the band that one
|
||||
// refuses, and kept in a table of its own so both spaces stay DENSE: the band's base
|
||||
// is 983040, and minting one composite into the slot-indexed vector would allocate
|
||||
// ~23 MB of SlotState for a single program pipeline.
|
||||
KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||
|
||||
Uint32 slot = 0;
|
||||
Bool reused = false;
|
||||
if (!state.BandFreeList.empty()) {
|
||||
slot = state.BandFreeList.back();
|
||||
state.BandFreeList.pop_back();
|
||||
reused = true;
|
||||
}
|
||||
|
||||
if (!reused) {
|
||||
const SizeT next = kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size();
|
||||
slot = static_cast<Uint32>(next);
|
||||
// The band's own exhaustion assert, mirroring Allocate()'s: a composite that
|
||||
// cannot be minted is a NAMED failure, not a silent fall-through into the ordinary
|
||||
// program slots, which is exactly what reserving a band rather than setting a flag
|
||||
// buys.
|
||||
MOBILEGL_ASSERT(next < kMGPipeShaderCsoSlotLimit,
|
||||
"the MGPipe ShaderCso COMPOSITE band is exhausted at slot %zu; a "
|
||||
"program-pipeline composite cannot be minted and must not take an "
|
||||
"ordinary program's slot",
|
||||
next);
|
||||
if (next >= kMGPipeShaderCsoSlotLimit) return kMGPipeNullHandle;
|
||||
state.BandSlots.emplace_back();
|
||||
}
|
||||
|
||||
SlotState* entry = EntryOf(state, MGPipeKind::ShaderCso, slot);
|
||||
if (entry == nullptr) return kMGPipeNullHandle;
|
||||
if (entry->EverHandedOut) {
|
||||
MOBILEGL_ASSERT(entry->Gen != ~Uint32{0},
|
||||
"MGPipe handle generation wrapped on the ShaderCso composite band, "
|
||||
"slot %u; {slot, gen} is no longer unique",
|
||||
slot);
|
||||
++entry->Gen;
|
||||
}
|
||||
entry->EverHandedOut = true;
|
||||
entry->Live = true;
|
||||
entry->LifetimeId = lifetimeId;
|
||||
++state.LiveCount;
|
||||
// The band's share of LiveCount, so CompositeLiveCount() can answer without a walk.
|
||||
++state.BandLiveCount;
|
||||
if (lifetimeId != 0) {
|
||||
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
|
||||
"lifetime id %llu already owns a ShaderCso slot",
|
||||
static_cast<unsigned long long>(lifetimeId));
|
||||
state.ByLifetimeId[lifetimeId] = slot;
|
||||
}
|
||||
return MGPipeHandle{slot, entry->Gen};
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
|
||||
if (lifetimeId == 0) return kMGPipeNullHandle;
|
||||
const KindState& state = StateOf(kind);
|
||||
const auto it = state.ByLifetimeId.find(lifetimeId);
|
||||
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
|
||||
const Uint32 slot = it->second;
|
||||
if (slot >= state.Slots.size() || !state.Slots[slot].Live) return kMGPipeNullHandle;
|
||||
return MGPipeHandle{slot, state.Slots[slot].Gen};
|
||||
const SlotState* entry = EntryOf(state, kind, it->second);
|
||||
if (entry == nullptr || !entry->Live) return kMGPipeNullHandle;
|
||||
return MGPipeHandle{it->second, entry->Gen};
|
||||
}
|
||||
|
||||
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
|
||||
@@ -112,63 +183,98 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
|
||||
KindState& state = StateOf(kind);
|
||||
if (handle.Slot >= state.Slots.size()) return;
|
||||
SlotState& entry = state.Slots[handle.Slot];
|
||||
SlotState* entry = EntryOf(state, kind, handle.Slot);
|
||||
if (entry == nullptr) return;
|
||||
// A stale handle must not free the slot its successor now owns - that is the whole
|
||||
// reason the generation is in the key.
|
||||
if (!entry.Live || entry.Gen != handle.Gen) return;
|
||||
if (entry.LifetimeId != 0) {
|
||||
const auto it = state.ByLifetimeId.find(entry.LifetimeId);
|
||||
// reason the generation is in the key. It is also what makes the SECOND of a
|
||||
// composite's two independent release paths a proven no-op.
|
||||
if (!entry->Live || entry->Gen != handle.Gen) return;
|
||||
if (entry->LifetimeId != 0) {
|
||||
const auto it = state.ByLifetimeId.find(entry->LifetimeId);
|
||||
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
|
||||
state.ByLifetimeId.erase(it);
|
||||
}
|
||||
}
|
||||
entry.Live = false;
|
||||
entry.LifetimeId = 0;
|
||||
entry->Live = false;
|
||||
entry->LifetimeId = 0;
|
||||
--state.LiveCount;
|
||||
state.FreeList.push_back(handle.Slot);
|
||||
if (kind == MGPipeKind::ShaderCso && MGPipeIsCompositeShaderSlot(handle.Slot)) {
|
||||
--state.BandLiveCount;
|
||||
state.BandFreeList.push_back(handle.Slot);
|
||||
} else {
|
||||
state.FreeList.push_back(handle.Slot);
|
||||
}
|
||||
}
|
||||
|
||||
Bool MGPipeSlotAllocator::IsLive(MGPipeKind kind, MGPipeHandle handle) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (handle.Slot >= state.Slots.size()) return false;
|
||||
const SlotState& entry = state.Slots[handle.Slot];
|
||||
return entry.Live && entry.Gen == handle.Gen;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, handle.Slot);
|
||||
return entry != nullptr && entry->Live && entry->Gen == handle.Gen;
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (slot >= state.Slots.size()) return 0;
|
||||
return state.Slots[slot].Gen;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||
return entry != nullptr ? entry->Gen : 0;
|
||||
}
|
||||
|
||||
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
|
||||
const KindState& state = StateOf(kind);
|
||||
if (slot >= state.Slots.size()) return 0;
|
||||
return state.Slots[slot].LifetimeId;
|
||||
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
|
||||
return entry != nullptr ? entry->LifetimeId : 0;
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
|
||||
// THE ORDINARY SPACE ONLY, and the band is reported by CompositeHighWater() below.
|
||||
// Folding the two would pin this at ~983k from the first composite mint onward and
|
||||
// take the ordinary space's "the high-water mark did not move" assertion away for the
|
||||
// rest of the process - the assertion that catches a dense table that never shrinks,
|
||||
// which is the leak shape this allocator exists to make visible. Two spaces, two
|
||||
// numbers, two real assertions. See SlotAllocator.h.
|
||||
return static_cast<Uint32>(StateOf(kind).Slots.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeHighWater() const {
|
||||
const KindState& state = StateOf(MGPipeKind::ShaderCso);
|
||||
// One past the highest composite slot ever handed out; exactly the base when none ever
|
||||
// was, so the number is monotone from the first mint and a LEAKED COMPOSITE MOVES IT.
|
||||
return static_cast<Uint32>(kMGPipeShaderCsoCompositeSlotBase + state.BandSlots.size());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeLiveCount() const {
|
||||
return StateOf(MGPipeKind::ShaderCso).BandLiveCount;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
Uint32 MGPipeSlotAllocator::CompositeFreeCount() const {
|
||||
return static_cast<Uint32>(StateOf(MGPipeKind::ShaderCso).BandFreeList.size());
|
||||
}
|
||||
|
||||
void MGPipeSlotAllocator::Reset() {
|
||||
for (KindState& state : m_kinds) {
|
||||
state.Slots.clear();
|
||||
state.FreeList.clear();
|
||||
state.BandSlots.clear();
|
||||
state.BandFreeList.clear();
|
||||
state.ByLifetimeId.clear();
|
||||
state.LiveCount = 0;
|
||||
state.BandLiveCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
MGPipeSlotAllocator& MGPipeSlots() {
|
||||
static MGPipeSlotAllocator allocator;
|
||||
return allocator;
|
||||
// NEVER DESTROYED, deliberately (one allocation for the life of the process). A
|
||||
// frontend object's destructor reaches this allocator - ~BufferObject through
|
||||
// MGPipeEmitResourceDestroyAndFree, ~VertexArrayObject through the death notice - and
|
||||
// MG_Backend/MGPipe/PipeInputs.h's gPipeInputs holds SharedPtrs to those objects at
|
||||
// namespace scope, so they are destroyed by __run_exit_handlers AFTER this
|
||||
// function-local static would have been. A destroyed allocator then answers
|
||||
// FindByLifetimeId out of a freed hash table and Free() writes into freed vectors -
|
||||
// an exit-time heap corruption whose fatality depends only on the allocator's layout.
|
||||
static MGPipeSlotAllocator* allocator = new MGPipeSlotAllocator();
|
||||
return *allocator;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
Executable → Regular
+68
-2
@@ -47,6 +47,25 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle Allocate(MGPipeKind kind);
|
||||
// Allocate and remember `lifetimeId` as this handle's frontend identity.
|
||||
MGPipeHandle AllocateFor(MGPipeKind kind, Uint64 lifetimeId);
|
||||
|
||||
// P4a, D-H7: THE ONE ENTRY POINT INTO THE ShaderCso COMPOSITE BAND, and the only one
|
||||
// there will ever be. Allocate() above refuses that band on purpose, so a program
|
||||
// pipeline's flattened composite - minted client-side from the stage programs bound to
|
||||
// the pipeline object, and indistinguishable from an ordinary program to the server -
|
||||
// needs a door of its own rather than a flag on the handle. The kind is implied: only
|
||||
// ShaderCso has a band.
|
||||
//
|
||||
// It behaves exactly like AllocateFor in every other respect (free list first, then
|
||||
// the band's own high-water mark; Gen moves only on reuse; the lifetimeId -> slot map
|
||||
// is written) and it carries the band's own exhaustion assert, so exhausting the
|
||||
// composite space is a NAMED Fatal rather than silent slot theft from ordinary
|
||||
// programs. Returns kMGPipeNullHandle when the band is full.
|
||||
//
|
||||
// Freed through the ordinary Free(MGPipeKind::ShaderCso, handle): a composite's slot
|
||||
// has two independent release paths - the pipeline cache's LRU eviction and the
|
||||
// composite ProgramObject's own destructor - and Free refusing a slot that is not live
|
||||
// at that generation is what makes the second one a proven no-op.
|
||||
MGPipeHandle AllocateComposite(Uint64 lifetimeId);
|
||||
// The handle a lifetime id was allocated for, or kMGPipeNullHandle. A recycled heap
|
||||
// address does NOT reproduce a mapping: MG_State hands out a fresh lifetime id per
|
||||
// object, so the map key is unique for the life of the process.
|
||||
@@ -64,11 +83,40 @@ namespace MobileGL::MG_Pipe {
|
||||
// otherwise, live or not.
|
||||
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
|
||||
Uint64 LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const;
|
||||
// One past the highest slot ever handed out of this kind, i.e. what a server-side
|
||||
// slot-indexed table must be sized to.
|
||||
// One past the highest ORDINARY slot ever handed out of this kind. For every kind but
|
||||
// ShaderCso that is the whole story; for ShaderCso the composite band is a second,
|
||||
// separately dense space and CompositeHighWater() below answers it.
|
||||
//
|
||||
// THE TWO SPACES ARE REPORTED SEPARATELY, and that is the point rather than a detail.
|
||||
// Folding the band into this number pins it at ~983k from the first composite mint
|
||||
// onward, and every later assertion of the "the high-water mark did not move over N
|
||||
// churn rounds" shape - the one that catches a dense table that never shrinks, which
|
||||
// is the ~1.3 KB-per-record leak C-1 produced - becomes vacuously true for ordinary
|
||||
// ShaderCso slots for the rest of the process. A leak case per space is two real
|
||||
// assertions; one merged number is one real assertion and one that cannot go red.
|
||||
//
|
||||
// It is also NOT a table size for kind ShaderCso even now: the band is sparse against
|
||||
// the ordinary space by design, so a consumer indexing by slot must test
|
||||
// MGPipeIsCompositeShaderSlot(slot) first and keep the band in a table of its own,
|
||||
// exactly as this allocator does.
|
||||
Uint32 HighWater(MGPipeKind kind) const;
|
||||
// One past the highest COMPOSITE slot ever handed out, i.e.
|
||||
// kMGPipeShaderCsoCompositeSlotBase + (band slots ever handed out), and exactly the
|
||||
// base when none ever was. Kind ShaderCso is the only kind with a band, so it is
|
||||
// implied - as it is for AllocateComposite. A LEAKED COMPOSITE MOVES THIS and moves
|
||||
// nothing else, which is what the composite's own leak case asserts on.
|
||||
Uint32 CompositeHighWater() const;
|
||||
// Live slots of this kind, ORDINARY AND COMPOSITE TOGETHER for ShaderCso: a live
|
||||
// composite is a live ShaderCso, the applier's two record tables are one object class,
|
||||
// and a caller asking "how many shader CSOs does this client hold" wants both. The
|
||||
// band's own count is CompositeLiveCount(); the ordinary space's is the difference.
|
||||
Uint32 LiveCount(MGPipeKind kind) const;
|
||||
Uint32 CompositeLiveCount() const;
|
||||
// Slots waiting on a free list. Also BOTH SPACES for ShaderCso, for LiveCount's
|
||||
// reason and with the same caveat: a caller that needs to know WHICH space a slot went
|
||||
// back to reads CompositeFreeCount() and subtracts.
|
||||
Uint32 FreeCount(MGPipeKind kind) const;
|
||||
Uint32 CompositeFreeCount() const;
|
||||
|
||||
// Context teardown / server reset / a unit test's fixture.
|
||||
void Reset();
|
||||
@@ -85,12 +133,30 @@ namespace MobileGL::MG_Pipe {
|
||||
// Indexed by slot; [0] is the reserved slot and is never live.
|
||||
Vector<SlotState> Slots;
|
||||
Vector<Uint32> FreeList;
|
||||
// P4a: the ShaderCso COMPOSITE band, indexed by (slot - the band's base) and
|
||||
// EMPTY for every other kind. A SECOND VECTOR RATHER THAN MORE OF THE FIRST, and
|
||||
// it is not a micro-optimisation: the band starts at 983040, so minting one
|
||||
// composite into the slot-indexed vector above would allocate ~983k SlotStates -
|
||||
// ~23 MB - for a single program pipeline, and a consumer that sized a table off
|
||||
// HighWater would pay the same shape again with a far bigger record. Both spaces
|
||||
// stay dense against their own high-water mark, which is the property this
|
||||
// allocator exists to give the server.
|
||||
Vector<SlotState> BandSlots;
|
||||
Vector<Uint32> BandFreeList;
|
||||
UnorderedMap<Uint64, Uint32> ByLifetimeId;
|
||||
Uint32 LiveCount = 0;
|
||||
// The band's share of LiveCount above, so the two spaces can be reported apart
|
||||
// without walking either table. Always 0 for every kind but ShaderCso.
|
||||
Uint32 BandLiveCount = 0;
|
||||
};
|
||||
|
||||
KindState& StateOf(MGPipeKind kind);
|
||||
const KindState& StateOf(MGPipeKind kind) const;
|
||||
// The SlotState a (kind, slot) names, in whichever of the two vectors holds it, or
|
||||
// null when the slot has never been handed out. One resolver, so a caller that forgets
|
||||
// the band cannot exist.
|
||||
static SlotState* EntryOf(KindState& state, MGPipeKind kind, Uint32 slot);
|
||||
static const SlotState* EntryOf(const KindState& state, MGPipeKind kind, Uint32 slot);
|
||||
|
||||
Array<KindState, kKindCount> m_kinds{};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/TextureEmit.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The CLIENT side of P4a's texture and renderbuffer family: resource_create from the object's
|
||||
// constructor, resource_respecify from every storage-defining entry point, set_texture_params
|
||||
// from the parameter mutators, and resource_subdata from the DRAIN LIST at the validate point.
|
||||
//
|
||||
// THE THREE OBJECT CALLS ARE NOT EMITTED FROM HERE'S CALLER, they are emitted from MG_State's
|
||||
// own mutators - a constructor, a storage definition, a glTexParameter - exactly as P3a's
|
||||
// buffer family is, because that is where the event happens. Only the sub-data drain runs at
|
||||
// the validate point, which is the explicit exception ARCHITECTURE.md 5.1 makes for texture
|
||||
// upload: walking every live texture per verb is the cost the drain list exists to avoid.
|
||||
//
|
||||
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
|
||||
// FramebufferEmit.h for why, in full: PipeFill.cpp is the contract package's for the whole
|
||||
// phase, so the emitter package edits this header and the value of
|
||||
// kMGPipeWiredTextureSubsystem below, and never that file.
|
||||
//
|
||||
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitter below has a body; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = 0;
|
||||
|
||||
// STUB AT THE CONTRACT COMMIT: emits nothing, returns 0 payload bytes.
|
||||
class MGPipeTextureEmitter {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// The DRAIN LIST, at the validate point: one resource_subdata per dirty
|
||||
// (storage owner, upload target, level) that was appended on its FIRST dirty mark and
|
||||
// is cleared at emission. Keyed on the STORAGE OWNER from day one - a view and its
|
||||
// owner already share one dirty state - so an upload through a view and an upload
|
||||
// through the owner land on the same key.
|
||||
//
|
||||
// The client clears its own dirty flags here, and ONLY for the levels whose record the
|
||||
// applier accepted; the applier accumulates the emitted shape into a server-side
|
||||
// pending-upload set that survives Espryt's bail arms, which is what stops a bail from
|
||||
// losing texels.
|
||||
//
|
||||
// Returns the bytes that went on the wire, for the per-draw payload histogram.
|
||||
Uint64 DrainTextureSubData(GLContext& ctx) {
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
||||
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason.
|
||||
static MGPipeTextureEmitter* emitter = new MGPipeTextureEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
+242
-22
@@ -20,9 +20,24 @@
|
||||
//
|
||||
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
|
||||
// computed by comparing a shutter against what the tracker last pushed. P2 emitted for bits
|
||||
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and the
|
||||
// rest are still computed, latched and counted so the per-bit fire rate is a measurement
|
||||
// rather than a plan, with their fields going through the residual fill until P3b/P4a/P4b.
|
||||
// 0..4 (the value-class ones); P3a adds bits 5, 9 and 10 - the vertex-input family - and P4a
|
||||
// adds SEVEN: 6, 7 and 8 (the program family), 11 (the framebuffer) and 12, 13 and 14 (the
|
||||
// three unit sets). Only bits 15, 16 and 17 - the const-buffer, shader-buffer and
|
||||
// stream-output sets - are still computed, latched and counted without an emitter, so the
|
||||
// per-bit fire rate is a measurement rather than a plan and their fields go through the
|
||||
// residual fill until P4b.
|
||||
//
|
||||
// P4a NARROWS NOTHING AND WIDENS THREE THINGS, and every one of them was an UNDER-FIRE that
|
||||
// only became reachable once the bit gained an emitter:
|
||||
// (1) bit 11's shutter gains the READ framebuffer binding slot's version, because
|
||||
// set_framebuffer_state is emitted per bound TARGET and a glBindFramebuffer(
|
||||
// GL_READ_FRAMEBUFFER, ...) moved no shutter at all before;
|
||||
// (2) bit 13's gains the TEXTURE BIND generation, because glBindSampler moves that one and
|
||||
// not the sampling-resolution one, so bind_sampler_states could not see a sampler bind;
|
||||
// (3) bits 6/7/8 - and with them bit 14's program half - read the EFFECTIVE program source
|
||||
// instead of GetCurrentProgram() alone, which is null for the whole life of a bound
|
||||
// separable program pipeline, so a re-composited pipeline reached no program emitter.
|
||||
// Over-firing is free; all three of those were the other direction.
|
||||
//
|
||||
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
|
||||
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
|
||||
@@ -61,22 +76,24 @@ namespace MobileGL::MG_Pipe {
|
||||
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
|
||||
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
||||
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
||||
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a; the other three are
|
||||
// still computed and counted, and are emitted from P3b/P4a on ----
|
||||
// ---- value class: NEW_VERTEX_ELEMENTS is emitted from P3a and the other three from
|
||||
// P4a - the program family, one subsystem, three bits because the frontend moves them
|
||||
// as three separate events ----
|
||||
NewVertexElements, // the bound VAO's attribute configuration -> create/bind_vertex_elements
|
||||
NewShader, // the current program's link version
|
||||
NewShaderBindings, // image units, block bindings, uniform write set
|
||||
NewGlobalConstants, // the default-uniform-block image
|
||||
NewShader, // the current program's link version -> create/bind_shader_state,
|
||||
// set_draw_program, set_dispatch_program (P4a)
|
||||
NewShaderBindings, // image units, block bindings, uniform write set (P4a)
|
||||
NewGlobalConstants, // the default-uniform-block image -> set_global_constants (P4a)
|
||||
// ---- object class. THE FIRST TWO ARE P3a's, not P3b/P4b's: the roadmap puts
|
||||
// set_vertex_buffers and set_index_buffer in the same phase as the vertex-elements
|
||||
// trio, and this comment said otherwise until the commit that wired them. The rest
|
||||
// are still computed and counted only. ----
|
||||
// trio, and this comment said otherwise until the commit that wired them. THE NEXT
|
||||
// FOUR ARE P4a's. The last three are still computed and counted only, until P4b. ----
|
||||
NewVertexBuffers, // -> set_vertex_buffers (P3a)
|
||||
NewIndexBuffer, // -> set_index_buffer (P3a)
|
||||
NewFramebuffer,
|
||||
NewSamplerViews,
|
||||
NewSamplers,
|
||||
NewShaderImages,
|
||||
NewFramebuffer, // -> set_framebuffer_state, per bound target (P4a)
|
||||
NewSamplerViews, // -> set_sampler_views (P4a)
|
||||
NewSamplers, // -> bind_sampler_states (P4a)
|
||||
NewShaderImages, // -> set_shader_images (P4a)
|
||||
NewConstBuffers,
|
||||
NewShaderBuffers,
|
||||
NewSoTargets,
|
||||
@@ -103,6 +120,22 @@ namespace MobileGL::MG_Pipe {
|
||||
kMGPipeDirtyEmittedAtP2 | MGPipeDirtyBit(MGPipeDirty::NewVertexElements) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers) | MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer);
|
||||
|
||||
// The SEVEN P4a adds, across FOUR subsystems: bits 6/7/8 are the program family, 11 the
|
||||
// framebuffer, and 12/13/14 the sampler-view / sampler-state / image-unit sets. Added
|
||||
// rather than edited into the two above, for the reason those two exist: each phase's
|
||||
// constant survives as the next phase's A/B control and as what a test compares the
|
||||
// subsystem map against.
|
||||
//
|
||||
// EVERY ONE OF THESE SHUTTERS WAS ALREADY COMPUTED, LATCHED AND COUNTED before P4a; what
|
||||
// P4a adds is an emitter for them. That is why this is a one-line constant and not seven
|
||||
// new shutters - and it is also why the two narrowings below are stated as requirements.
|
||||
inline constexpr Uint32 kMGPipeDirtyEmittedAtP4a =
|
||||
kMGPipeDirtyEmittedAtP3a | MGPipeDirtyBit(MGPipeDirty::NewShader) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewShaderBindings) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewFramebuffer) | MGPipeDirtyBit(MGPipeDirty::NewSamplerViews) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewSamplers) | MGPipeDirtyBit(MGPipeDirty::NewShaderImages);
|
||||
|
||||
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
|
||||
"NEW_RENDER_STATE",
|
||||
"NEW_PIPELINE_STATE",
|
||||
@@ -146,8 +179,33 @@ namespace MobileGL::MG_Pipe {
|
||||
case MGPipeDirty::NewVertexBuffers:
|
||||
case MGPipeDirty::NewIndexBuffer:
|
||||
return kMGPipeSubsystemVertexInput;
|
||||
// P4a's seven, across four subsystems. FOUR AND NOT ONE for P3a's reason one level
|
||||
// out: a framebuffer path that regressed, a texture path that regressed, a sampler
|
||||
// path that regressed and a program path that regressed are four different findings.
|
||||
//
|
||||
// The program family is three bits because the frontend moves them separately - a
|
||||
// relink, a binding change and a uniform write are three events - but one subsystem,
|
||||
// because an operator switching programs off has to get the whole family's legacy arm.
|
||||
// Same for the three unit sets: create_sampler_state, create_sampler_view and the
|
||||
// three kVarTail sets are one family, and half of it is not a control.
|
||||
case MGPipeDirty::NewShader:
|
||||
case MGPipeDirty::NewShaderBindings:
|
||||
case MGPipeDirty::NewGlobalConstants:
|
||||
return kMGPipeSubsystemPrograms;
|
||||
case MGPipeDirty::NewFramebuffer:
|
||||
return kMGPipeSubsystemFramebuffer;
|
||||
case MGPipeDirty::NewSamplerViews:
|
||||
case MGPipeDirty::NewSamplers:
|
||||
case MGPipeDirty::NewShaderImages:
|
||||
return kMGPipeSubsystemSamplers;
|
||||
// NO BIT NAMES kMGPipeSubsystemTextureResources, and that is deliberate rather than an
|
||||
// omission: the texture and renderbuffer resource_* calls and set_texture_params are
|
||||
// dispatched from the GL entry points that cause them - a constructor, a storage
|
||||
// definition, a glTexParameter - not from a dirty walk, exactly as P3a's buffer family
|
||||
// is. Bit 10 gates those dispatch sites; there is no dirty bit to map onto it and
|
||||
// there must not be one, or the emission would be gated twice and disagree with itself.
|
||||
default:
|
||||
// The remaining bits have no call of their own until P3b/P4a/P4b, so there is no
|
||||
// The remaining bits have no call of their own until P4b, so there is no
|
||||
// subsystem to switch and the residual fill keeps supplying their fields.
|
||||
return 0;
|
||||
}
|
||||
@@ -243,6 +301,49 @@ namespace MobileGL::MG_Pipe {
|
||||
// must not force a compile just to answer "did the shader move". These version
|
||||
// counters are plain members and are exactly what the backends already read
|
||||
// without joining (Core.cpp, the glUseProgram half of join site J1).
|
||||
//
|
||||
// BUT GetCurrentProgram() ALONE IS NOT THE PROGRAM SOURCE, AND AT P4a THAT IS AN
|
||||
// UNDER-FIRE. Under GL_ARB_separate_shader_objects an application drives
|
||||
// `glUseProgram(0); glBindProgramPipeline(P)`, and m_currentProgram is then null
|
||||
// for the whole life of that pipeline (Core.cpp, GetProgramForDraw's second half):
|
||||
// all three of these shutters read 0 == 0 forever, so after the first walk on a
|
||||
// fresh context - the one !m_primed fires unconditionally - bits 6, 7 and 8 never
|
||||
// fire again however the pipeline is restaged.
|
||||
//
|
||||
// WHILE NOTHING WAS EMITTED FOR THEM THAT WAS INVISIBLE, which is how it survived
|
||||
// to P4a: GetProgramForDraw is emitted-and-still-pulled, the residual fill copies
|
||||
// it at every verb, and DirtySurface.def rules BindProgramPipelineObject
|
||||
// kPulledEveryVerb for exactly that reason - the backend still receives the right
|
||||
// SharedPtr and nothing renders wrong. The moment P4a emits off these bits it
|
||||
// stops being invisible: glUseProgramStages rebuilds the composite, EmitShaderState
|
||||
// is never called again, so the new composite gets no ShaderCso handle and no
|
||||
// create_shader_state while set_draw_program keeps naming the previous one - a
|
||||
// program the handle protocol never announced, which is exactly the seam-defect
|
||||
// class P3a spent a phase closing. And bit 8 never firing means
|
||||
// set_global_constants is never sent for a pipeline draw at all, where the pull
|
||||
// rescues nothing.
|
||||
//
|
||||
// SO THE SHUTTER READS THE EFFECTIVE SOURCE: the program in use when there is one,
|
||||
// and the bound pipeline when there is not. What it reads OF that pipeline is the
|
||||
// pair ComputeDrawProgramSignature() is built from - each stage program's lifetime
|
||||
// id and LINK version - so bit 6 fires exactly when GetProgramForDraw would hand
|
||||
// back a different composite, which is exactly when a new ShaderCso handle has to
|
||||
// be minted. Those are the same non-artefact fields the plain-program arm above
|
||||
// reads, and the ones Core.cpp calls out as not passing through ProgramObject's
|
||||
// join gate, so the "must not force a compile" rule survives intact: no join, no
|
||||
// flatten, no Link().
|
||||
//
|
||||
// THE PIPELINE NAME IS MIXED IN because two pipelines can carry the same stage set
|
||||
// and each caches its OWN composite object, so the signature alone would let a
|
||||
// glBindProgramPipeline between two such pipelines pass without a fire. What that
|
||||
// does NOT close is a name RECYCLED (glDeleteProgramPipelines +
|
||||
// glGenProgramPipelines) back onto the same stage programs at the same link
|
||||
// versions with no other program-family change in between: a ProgramPipelineObject
|
||||
// has no lifetime id and no wire object at all - DirtySurface.def says so where it
|
||||
// rules MarkProgramPipelineForDeletion kUnpublishedDestroy - so there is nothing
|
||||
// else here to mix it with. Recorded rather than quietly left: closing it needs a
|
||||
// generation counter on the frontend object, which is an MG_State change and not
|
||||
// this file's to make.
|
||||
const auto& program = ctx.GetCurrentProgram();
|
||||
Uint64 shader = 0;
|
||||
Uint64 bindings = 0;
|
||||
@@ -257,6 +358,66 @@ namespace MobileGL::MG_Pipe {
|
||||
program->GetUniformWriteSetVersion());
|
||||
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
|
||||
programImages = program->GetImageUnitVersion();
|
||||
} else if (const auto& pipeline = ctx.GetBoundProgramPipeline(); pipeline) {
|
||||
using Pipeline = MG_State::GLState::ProgramPipelineObject;
|
||||
// THE FIELDS ARE READ DIRECTLY RATHER THAN THROUGH THE TWO FUNCTIONS THAT
|
||||
// ALREADY PACK THEM, and that is a gate constraint, not a preference. Calling
|
||||
// ComputeDrawProgramSignature() / ComputeUniformMirrorVersions() would say
|
||||
// "the same pairs the composite cache and the uniform-mirror gate compare"
|
||||
// far better than this loop does - but gen_pipe_dirty_surface.py derives a
|
||||
// shutter by following each accessor to the member it returns, and both of
|
||||
// those build a LOCAL array and return that, which it cannot place. A shutter
|
||||
// naming them is UNRESOLVED, and then every DirtySurface.def row that names
|
||||
// bits 6, 7, 8 or 14 loses its verdict - including the derivation that is the
|
||||
// only mechanism able to catch the next under-fire here. So the pairs are
|
||||
// spelled out, and the two static_asserts below are what say they must stay in
|
||||
// step with the functions they mirror.
|
||||
static_assert(sizeof(Pipeline::DrawProgramSignature) ==
|
||||
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
|
||||
"bit 6 reads the {lifetimeId, linkVersion} pair per graphics "
|
||||
"stage that ComputeDrawProgramSignature packs");
|
||||
static_assert(sizeof(Pipeline::UniformMirrorVersions) ==
|
||||
2 * Pipeline::kGraphicsStageCount * sizeof(Uint64),
|
||||
"bits 7 and 8 read the four counters per graphics stage that "
|
||||
"ComputeUniformMirrorVersions packs");
|
||||
|
||||
// Bit 6 is the pipeline's identity plus the composite cache key. Bits 7 and 8
|
||||
// add the per-program state, which under a pipeline is written to the STAGE
|
||||
// programs - glUniform* addresses the pipeline's active program,
|
||||
// glProgramUniform* and the two block-binding calls address a named one - and
|
||||
// only reaches the composite through RefreshCompositeUniforms. Bit 14's half
|
||||
// takes the image-unit generation, which is its own counter for the reason
|
||||
// ProgramObject gives (ES forbids glUniform1i on an image uniform, so Espryt
|
||||
// BAKES the unit into the ESSL it generates and only a regeneration honours a
|
||||
// change) and which D-G4 asks this shutter to keep reading as a FRONTEND
|
||||
// counter rather than any server-side epoch.
|
||||
//
|
||||
// STAGELINKS IS MIXED INTO ALL THREE OF THE OTHERS, ON PURPOSE. A composite
|
||||
// REBUILD hands back a brand-new ProgramObject with an empty default uniform
|
||||
// block and no backend state at all - SetCachedDrawProgram clears the mirror
|
||||
// versions with it - so a shutter watching only the per-stage state counters
|
||||
// would let a rebuilt composite inherit the bindings, the constants and the
|
||||
// image units of the one it replaced.
|
||||
Uint64 stageLinks = static_cast<Uint64>(ctx.GetBoundProgramPipelineName());
|
||||
Uint64 stageState = 0;
|
||||
Uint64 stageImages = 0;
|
||||
for (SizeT stage = 0; stage < Pipeline::kGraphicsStageCount; ++stage) {
|
||||
const auto& staged = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!staged) continue;
|
||||
stageLinks = MGPipeMixShutter(
|
||||
MGPipeMixShutter(stageLinks, staged->GetLifetimeId()), staged->GetLinkVersion());
|
||||
stageState = MGPipeMixShutter(
|
||||
MGPipeMixShutter(MGPipeMixShutter(stageState, staged->GetBackendStateVersion()),
|
||||
MGPipeMixShutter(staged->GetUBOContentVersion(),
|
||||
staged->GetBlockBindingVersion())),
|
||||
staged->GetUniformWriteSetVersion());
|
||||
stageImages = MGPipeMixShutter(stageImages, staged->GetImageUnitVersion());
|
||||
}
|
||||
shader = stageLinks;
|
||||
stageState = MGPipeMixShutter(stageLinks, stageState);
|
||||
bindings = MGPipeMixShutter(stageState, stageImages);
|
||||
constants = stageState;
|
||||
programImages = MGPipeMixShutter(stageLinks, stageImages);
|
||||
}
|
||||
now[Index(MGPipeDirty::NewShader)] = shader;
|
||||
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
|
||||
@@ -300,14 +461,60 @@ namespace MobileGL::MG_Pipe {
|
||||
indexObject ? indexObject->GetLifetimeId() : 0);
|
||||
}
|
||||
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(vaoIdentity, indexShutter);
|
||||
// Bit 11, WIDENED AT P4a AND THIS IS A REQUIREMENT RATHER THAN AN OPTION. The
|
||||
// shutter observed the DRAW binding slot only, so glBindFramebuffer(
|
||||
// GL_READ_FRAMEBUFFER, ...) moved nothing at all - which was harmless while
|
||||
// nothing was emitted for the bit and is an UNDER-FIRE the moment P4a emits
|
||||
// set_framebuffer_state per bound target (D-C2): the read record would never be
|
||||
// sent and the server's ReadSurface would stay the previous framebuffer's. Over-
|
||||
// firing costs one extra push; under-firing renders stale, and this file's own
|
||||
// rule is that under-firing is the dangerous direction.
|
||||
//
|
||||
// A RENDERBUFFER RESPECIFY IS STILL INVISIBLE HERE, and deliberately so:
|
||||
// RenderbufferObject's SetInternalFormat / AllocateStorage / SetSamples bump no
|
||||
// version and raise no notice, so re-storaging an ALREADY-ATTACHED renderbuffer
|
||||
// moves neither half of this shutter. That hole is closed by emitting
|
||||
// resource_respecify straight from the storage entry point - not by widening this
|
||||
// shutter and not by adding a version counter to RenderbufferObject, which would
|
||||
// resize the pull build's object and break G1.
|
||||
//
|
||||
// AND A TRAP THE NEXT NARROWING WOULD WALK INTO, recorded here because it is
|
||||
// invisible from the shutter: FramebufferObject::SetDrawBuffer versions the VALUE
|
||||
// being written rather than the index being written TO - it calls
|
||||
// BumpAttachmentVersion(buffer). The object version and the aggregate still move,
|
||||
// so THIS shutter is safe; a narrower one built on m_attachmentVersions would not
|
||||
// be, and P4a must not build one.
|
||||
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion()));
|
||||
MGPipeMixShutter(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion())),
|
||||
m_readFramebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Read).GetVersion()));
|
||||
now[Index(MGPipeDirty::NewSamplerViews)] =
|
||||
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewSamplers)] =
|
||||
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration());
|
||||
// Bit 13, WIDENED AT P4a FOR BIT 11's REASON and found the same way. glBindSampler
|
||||
// moves NEITHER half of what this used to read: GL_Sampler.cpp's BindSampler_State
|
||||
// goes through NoteTextureUnitTouched and TextureUnit::SetSamplerObject, and both
|
||||
// of those bump the TEXTURE BIND generation - bit 12's. The only two writers of
|
||||
// BumpSamplingResolutionGeneration are PARAMETER changes (SamplerObject.cpp,
|
||||
// TextureObject.cpp). So `glBindSampler(3, a); draw; glBindSampler(3, b); draw`
|
||||
// fired bit 12 twice and bit 13 not once, and the server's BoundSamplerStates[3]
|
||||
// went on naming a's CSO: wrong filtering, with nothing able to see it, because
|
||||
// bind_sampler_states has no pulled twin to fall back on the way the view set does.
|
||||
//
|
||||
// MIXING THE GENERATION IN IS THE FIX RATHER THAN A SECOND GATE ON THE EMITTER,
|
||||
// because that generation is what the unit SET is derived from: a sampler bind
|
||||
// changes which sampler state applies at a unit, and a texture bind changes it too
|
||||
// whenever the unit carries no sampler object and the texture's BUILT-IN sampler is
|
||||
// what applies. Keeping it one shutter per bit is also what keeps the per-subsystem
|
||||
// A/B and the per-bit fire tallies meaning what they say - a bit gated on another
|
||||
// bit's shutter measures neither. The extra fires a plain texture bind now costs
|
||||
// are swallowed by the emitter's own set-hash suppressor, which MGPipeTypes.h makes
|
||||
// mandatory for every kVarTail set for this exact traffic.
|
||||
now[Index(MGPipeDirty::NewSamplers)] = MGPipeMixShutter(
|
||||
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration()),
|
||||
ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewShaderImages)] =
|
||||
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages);
|
||||
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
|
||||
@@ -380,6 +587,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_renderStateVersion.Reset();
|
||||
m_pipelineStateVersion.Reset();
|
||||
m_framebufferBind.Reset();
|
||||
m_readFramebufferBind.Reset();
|
||||
m_indexSlotVersion.Reset();
|
||||
m_pack = PixelStoreParameters{};
|
||||
m_patch = PatchTrio{};
|
||||
@@ -469,6 +677,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
|
||||
// that wrapped would let a composite shutter repeat and cost a missed fire.
|
||||
MGPipeWidenedCounter m_framebufferBind;
|
||||
// P4a: the READ framebuffer binding slot's version, its own counter for the same
|
||||
// reason the draw one exists. Two counters rather than one over both slots: a single
|
||||
// widened counter fed two independent Uint16s reads a decrease as a wrap on every
|
||||
// alternation and would add 65536 per switch, which costs nothing in correctness
|
||||
// (over-firing) but makes the high word meaningless.
|
||||
MGPipeWidenedCounter m_readFramebufferBind;
|
||||
// The BOUND VAO's element-array slot version, widened for the same reason. One
|
||||
// counter over a slot that changes with the bound VAO: a stale high word can only
|
||||
// ADD a fire, never drop one, and the VAO identity in the same mix is what makes a
|
||||
@@ -521,8 +735,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// The monolith's one tracker. Under split there is one per client context; the context
|
||||
// identity check inside Update is what makes the single instance safe today.
|
||||
inline MGPipeTracker& MGPipeTrackerInstance() {
|
||||
static MGPipeTracker tracker;
|
||||
return tracker;
|
||||
// NEVER DESTROYED, for MGPipeSlots()' reason (MG_Impl/Pipe/SlotAllocator.cpp). The
|
||||
// rule is stated over the SET of MGPipe process singletons rather than over the two
|
||||
// that a frontend destructor reaches today: which of them a destructor reaches is a
|
||||
// property of the emitters, and the emitters change (C-1 added a second reaching
|
||||
// path in one commit). One allocation per process, no destructor to lose - this type
|
||||
// has none - and nothing can then answer a late call out of freed storage.
|
||||
static MGPipeTracker* tracker = new MGPipeTracker();
|
||||
return *tracker;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -431,8 +431,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// The monolith's one vertex-input emitter, beside the tracker, the CSO cache, the
|
||||
// set-hash suppressor and the resource tracker.
|
||||
inline MGPipeVertexInputEmitter& MGPipeVertexInputEmitterInstance() {
|
||||
static MGPipeVertexInputEmitter emitter;
|
||||
return emitter;
|
||||
// NEVER DESTROYED, for MGPipeSlots()' reason (MG_Impl/Pipe/SlotAllocator.cpp), and
|
||||
// this one is not hypothetical: C-1 put this emitter DIRECTLY on ~VertexArrayObject's
|
||||
// path - MGPipeEmitVertexElementsDestroyAndFree asks RecordIsPublished(handle) and
|
||||
// then NoteRecordDestroyed(handle), which read and WRITE m_latch. A destroyed
|
||||
// emitter answers out of a freed Vector and the write grows it, i.e. an operator
|
||||
// new + memcpy + operator delete on an already-freed block.
|
||||
static MGPipeVertexInputEmitter* emitter = new MGPipeVertexInputEmitter();
|
||||
return *emitter;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -195,6 +195,46 @@
|
||||
// resolution reads the applier's BoundVertexElements instead of the object - at which point
|
||||
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
|
||||
// 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) \
|
||||
X(GetBlendColor, SetDynamicState) \
|
||||
X(GetBlendEquationIndexed, CreateRenderState) \
|
||||
@@ -210,8 +250,11 @@
|
||||
X(GetDepthFunc, CreateRenderState) \
|
||||
X(GetDepthMask, CreateRenderState) \
|
||||
X(GetDepthRangeIndexed, SetDynamicState) \
|
||||
X(GetFramebufferBindingSlot, SetFramebufferState) \
|
||||
X(GetImageTextureBinding, SetShaderImages) \
|
||||
X(GetLineWidth, SetDynamicState) \
|
||||
X(GetLogicOp, CreateRenderState) \
|
||||
X(GetMaxTouchedTextureUnit, SetSamplerViews) \
|
||||
X(GetMinSampleShadingValue, CreateRenderState) \
|
||||
X(GetPatchDefaultInnerLevel, SetPatchState) \
|
||||
X(GetPatchDefaultOuterLevel, SetPatchState) \
|
||||
@@ -221,11 +264,14 @@
|
||||
X(GetPolygonOffsetFactor, SetDynamicState) \
|
||||
X(GetPolygonOffsetUnits, SetDynamicState) \
|
||||
X(GetPrimitiveRestartIndex, SetDynamicState) \
|
||||
X(GetProgramForDispatch, SetDispatchProgram) \
|
||||
X(GetProgramForDraw, SetDrawProgram) \
|
||||
X(GetProvokingVertexMode, CreateRenderState) \
|
||||
X(GetRenderStateParameters, CreateRenderState) \
|
||||
X(GetRenderStateParametersVersion, BindRenderState) \
|
||||
X(GetScissorBox, SetDynamicState) \
|
||||
X(GetStencilState, CreateRenderState) \
|
||||
X(GetTextureUnitObject, SetSamplerViews) \
|
||||
X(GetViewport, SetDynamicState) \
|
||||
X(GetViewportIndexed, SetDynamicState) \
|
||||
X(IsCapabilityEnabled, CreateRenderState) \
|
||||
|
||||
@@ -154,6 +154,21 @@
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
|
||||
// X(Mutator, Answer)
|
||||
@@ -279,6 +294,34 @@
|
||||
/* 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. */ \
|
||||
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 */ \
|
||||
/* 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 */ \
|
||||
@@ -296,9 +339,24 @@
|
||||
/* Destroyed consumer, package espryt), not the client's: the client mints */ \
|
||||
/* 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. */ \
|
||||
/* 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(MarkFramebufferObjectForDeletion, kExplicitDestroy) \
|
||||
X(MarkProgramForDeletion, kUnpublishedDestroy) \
|
||||
X(MarkProgramForDeletion, kExplicitDestroy) \
|
||||
X(MarkProgramPipelineForDeletion, kUnpublishedDestroy) \
|
||||
X(MarkRenderbufferObjectForDeletion, kExplicitDestroy) \
|
||||
X(MarkSamplerObjectForDeletion, kExplicitDestroy) \
|
||||
@@ -326,9 +384,32 @@
|
||||
// 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
|
||||
// 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
|
||||
// level. The ten mutators that reach a tainted body (--check prints the count) all carry a
|
||||
// prose answer, which no derivation checks.
|
||||
#define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X)
|
||||
// row nor outlive its reason.
|
||||
//
|
||||
// IT WAS EMPTY UNTIL P4a, and it stops being empty for a reason that is a property of the
|
||||
// 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
|
||||
|
||||
@@ -83,7 +83,30 @@ namespace MobileGL::MG_Pipe {
|
||||
// the other.
|
||||
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7;
|
||||
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
|
||||
// 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).
|
||||
@@ -93,6 +116,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// "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 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
|
||||
// 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
|
||||
// ShaderCso. Reserving a band rather than a flag keeps the composite resolver's
|
||||
// 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 kMGPipeShaderCsoCompositeSlotBase =
|
||||
kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4);
|
||||
|
||||
+466
-22
@@ -145,15 +145,152 @@ namespace MobileGL::MG_Pipe {
|
||||
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
|
||||
"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
|
||||
// share one create/respecify shape (section 4.5.1).
|
||||
struct MGPResourceDesc {
|
||||
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)
|
||||
// VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|
|
||||
// DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The ELEMENT_ARRAY bit is the
|
||||
// D-B7 switch: with kCapNeedsHostIndexBytes set the server mirrors this resource.
|
||||
// MGPipeBindBit, above: VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|
|
||||
// SHADER_IMAGE|RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY. The
|
||||
// ELEMENT_ARRAY bit is the D-B7 switch: with kCapNeedsHostIndexBytes set the server
|
||||
// mirrors this resource.
|
||||
Uint16 BindMask;
|
||||
Uint32 InternalFormat; // already resolved to an uncompressed fallback by the client
|
||||
Uint32 Width, Height, Depth;
|
||||
@@ -161,9 +298,16 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint8 FixedSampleLocations, Immutable;
|
||||
Uint32 Usage; // BufferUsage
|
||||
Uint32 StorageFlags; // glBufferStorage flags
|
||||
Uint8 HasDefinedContent; // false after a NULL-data respecify
|
||||
Uint8 HasDefinedContent; // false after a NULL-data respecify - STORAGE-DEFINING
|
||||
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
|
||||
Uint16 Pad0;
|
||||
// ImageBindableHint and BindMask above are the two METADATA fields the rule exists
|
||||
// for: a respecify that moves only them - every storage-defining field equal to the
|
||||
// stored descriptor - is a metadata update, with no reallocation ack and no
|
||||
// PendingUploads clear. The rule, the full storage-defining field set and the third
|
||||
// field allowed to differ (GlNameForDiag) are stated beside
|
||||
// MGPipeResourceRespecifyNeedsAck below (P4a, ID-18 M4), which is where the applier
|
||||
// and both twins read them from.
|
||||
// Diagnostics only. A GL name is NEVER an identity, never a memo key and never part
|
||||
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
|
||||
// cannot hold one.
|
||||
@@ -304,18 +448,49 @@ namespace MobileGL::MG_Pipe {
|
||||
MGP_ASSERT_POD(MGPSamplerView, 36);
|
||||
|
||||
// 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 {
|
||||
MGPipeHandle Res;
|
||||
Uint16 BaseLevel, MaxLevel;
|
||||
Uint8 Swizzle[4];
|
||||
Uint8 DepthStencilMode;
|
||||
MGPipeHandle Res; // 0
|
||||
// Kind SamplerCso. kMGPipeNullHandle is ILLEGAL - every texture object owns a sampler
|
||||
// object, so a null here is Fatal{ProtocolCorruption} rather than "no sampler".
|
||||
MGPipeHandle BuiltinSampler; // 8
|
||||
Uint16 BaseLevel, MaxLevel; // 16
|
||||
Uint8 Swizzle[4]; // 20
|
||||
Uint8 DepthStencilMode; // 24
|
||||
// Mirrors m_forceTextureParamsResync: the widened-channel carrier needs a swizzle
|
||||
// override that the frontend params version does not move for.
|
||||
Uint8 ForceResync;
|
||||
Uint8 Pad0[2];
|
||||
Float MinLod, MaxLod, LodBias;
|
||||
Uint8 ForceResync; // 25
|
||||
// Mirrors m_forceSamplerResync, which had no wire spelling at all before P4a. What it
|
||||
// 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);
|
||||
|
||||
// MGPTextureParams::DepthStencilMode's two legal values, and the ONLY spelling of them
|
||||
// (P4a, ID-12 / esprytobj DV-2). The frontend keeps a GLenum - GL_DEPTH_COMPONENT 0x1902,
|
||||
// GL_STENCIL_INDEX 0x1901 - and a Uint8 cannot hold one, so the aspect is NUMBERED here
|
||||
// rather than truncated there. The GLenum -> byte helper belongs to the client emitter;
|
||||
// this header owns the two numbers, so the emitter and both backends cannot disagree.
|
||||
//
|
||||
// 0 IS DEPTH, AND THAT IS THE WHOLE REASON FOR THIS ORDER RATHER THAN THE ENUM'S LOW BYTE.
|
||||
// GL_DEPTH_COMPONENT is the GL initial value of GL_DEPTH_STENCIL_TEXTURE_MODE and a
|
||||
// texture that never asks for the stencil aspect never emits the call at all, so A ZEROED
|
||||
// RECORD MUST DECODE TO EXACTLY WHAT AN UNTOUCHED TEXTURE ALREADY HAS. Numbering by the
|
||||
// low byte would have made depth 0x02 and stencil 0x01 and left zero meaning nothing.
|
||||
inline constexpr Uint8 kMGPipeDepthStencilModeDepth = 0; // GL_DEPTH_COMPONENT
|
||||
inline constexpr Uint8 kMGPipeDepthStencilModeStencil = 1; // GL_STENCIL_INDEX
|
||||
|
||||
// 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
|
||||
@@ -338,35 +513,183 @@ namespace MobileGL::MG_Pipe {
|
||||
// set_*
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// MGPSurface::Kind's three values (P4a, ID-12 / esprytobj DV-4). MGPipeKind is REUSED
|
||||
// rather than a second three-value enum minted beside it: it already spells Texture and
|
||||
// Renderbuffer, its None is 0, and a zero-initialised MGPSurface is therefore ALREADY the
|
||||
// empty attachment point this record describes - {Res = kMGPipeNullHandle, Kind = None}
|
||||
// and every other field zero. The static_assert is what keeps that true if MGPipeKind is
|
||||
// ever reordered.
|
||||
inline constexpr Uint8 kMGPipeSurfaceKindNone = static_cast<Uint8>(MGPipeKind::None);
|
||||
inline constexpr Uint8 kMGPipeSurfaceKindTexture = static_cast<Uint8>(MGPipeKind::Texture);
|
||||
inline constexpr Uint8 kMGPipeSurfaceKindRenderbuffer =
|
||||
static_cast<Uint8>(MGPipeKind::Renderbuffer);
|
||||
static_assert(kMGPipeSurfaceKindNone == 0,
|
||||
"a zero-initialised MGPSurface must already be the empty attachment point");
|
||||
|
||||
// MGPSurface::TextureTarget for a point that names no texture: the renderbuffer point and
|
||||
// the empty point both carry it. It is MobileGL::TextureTarget::Unknown, which is -1 and
|
||||
// therefore 0xFFFF in the field's Uint16 - a value no real target has, so a reader that
|
||||
// forgets to gate on Kind gets a nonsense target rather than a plausible wrong one.
|
||||
inline constexpr Uint16 kMGPipeSurfaceNoTextureTarget = 0xFFFF;
|
||||
static_assert(kMGPipeSurfaceNoTextureTarget ==
|
||||
static_cast<Uint16>(MobileGL::TextureTarget::Unknown),
|
||||
"kMGPipeSurfaceNoTextureTarget is TextureTarget::Unknown widened to the "
|
||||
"field, and MG_State moved Unknown off -1");
|
||||
|
||||
// = pipe_surface. internalFormat is INLINE so the four cross-object masks fall out at
|
||||
// push time with no lookup (section 4.5.6).
|
||||
struct MGPSurface {
|
||||
MGPipeHandle Res;
|
||||
Uint32 InternalFormat;
|
||||
Uint8 Kind; // Texture | Renderbuffer | None
|
||||
Uint8 Kind; // kMGPipeSurfaceKind{None,Texture,Renderbuffer}, above
|
||||
Uint8 Layered;
|
||||
Uint16 Level;
|
||||
Uint32 Layer;
|
||||
Uint16 UploadTarget;
|
||||
Uint16 Pad0;
|
||||
Uint16 UploadTarget; // static_cast<Uint16>(MobileGL::TextureUploadTarget)
|
||||
// P4a, ID-12 / esprytobj DV-5: WAS Pad0, and the size did not move - the two bytes
|
||||
// were already here. static_cast<Uint16>(MobileGL::TextureTarget), and
|
||||
// kMGPipeSurfaceNoTextureTarget on every point that is not a texture.
|
||||
//
|
||||
// THE FOUR CROSS-OBJECT MASKS ARE WHY IT EXISTS. IsSnormFallbackAttachment,
|
||||
// IsUnormFallbackAttachment and IsAlphaWidenedColorAttachment all reduce to
|
||||
// (format, TEXTURE TARGET) - ShouldUseCaveatTextureFormat(format, target) and
|
||||
// BackendTextureFormatAddsAlpha(format, target) - and no TextureUploadTarget ->
|
||||
// TextureTarget inverse exists anywhere in the tree, so UploadTarget cannot answer
|
||||
// them. Without this field D-C1's promise that the inline InternalFormat makes the
|
||||
// masks "fall out at push time with no lookup" is unkeepable and the backend keeps
|
||||
// reading the frontend attachment objects.
|
||||
//
|
||||
// CONSULTED ONLY WHEN Kind == kMGPipeSurfaceKindTexture. A zero-initialised record
|
||||
// carries 0, which is TextureTarget::Texture1D and not the sentinel; that is not a
|
||||
// defect, because such a record is Kind == None and names no texture at all. Gating
|
||||
// on Kind is the reader's contract.
|
||||
Uint16 TextureTarget;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSurface, 24);
|
||||
|
||||
// P4a, D-C2/D-C3 and ID-19: THE RECORD DESCRIBES A FRAMEBUFFER OBJECT, and Target says
|
||||
// whether it ALSO moves a binding.
|
||||
//
|
||||
// A `Named` record describes the framebuffer object it names (Fbo) and changes NO
|
||||
// binding. `Draw` / `Read` / `Both` records describe the same object AND set the bound
|
||||
// handle(s) of the target(s) they name.
|
||||
//
|
||||
// The applier therefore keeps records PER FRAMEBUFFER OBJECT, keyed by the handle's slot
|
||||
// (the generation is checked on lookup and a stale one refuses; a framebuffer has no wire
|
||||
// lifetime - D-I2, the catalogue has no framebuffer delete - so a successor's record
|
||||
// simply OVERWRITES the slot), plus the two bound handles. And every DSA entry point that
|
||||
// hands a framebuffer to the server BY NAME - BlitNamedFramebuffer, the four
|
||||
// ClearNamedFramebuffer*, and the DSA attachment / draw-buffer / read-buffer setters at
|
||||
// their validate point - is PRECEDED BY A Named RECORD, so that any framebuffer the
|
||||
// server is about to receive by name already has one.
|
||||
//
|
||||
// That last rule is the phase's main correction, not a nicety. With only the two
|
||||
// bound-target records, glClearNamedFramebufferfv(fbo) on an unbound fbo made the backend
|
||||
// mint a fresh driver framebuffer with NO ATTACHMENTS, find no record for it, decline,
|
||||
// and then issue the clear against it anyway - GL_INVALID_FRAMEBUFFER_OPERATION and
|
||||
// nothing cleared, where the legacy arm cleared correctly. Writing such an object into
|
||||
// the bound-target record instead would have been worse: the applier would then claim it
|
||||
// is bound.
|
||||
//
|
||||
// GL has two independent framebuffer bindings and one record carries one Fbo, so a
|
||||
// Draw/Read pair is two records and one object bound to both targets is one record with
|
||||
// Both. The draw-buffer array belongs to the OBJECT the record names; it reaches the
|
||||
// driver's bound draw framebuffer only for a record whose Target is Draw or Both -
|
||||
// Espryt's own comment records the Minecraft 26.x OIT bug where a READ-only sync landed
|
||||
// glDrawBuffers on the wrong framebuffer - and a Named record's draw buffers are applied
|
||||
// when that object is next configured, never to whatever happens to be bound. ReadSurface
|
||||
// is resolved from THAT framebuffer's own read buffer in EVERY record, Named included,
|
||||
// which is what makes the read-buffer-shared-FBO defect class unrepresentable rather than
|
||||
// merely fixed.
|
||||
//
|
||||
// Target IS A ContentHash INPUT (the hash covers the whole record), and the emitter's
|
||||
// suppressor must be keyed by the framebuffer the record names, not by one global slot:
|
||||
// two different objects' Named records in a row must both go out, and a Named record must
|
||||
// never be suppressed against the same object's bound record or the reverse.
|
||||
enum class MGPipeFramebufferTarget : Uint8 {
|
||||
Draw = 0,
|
||||
Read = 1,
|
||||
Both = 2,
|
||||
// Describes the framebuffer named by Fbo and changes no binding (ID-19). Emitted
|
||||
// ahead of every DSA entry point that hands that framebuffer over by name.
|
||||
Named = 3,
|
||||
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");
|
||||
|
||||
// ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by Fbo - and Target says
|
||||
// whether it also moves a binding (P4a, D-C2 as corrected by ID-19; see
|
||||
// MGPipeFramebufferTarget above for the failure that forced it).
|
||||
//
|
||||
// A Named record describes that object and changes NO binding. Draw / Read / Both records
|
||||
// describe that object AND set the bound handle(s) of the target(s) they name. The applier
|
||||
// keeps these records PER FRAMEBUFFER OBJECT, keyed by Fbo's slot (generation checked on
|
||||
// lookup; a framebuffer has no wire lifetime - D-I2 - so a successor's record simply
|
||||
// overwrites the slot), plus the two bound handles; every DSA entry point that hands a
|
||||
// framebuffer to the server by name is preceded by a Named record.
|
||||
//
|
||||
// WHAT Target CHANGES, FIELD BY FIELD. NO FIELD IN THIS RECORD REFERS TO "the currently
|
||||
// bound framebuffer" - every one of them describes the object named by Fbo - and that is
|
||||
// the invariant a reader depends on:
|
||||
//
|
||||
// Fbo, Color[], Depth, Stencil, ReadSurface, Width/Height/Layers/Samples,
|
||||
// FixedSampleLocations, IsDefault, Complete
|
||||
// properties of the object named by Fbo, identical in meaning under every Target.
|
||||
// In particular ReadSurface is resolved from THAT framebuffer's own read buffer -
|
||||
// on a Named record too - never from whichever framebuffer is bound to GL_READ.
|
||||
// DrawBuffers[]
|
||||
// a property of the named object; it reaches the driver's bound draw framebuffer
|
||||
// only when Target is Draw or Both. Under Named it is stored with the object and
|
||||
// applied when that object is next configured.
|
||||
// Target
|
||||
// the only binding-specific field: Draw/Read/Both name the binding(s) this record
|
||||
// also sets, Named names none. It is a ContentHash input.
|
||||
// ContentHash
|
||||
// per RECORD, not per object, and the emitter's suppressor is keyed by the
|
||||
// framebuffer named: a Named record must never be suppressed against the same
|
||||
// object's bound record, nor one object's Named record against another's.
|
||||
struct MGPFramebufferState {
|
||||
MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer
|
||||
MGPSurface Color[8];
|
||||
MGPSurface Depth, Stencil;
|
||||
// The RESOLVED read surface, not an index. This is what structurally closes the
|
||||
// The RESOLVED read surface, not an index, and it is THIS framebuffer's own read
|
||||
// buffer under every Target - Named included. This is what structurally closes the
|
||||
// read-buffer-shared-FBO defect class.
|
||||
MGPSurface ReadSurface;
|
||||
Int8 DrawBuffers[8]; // attachment index, -1 = NONE
|
||||
// attachment index, -1 = NONE. The named object's array; applied to the bound draw
|
||||
// framebuffer only when Target is Draw or Both.
|
||||
Int8 DrawBuffers[8];
|
||||
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;
|
||||
// MGPipeFramebufferTarget, above (P4a, D-C2; was Pad0). Draw/Read/Both also set the
|
||||
// named binding(s); Named sets none (ID-19). The ONLY binding-specific field.
|
||||
Uint8 Target;
|
||||
Uint32 Pad1;
|
||||
// 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.
|
||||
// The same pattern is mandatory for every kVarTail set_* below, or 26.2's
|
||||
// redundant glBindSampler traffic reappears as a variable-length record per batch.
|
||||
// Target is one of its inputs, and the suppressor is keyed per framebuffer.
|
||||
Uint64 ContentHash;
|
||||
};
|
||||
MGP_ASSERT_POD(MGPFramebufferState, 304);
|
||||
@@ -378,6 +701,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// static_assert, because this header may not include a frontend one.
|
||||
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 {
|
||||
MGPipeHandle Res;
|
||||
Uint64 Offset;
|
||||
@@ -617,6 +954,30 @@ namespace MobileGL::MG_Pipe {
|
||||
// decision belongs on the side that pays the GPU cost. Mali prices texture upload by
|
||||
// JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame.
|
||||
//
|
||||
// `Target` IS TWO FACTS IN ONE Uint16 (P4a, D-D3 / ID-12), and MGPipePackSubDataTarget
|
||||
// under the struct is the only spelling of the encoding - nothing may open-code a half:
|
||||
//
|
||||
// low byte = MGPipeResourceTarget - WHICH KIND of storage the destination is.
|
||||
// The applier branches on it: a buffer
|
||||
// target dispatches into MGPipeResourceOps,
|
||||
// every other target accumulates a pending
|
||||
// upload for the texture sync to consume.
|
||||
// high byte = MobileGL::TextureUploadTarget - WHICH cube face / upload target the level
|
||||
// belongs to. It is NOT derivable from the
|
||||
// resource target - six faces share TexCube
|
||||
// - and 26 enumerators leave a byte ample.
|
||||
//
|
||||
// WHY THAT WAY ROUND, AND WHY THE ENCODING LIVES HERE RATHER THAN IN EACH EMITTER. The
|
||||
// applier's SubDataNamesABuffer tests the WHOLE field == 0, and
|
||||
// TextureUploadTarget::Texture1D is 0 - so a texture record carrying the bare upload
|
||||
// enumerator is indistinguishable from a buffer record exactly when its owner is a 1D
|
||||
// texture, and that texture's upload is dispatched into the buffer path. With the
|
||||
// resource target in the LOW byte a buffer record's Target stays EXACTLY
|
||||
// kMGPipeResourceTargetBuffer - P3a's buffer records are unchanged on the wire, their
|
||||
// upload byte being zero too - while a texture record can never be zero, because no
|
||||
// texture's MGPipeResourceTarget is. The static_assert under the struct holds that
|
||||
// invariant, and the applier's whole-field test stays right either way.
|
||||
//
|
||||
// THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination
|
||||
// byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte
|
||||
// offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0.
|
||||
@@ -635,6 +996,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// the only spelling of this convention; nothing else reads the box for a buffer.
|
||||
struct MGPSubData {
|
||||
MGPipeHandle Res;
|
||||
// Target is PACKED - see the block above, and read it only through
|
||||
// MGPipeSubDataResourceTargetOf / MGPipeSubDataUploadTargetOf below.
|
||||
Uint16 Target, Level;
|
||||
// Replaces the backend's `uploadData == mipData` pointer comparison: are these
|
||||
// bytes an untransformed level shadow?
|
||||
@@ -647,6 +1010,38 @@ namespace MobileGL::MG_Pipe {
|
||||
};
|
||||
MGP_ASSERT_POD(MGPSubData, 72);
|
||||
|
||||
// The one spelling of MGPSubData::Target's encoding, stated above the struct.
|
||||
//
|
||||
// Uint32 ARGUMENTS RATHER THAN THE TWO ENUM TYPES, and that is deliberate. This header is
|
||||
// the contract: MGPipeResourceTarget is minted in it, but the upload half is MG_State's
|
||||
// TextureUploadTarget, and nobody who reads the packed field ever needs that type - the
|
||||
// applier and both backends read the halves BACK, as bytes, through the two accessors.
|
||||
// Naming it in a signature would pin the contract's own API to a frontend enum for no
|
||||
// reader's benefit, and would stop kMGPipeResourceTargetBuffer being passed as it stands.
|
||||
// Callers pass static_cast<Uint32>(MobileGL::TextureUploadTarget) for `uploadTarget` and
|
||||
// static_cast<Uint32>(MGPipeResourceTarget) - or kMGPipeResourceTargetBuffer - for
|
||||
// `resourceTarget`.
|
||||
constexpr inline Uint16 MGPipePackSubDataTarget(Uint32 resourceTarget, Uint32 uploadTarget) {
|
||||
return static_cast<Uint16>((resourceTarget & 0xFFu) | ((uploadTarget & 0xFFu) << 8));
|
||||
}
|
||||
// Comparable against static_cast<Uint8>(MGPipeResourceTarget) / kMGPipeResourceTargetBuffer.
|
||||
constexpr inline Uint8 MGPipeSubDataResourceTargetOf(Uint16 packed) {
|
||||
return static_cast<Uint8>(packed & 0xFFu);
|
||||
}
|
||||
// Comparable against static_cast<Uint8>(MobileGL::TextureUploadTarget).
|
||||
constexpr inline Uint8 MGPipeSubDataUploadTargetOf(Uint16 packed) {
|
||||
return static_cast<Uint8>((packed >> 8) & 0xFFu);
|
||||
}
|
||||
// THE INVARIANT P3a's records and the applier's buffer test both rest on: a buffer
|
||||
// record's Target is exactly kMGPipeResourceTargetBuffer, whole field, upload byte and
|
||||
// all. TextureUploadTarget::Texture1D is 0, so the buffer case is the one place where the
|
||||
// packed form and a bare enumerator agree - and it has to stay that place.
|
||||
static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u) ==
|
||||
kMGPipeResourceTargetBuffer,
|
||||
"a buffer sub-data record's Target must stay exactly "
|
||||
"kMGPipeResourceTargetBuffer: the applier's SubDataNamesABuffer tests the "
|
||||
"whole field == 0");
|
||||
|
||||
// Encodes a buffer byte range into the record's box. False, with the record untouched,
|
||||
// when the range does not fit one record: the emitter has to split it.
|
||||
inline Bool MGPipeSetSubDataBufferRange(MGPSubData& record, Uint64 offset, Uint64 size) {
|
||||
@@ -675,12 +1070,61 @@ namespace MobileGL::MG_Pipe {
|
||||
// is ((void)0) - the applier is one function call away - and the transport wires the
|
||||
// doorbell to this predicate when it lands.
|
||||
//
|
||||
// Immutable is exactly the right discriminator: it is set iff the store came from a
|
||||
// glBufferStorage* entry point, which is the definition of the allowed case.
|
||||
// P4a, D-A2: THE PREDICATE IS NARROWED TO NAME THE BUFFER TARGET, and that is a
|
||||
// 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) {
|
||||
return desc.Immutable != 0;
|
||||
return desc.Immutable != 0 && desc.Target == kMGPipeResourceTargetBuffer;
|
||||
}
|
||||
|
||||
// P4a, ID-18 M4: A RESPECIFY WHOSE STORAGE-DEFINING FIELDS ALL EQUAL THE STORED
|
||||
// DESCRIPTOR IS A METADATA UPDATE, NOT A REALLOCATION.
|
||||
//
|
||||
// MGPResourceDesc::BindMask and ImageBindableHint are STICKY facts the client discovers
|
||||
// AFTER allocation - a texture first bound as a shader image, first used as a render
|
||||
// target - and they ride resource_create and every resource_respecify. An IMMUTABLE
|
||||
// texture never has a later respecify, so without a rule those two would reach the server
|
||||
// only by accident, or never; with one, a mask change after allocation emits a
|
||||
// resource_respecify that REPEATS the storage the resource already has.
|
||||
//
|
||||
// The applier and both twins must classify such a record as a metadata update:
|
||||
// - NO reallocation acknowledgement. MGPipeResourceRespecifyNeedsAck above still
|
||||
// answers the per-record question, but a metadata update allocates nothing, so a
|
||||
// record it classifies as metadata is not acked even when that predicate says the
|
||||
// call may require one.
|
||||
// - NO PendingUploads clear - not the whole vector, and not the redefined level either.
|
||||
// This REFINES the level-scoped clear: identical storage fields clear NOTHING. (The
|
||||
// level-scoped rule exists because clearing the whole vector on a level-1 definition
|
||||
// silently dropped level 0's accepted texels; a metadata update must drop neither.)
|
||||
// - The stored descriptor's BindMask and ImageBindableHint ARE updated - BindMask is
|
||||
// sticky and therefore ORed, never replaced - and the twin re-derives its storage
|
||||
// flags from the new mask on its next sync, recreating backend storage only where the
|
||||
// backend actually needs it. The record itself is not a request to recreate.
|
||||
//
|
||||
// THE STORAGE-DEFINING FIELD SET, named here so that neither side has to guess and a
|
||||
// later field cannot join it by silence. It is every MGPResourceDesc member except the
|
||||
// three metadata ones and the padding:
|
||||
//
|
||||
// Target, StorageKind, InternalFormat, Width, Height, Depth, ArrayLayers, Levels,
|
||||
// Samples, FixedSampleLocations, Immutable, Usage, StorageFlags, HasDefinedContent,
|
||||
// ViewOf, BufferForTexBuffer, BufOffset, BufSize.
|
||||
//
|
||||
// `Resource` is the identity the stored descriptor is looked up BY, not a comparand. The
|
||||
// three fields that may differ on a metadata update are exactly BindMask,
|
||||
// ImageBindableHint and GlNameForDiag (diagnostics only, never an identity, never a memo
|
||||
// key). HasDefinedContent is storage-defining ON PURPOSE: glBufferData(size, NULL) at an
|
||||
// unchanged size is an orphaning reallocation and has to keep clearing, rather than being
|
||||
// mistaken for a mask change. A field added to MGPResourceDesc must be placed in one of
|
||||
// the two lists in the same commit - PipeCatalogue pins the struct's size for that.
|
||||
|
||||
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
|
||||
// zero regions - that is how a pull that needs nothing is answered.
|
||||
struct MGPSubDataComplete {
|
||||
|
||||
+1375
-27
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,17 @@
|
||||
// (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.
|
||||
// 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 {
|
||||
struct PipeInputs;
|
||||
|
||||
@@ -109,8 +120,91 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
// 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 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");
|
||||
// ID-19(b): the framebuffer record is now PER OBJECT and its table is slot-indexed like the
|
||||
// five above, so it takes a bound on the same terms. A framebuffer record is 304 bytes and
|
||||
// an FBO is a CONTAINER object - not shared between contexts, minted a few dozen at a time
|
||||
// by a renderer and a few hundred by a shader pack - so 1<<16 is orders of magnitude above
|
||||
// any live population and still turns a corrupt Uint32 into a refusal rather than a
|
||||
// 4-billion-entry resize.
|
||||
inline constexpr Uint32 kMGPipeMaxFramebufferSlots = 1u << 16;
|
||||
|
||||
// THE FOURTH set_framebuffer_state TARGET is the contract's MGPipeFramebufferTarget::Named (c0e):
|
||||
// "this record describes the framebuffer it names; no binding changes." Draw / Read / Both
|
||||
// write the record AND set the bound handle(s); Named writes the record only, which is how
|
||||
// the DSA entry points - BlitNamedFramebuffer and the four ClearNamedFramebuffer* - hand the
|
||||
// server a record for a framebuffer bound to neither binding (esprytobj review C-1, ID-19).
|
||||
|
||||
// The two framebuffer BINDINGS, and there are two rather than three: Both and Named are
|
||||
// things a RECORD says, not bindings a server has. MGPipeApplierState::BoundFramebuffer is
|
||||
// indexed by MGPipeFramebufferTarget::Draw / ::Read, which is what makes package D's
|
||||
// "is this framebuffer the one bound to target t" one array compare (ID-19(d)).
|
||||
inline constexpr Uint32 kMGPipeFramebufferBindingCount = 2;
|
||||
static_assert(static_cast<Uint8>(MGPipeFramebufferTarget::Draw) == 0 &&
|
||||
static_cast<Uint8>(MGPipeFramebufferTarget::Read) == 1,
|
||||
"BoundFramebuffer is indexed by the target byte; Draw and Read must be 0 and 1");
|
||||
|
||||
// ---- P4a's SHAPE bounds, and they are the same argument the slot bounds above make, one
|
||||
// level down: every number below arrives inside a payload, every one of them decides how
|
||||
// much the applier allocates or how far it indexes, and NONE of them is ever allocated by
|
||||
// being named. A record that names one past its bound is Fatal{ProtocolCorruption} - the
|
||||
// verdict this file reserves for a record that would make the server act outside its own
|
||||
// storage - and never a resize.
|
||||
|
||||
// A sub-data record's mip level. GL's own bound is log2 of the maximum texture size, which
|
||||
// no device reports above 2^16, so a level index of 32 addresses a texture no
|
||||
// implementation can allocate and is a corrupt record rather than a large one. It is NOT
|
||||
// MGPTextureParams::MaxLevel's bound: GL_TEXTURE_MAX_LEVEL defaults to 1000 and is a
|
||||
// parameter, not a storage level, so nothing here polices it.
|
||||
inline constexpr Uint16 kMGPipeMaxTextureLevels = 32;
|
||||
|
||||
// The pending-upload set (below) is keyed by (UploadTarget, Level) and both halves come
|
||||
// off the wire. Levels are bounded above; upload targets are not - a cube face, an array
|
||||
// target and a rectangle target are all legal values - so the number of DISTINCT keys one
|
||||
// resource may accumulate is bounded here. Six cube faces times 32 levels is 192; 256
|
||||
// leaves room for a target space this phase has not enumerated and still refuses the
|
||||
// unbounded growth a corrupt Uint16 would otherwise buy.
|
||||
inline constexpr Uint32 kMGPipeMaxPendingUploads = 256;
|
||||
|
||||
// The rect list behind one pending entry. The frontend keeps at most MipmapStorage's
|
||||
// kMaxDirtyRects = 96 per level and answers "0 rects" for everything it cannot describe
|
||||
// that way, which is the model this mirrors: an accumulation that would exceed this
|
||||
// collapses to BOX ONLY - the same answer, with the same meaning, and never a dropped
|
||||
// region. 256 is that bound with room for several emissions accumulating behind a bail.
|
||||
inline constexpr Uint32 kMGPipeMaxPendingUploadRegions = 256;
|
||||
|
||||
// The default uniform block's image, the one allocation P4a adds per program. The size
|
||||
// comes from the program's own MGPProgramDesc::GlobalUboSize, so it is checked ONCE at
|
||||
// create_shader_state and the set_global_constants that follows can only allocate what the
|
||||
// create already declared. 16 MiB is four orders of magnitude above any default uniform
|
||||
// block a real program links and still turns a corrupt Uint32 into a refusal.
|
||||
inline constexpr Uint32 kMGPipeMaxGlobalConstantsBytes = 16u << 20;
|
||||
|
||||
// One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the
|
||||
// reserved null handle and is never live.
|
||||
@@ -129,6 +223,126 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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.
|
||||
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.
|
||||
//
|
||||
// Params.BuiltinSampler MAY NAME A CSO WHOSE RECORD IS GONE. set_texture_params
|
||||
// deliberately does not resolve it (the sampler CSO is content-addressed and shared,
|
||||
// D-F1, and the ordering between the two families is the emitter's), and
|
||||
// delete_sampler_state does not sweep the textures that name the CSO it drops. So a
|
||||
// consumer that follows this handle must expect SamplerCsos[slot] to be dead or
|
||||
// recycled and treat that as it treats any other stale handle - it is an ordering fact
|
||||
// about the two emitters, not a corrupt record.
|
||||
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.
|
||||
//
|
||||
// THE SET IS KEYED (UploadTarget, Level) AND EVERY KEY IS INDEPENDENT OF EVERY OTHER.
|
||||
// That is not a detail: a respecify redefines ONE level when it arrives from
|
||||
// glTexImage*D (MGPipeApplyResourceRespecify's trailing MGPRespecifiedLevel*), so it
|
||||
// may only drop that one key - the frontend's AllocateStorage / MarkStorageDirty are
|
||||
// per (uploadTarget, level) too, and the other levels' dirty flags were cleared at
|
||||
// THEIR emission, so nothing anywhere still owes them.
|
||||
//
|
||||
// THE ACCUMULATED RECT LIST MAY OVERLAP, AND A CONSUMER MUST TOLERATE THAT. Behind one
|
||||
// level the frontend's own model is pairwise disjoint (MipmapStorage keeps it so), but
|
||||
// this list CONCATENATES the lists of successive emissions and the applier's gate only
|
||||
// asks that each rect be inside the record's own union box - so two emissions that
|
||||
// touch the same texels leave two rects that do. Staging N rects therefore uploads
|
||||
// those texels twice, which is a cost and never a correctness problem; nothing here
|
||||
// de-duplicates and nothing downstream may assume "the frontend's model" means disjoint
|
||||
// once the shapes have been accumulated.
|
||||
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
|
||||
@@ -151,6 +365,27 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 ContentSerial = 0;
|
||||
};
|
||||
|
||||
// set_framebuffer_state's record, HELD PER FRAMEBUFFER OBJECT and indexed by the handle's
|
||||
// slot (ID-19(b)). It is the one record kind in this file whose object has NO WIRE LIFETIME:
|
||||
// the catalogue has no framebuffer create and no framebuffer destroy, because a framebuffer
|
||||
// is state and set_framebuffer_state is the only call that names one (D-I2). So there is
|
||||
// nothing to mark dead and nothing to refuse against, and a slot is simply OVERWRITTEN by
|
||||
// its successor's record - which is correct rather than merely tolerable, since the record
|
||||
// that reaches this table is the description of whatever object holds the slot NOW.
|
||||
//
|
||||
// `Live` is therefore NOT a lifetime. It means "a record has been written at this slot",
|
||||
// which is the only question a reader can ask: it separates a table entry that exists
|
||||
// because the vector grew past it from one an emission actually wrote. The GENERATION is
|
||||
// still checked on every lookup (P3a contract-review M2), and a mismatch is a LOUD refusal -
|
||||
// it means an emitter handed a stale handle, or minted a successor without describing it,
|
||||
// which is exactly the seam defect the DSA arm would otherwise turn into a blit into a
|
||||
// driver framebuffer with no attachments.
|
||||
struct MGPipeFramebufferRecord {
|
||||
Uint32 Gen = 0;
|
||||
Bool Live = false;
|
||||
MGPFramebufferState State{};
|
||||
};
|
||||
|
||||
struct MGPipeApplierState {
|
||||
// Indexed by slot; slot 0 is the reserved null handle and is never live
|
||||
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
|
||||
@@ -217,6 +452,43 @@ namespace MobileGL::MG_Pipe {
|
||||
Vector<MGPipeResourceRecord> Resources;
|
||||
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;
|
||||
// AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot,
|
||||
// for the reason MGPipeFramebufferRecord states: the two bound-target records the phase
|
||||
// started with could not describe a framebuffer that is bound to neither binding, and
|
||||
// the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand
|
||||
// Espryt exactly that.
|
||||
//
|
||||
// IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also
|
||||
// its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared
|
||||
// between contexts, but its record is addressed by a slot out of one global allocator,
|
||||
// so nothing aliases across a switch - and dropping the table would leave a
|
||||
// DSA-only framebuffer with no record and no event that would ever re-emit one (the
|
||||
// client's suppressor invalidation re-emits the two BOUND records and nothing else).
|
||||
Vector<MGPipeFramebufferRecord> FramebufferRecords;
|
||||
|
||||
// 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
|
||||
// under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no
|
||||
@@ -227,6 +499,29 @@ namespace MobileGL::MG_Pipe {
|
||||
// build. Per context, like the four render-state wire counters above.
|
||||
Uint64 RefusedResourceCalls = 0;
|
||||
Uint64 RefusedVertexInputCalls = 0;
|
||||
// P4a's, in the same shape and for the same reason: every 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 four, because the 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.
|
||||
//
|
||||
// set_framebuffer_state IS DELIBERATELY NOT ON THAT LIST AND CANNOT BE. D-I2 gives a
|
||||
// framebuffer a handle and NO wire lifetime, so the call resolves no record - there is
|
||||
// nothing to look up, nothing to find missing and therefore nothing to refuse - and
|
||||
// MGPSurface::Res is likewise left unresolved on purpose (D-I3: the keep-alives are the
|
||||
// frontend's SharedPtrs and enforcing them is a later phase's). Its only verdict is
|
||||
// Fatal{ProtocolCorruption} on a malformed record, and this counter must stay at 0
|
||||
// across every framebuffer call in every build. ID-19(b) does not change that: the
|
||||
// per-object table is WRITTEN by that call and never looked up by it, and the refusal
|
||||
// that the table CAN produce - a lookup whose generation has moved on - happens on the
|
||||
// server's own read path and is counted apart, in StaleFramebufferRecordLookups.
|
||||
//
|
||||
// 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
|
||||
// all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever
|
||||
@@ -271,6 +566,89 @@ namespace MobileGL::MG_Pipe {
|
||||
// operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the
|
||||
// applier-side observable a unit case reads without a stats window.
|
||||
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.
|
||||
|
||||
// WHICH FRAMEBUFFER IS BOUND TO EACH BINDING, and that is ALL this pair is since
|
||||
// ID-19(b): the record itself lives in FramebufferRecords above, keyed by the handle.
|
||||
// Indexed by MGPipeFramebufferTarget::Draw / ::Read. kMGPipeNullHandle means "nothing
|
||||
// described this binding yet", which is what a make-current leaves behind.
|
||||
//
|
||||
// set_framebuffer_state Draw / Read / Both writes the RECORD at state.Fbo's slot AND
|
||||
// sets the handle(s) here; Named (MGPipeFramebufferTarget::Named) writes the record and
|
||||
// touches nothing here at all - that is the whole of the fourth target's meaning.
|
||||
Array<MGPipeHandle, kMGPipeFramebufferBindingCount> BoundFramebuffer{};
|
||||
// ONE SERIAL FOR THE FAMILY, and it moves on EVERY write - a Named record's included,
|
||||
// because a twin memoising "the framebuffer state I have seen" has to hear about a
|
||||
// named framebuffer's attachments exactly as it hears about a bound one's. It is the
|
||||
// number that retires the four g_fboSynced* arrays and the twin's {slot version, object
|
||||
// version, backend id generation} triple.
|
||||
Uint64 FramebufferSerial = 0;
|
||||
// Every FramebufferRecordFor() that found a record at the slot whose GENERATION had
|
||||
// moved on. It is NOT RefusedObjectCalls: this is a READ by the server's own sync path
|
||||
// and not a call this applier refused, and set_framebuffer_state's counter contract
|
||||
// (below) is that no framebuffer call ever moves that one. A non-zero value here is a
|
||||
// seam defect - an emitter minted a successor for a recycled slot and never described
|
||||
// it, or handed out a handle it had already retired - so it is counted AND logged, and
|
||||
// a unit case reads it in every build for the reason the other counters exist.
|
||||
//
|
||||
// `mutable` because the three accessors below are const: package E holds the applier
|
||||
// through a `const auto&` and must keep doing so.
|
||||
mutable Uint64 StaleFramebufferRecordLookups = 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 THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
|
||||
// members because the storage moved under them and their callers must not have to know
|
||||
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
|
||||
// used to answer - "which record describes the framebuffer bound to this binding" - by
|
||||
// resolving BoundFramebuffer[t] through FramebufferRecords.
|
||||
//
|
||||
// NULL IS A REAL ANSWER AND HAS EXACTLY THREE CAUSES: nothing is bound to that binding
|
||||
// (the null handle, which is what a make-current leaves and is NOT an error), no record
|
||||
// has been written at that slot, or the slot's generation has moved on under the handle
|
||||
// (which IS an error and is counted and logged - see StaleFramebufferRecordLookups). A
|
||||
// caller that used to test `MGPipeHandleIsNull(st.DrawFramebuffer.Fbo)` tests the
|
||||
// pointer instead; the two are the same question.
|
||||
//
|
||||
// Defined in PipeApply.cpp rather than inline HERE so this header keeps its include
|
||||
// closure: the stale-generation path logs, and MG_Util/Debug/Log.h is not in this
|
||||
// header's closure and may not become part of it.
|
||||
const MGPFramebufferState* FramebufferRecordFor(MGPipeHandle fbo) const;
|
||||
const MGPFramebufferState* DrawFramebuffer() const;
|
||||
const MGPFramebufferState* ReadFramebuffer() const;
|
||||
};
|
||||
|
||||
// The monolith's single applier. Under split there is one per served context.
|
||||
@@ -288,6 +666,30 @@ namespace MobileGL::MG_Pipe {
|
||||
// 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
|
||||
// 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
|
||||
// BINDINGS, 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, the
|
||||
// framebuffer RECORDS, 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.
|
||||
//
|
||||
// ID-19(b) MOVED THE FRAMEBUFFER RECORD ACROSS THAT LINE and the reason is worth stating.
|
||||
// Before it, the whole framebuffer state was working state and a make-current took it. Now
|
||||
// the RECORD is an object record and only the two BOUND HANDLES are working state, so a
|
||||
// switch clears the bindings - after which DrawFramebuffer() / ReadFramebuffer() answer
|
||||
// null, exactly as the cleared records used to answer a null Fbo - and leaves the table
|
||||
// standing. Dropping the table instead would silently lose the record of every framebuffer
|
||||
// that is described by NAME and never bound, because the client's re-emission on a fresh
|
||||
// context is driven by MGPipeSetHashSuppressor::InvalidateAll, which re-sends the two bound
|
||||
// records and nothing else.
|
||||
//
|
||||
// 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();
|
||||
|
||||
// THE OTHER SCOPE: the served context is going away and its applier with it, so the object
|
||||
@@ -344,18 +746,122 @@ namespace MobileGL::MG_Pipe {
|
||||
// the backend and the gates compile against, and the records above are what they write
|
||||
// into; the bodies land in the two commits that follow this one on the same branch.
|
||||
|
||||
// The scope of one resource_respecify, and it is an APPLIER-SIDE ARGUMENT and not a wire
|
||||
// record: it is not in PipeFields.def, it crosses no payload, and the transport reads the
|
||||
// scope off the call it is replaying rather than off a field. The two members mirror
|
||||
// MGPipeResourceRecord::PendingUpload's key exactly, which is the only thing the applier
|
||||
// does with them - so UploadTarget is MGPSubData::Target VERBATIM, the whole packed field
|
||||
// (ID-12: low byte = MGPipeResourceTarget, high byte = the cube-face upload target), the
|
||||
// same value the emission of that level put in the record. A per-face respecify therefore
|
||||
// drops the face it redefines and leaves the other five standing, and a caller that packs
|
||||
// the pair differently here than it packs it there simply matches nothing.
|
||||
struct MGPRespecifiedLevel {
|
||||
Uint16 UploadTarget = 0;
|
||||
Uint16 Level = 0;
|
||||
};
|
||||
|
||||
// THE THREE ACCEPTANCE RETURNS, AND WHY ALL THREE (ID-18 M3, clientfb review M3). D-D5
|
||||
// step 1 says the client clears a level's dirty flags "for the levels whose record the
|
||||
// applier ACCEPTED", and the emitter cannot answer that for itself: an `if constexpr` that
|
||||
// discarded the call, a dead or stale handle (a counted no-op) and a corrupt record (a Fatal
|
||||
// that deliberately moves no counter) are all invisible from the call site, so a client that
|
||||
// clears on the strength of having EMITTED drops those texels for good. resource_subdata
|
||||
// returns it, and so must the two calls that DEFINE the storage a subsequent upload lands
|
||||
// in - a create or a respecify the applier refused leaves no record for the upload to
|
||||
// accumulate onto, and B's own bookkeeping (its per-entry descriptor dedupe, its drain list)
|
||||
// must not advance past a call that never landed.
|
||||
//
|
||||
// ALL THREE ARE SOURCE-COMPATIBLE: a Bool return is ignorable, P3a's call sites in
|
||||
// MG_Impl/Pipe/PipeFill.cpp discard it, and gen_pipe.py never parses this header - the wire
|
||||
// path calls no MGPipeApply* at all (wire review W1), so PipeCalls.def and
|
||||
// MobileGL/MG_Pipe/generated do not move.
|
||||
|
||||
// resource_create: mints the record and marks the slot Live. Emitted from the buffer
|
||||
// object's CONSTRUCTOR, so a resource exists before anything can name it; storage is
|
||||
// defined lazily by the first respecify and a backend tolerates a resource with none.
|
||||
void MGPipeApplyResourceCreate(const MGPResourceDesc& desc);
|
||||
//
|
||||
// Returns true when the record was minted. False for the three refusals: the reserved slot
|
||||
// 0, a descriptor whose target names no resource kind, and a slot at or above
|
||||
// kMGPipeMaxResourceSlots.
|
||||
Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc);
|
||||
// resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is
|
||||
// the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call,
|
||||
// MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks.
|
||||
void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes);
|
||||
//
|
||||
// P4a: `level` IS THE SCOPE OF THE REDEFINITION, and MGPResourceDesc cannot carry it - the
|
||||
// descriptor describes the resource, and a mutable texture redefines its levels ONE
|
||||
// glTexImage*D AT A TIME. Null means "this respecify redefines the WHOLE resource" - every
|
||||
// glBufferData / glBufferStorage, every glTexStorage*, every texture view - and drops every
|
||||
// pending upload, which is right because every level's coordinate system has just been
|
||||
// replaced. Non-null names the single (uploadTarget, level) the call redefines and drops
|
||||
// ONLY that key: the frontend's AllocateStorage / MarkStorageDirty are per
|
||||
// (uploadTarget, level) as well (MG_State/GLState/TextureState/TextureObject.h), so a
|
||||
// glTexImage2D(level 1) re-marks level 1 AND NOTHING ELSE, while the levels already
|
||||
// emitted had their client dirty flags cleared at THEIR emission (D-D5 step 1) and nothing
|
||||
// anywhere still owes them. Clearing the whole set here would lose exactly those texels,
|
||||
// silently, in every build - the loss the server-side set exists to prevent.
|
||||
//
|
||||
// Trailing and defaulted for W1's reason: P3a's buffer call site (PipeFill.cpp:691) and
|
||||
// every existing case compile unchanged. PACKAGE B PASSES THE PAIR IT JUST ALLOCATED at
|
||||
// every per-level respecify; it has both halves in hand at the AllocateStorage call site.
|
||||
//
|
||||
// A METADATA RESPECIFY IS A RESPECIFY THAT REDEFINES NO STORAGE (ID-18 M4). A sticky
|
||||
// BindMask / ImageBindableHint bit reaches the applier only on a respecify, and an
|
||||
// IMMUTABLE texture has no further one - that is what immutable means - so the canonical
|
||||
// order (glTexStorage2D, then glBindImageTexture or an FBO attachment) would leave the
|
||||
// record's hint at 0 for ever, and the hint is the PREVENTION half of the texture-remint
|
||||
// stall class. So B re-emits the descriptor when the mask moves, and a record whose
|
||||
// STORAGE-DEFINING fields all equal the stored descriptor's is applied as a metadata
|
||||
// update:
|
||||
//
|
||||
// - the descriptor is replaced, so BindMask and ImageBindableHint take their new values;
|
||||
// - NO pending upload is dropped, whatever `level` says. This REFINES the rule above
|
||||
// rather than contradicting it: that rule drops the uploads against the storage a
|
||||
// respecify REPLACES, and a call that replaces no storage replaces no coordinate system
|
||||
// either, so there is nothing to drop. A mask change arriving between a
|
||||
// glTexSubImage2D and the sync that consumes it must not eat the texels;
|
||||
// - the serial advances, which is the whole publication - the twin re-derives its storage
|
||||
// flags from the new mask at its next sync and recreates only where the backend needs
|
||||
// it (D's side);
|
||||
// - and MGPipeResourceRespecifyNeedsAck is false for it BY CONSTRUCTION, because a buffer
|
||||
// is never classified this way (see the body: glBufferData at an unchanged size is a
|
||||
// real orphaning reallocation, and glBufferStorage is the one entry point allowed a
|
||||
// synchronous ack).
|
||||
//
|
||||
// Returns true when the descriptor was stored - metadata updates included, since the record
|
||||
// did move - and false when the call was refused: a descriptor whose target names no
|
||||
// resource kind, or a handle this applier has no live record for at that generation.
|
||||
Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
|
||||
const MGPRespecifiedLevel* level = nullptr);
|
||||
// resource_subdata, buffer half: the destination range rides in the record's box through
|
||||
// MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split.
|
||||
// The applier stores nothing per record - contents are the backend's - and bumps Serial.
|
||||
void MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes);
|
||||
//
|
||||
// P4a: `regions` IS THE CALL'S VARIABLE TAIL - MGPSubRegion[record.RegionCount] - and it is
|
||||
// a trailing DEFAULTED parameter rather than a second entry point. The call has carried
|
||||
// kVarTail since P2 (PipeCalls.def) and the texture half cannot be applied without it: the
|
||||
// applier's pending-upload set is (UnionBox, RegionCount, Regions[]) and the verify lane's
|
||||
// retain mode compares all three. The buffer half declares no regions, so P3a's one call
|
||||
// site and every existing case are unchanged by the default.
|
||||
//
|
||||
// THE RETURN IS THE ACCEPTANCE SIGNAL D-D5 STEP 1 NAMES: true when the record was stored -
|
||||
// the buffer half landed its range, or the texture half accumulated the shape onto the
|
||||
// record - and false when it was refused. THE EMITTER MUST GATE ITS DIRTY-FLAG CLEAR ON IT
|
||||
// ("only for levels whose record the applier ACCEPTED"), because the two refusal paths are
|
||||
// otherwise invisible to it: a dead or stale handle is a counted no-op and a corrupt record
|
||||
// is a Fatal that does NOT move RefusedResourceCalls, so in a shipped push build a refused
|
||||
// upload and an accumulated one are indistinguishable from the call site. A client that
|
||||
// clears on the strength of having emitted drops those texels for good.
|
||||
//
|
||||
// THE RESOURCE-TARGET HALF OF record.Target PICKS THE HALF. MGPSubData::Target is PACKED
|
||||
// (ID-12): low byte = MGPipeResourceTarget, high byte = the cube-face upload target. The
|
||||
// buffer half is the whole field being 0 - the encoding the emitter is held to, since a
|
||||
// buffer has no upload target - and the texture half additionally requires the low byte to
|
||||
// name a TEXTURE target: Buffer, Renderbuffer and anything at or above
|
||||
// MGPipeResourceTarget::Count are Fatal{ProtocolCorruption} rather than an upload onto
|
||||
// whatever object holds that slot in the texture slot space.
|
||||
Bool MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes,
|
||||
const MGPSubRegion* regions = nullptr);
|
||||
// buffer_subdata_resident: same shape; `bytes` is the application's staging store and is
|
||||
// valid for the duration of the call only. The op-table entry may be null.
|
||||
void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes);
|
||||
@@ -403,6 +909,123 @@ namespace MobileGL::MG_Pipe {
|
||||
// configuration. Bumps IndexBufferSerial.
|
||||
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. 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".
|
||||
//
|
||||
// `state.Target` NOW SAYS TWO THINGS AT ONCE (ID-19(b)), and the record always does the
|
||||
// first of them:
|
||||
//
|
||||
// - THE RECORD IS ALWAYS WRITTEN, at FramebufferRecords[state.Fbo.Slot], whatever the
|
||||
// target is. The table is keyed by the framebuffer HANDLE, so one framebuffer's record
|
||||
// can never displace another's, and a slot whose object has been recycled is simply
|
||||
// overwritten by its successor's record (D-I2: no wire lifetime, so nothing to retire).
|
||||
// - Draw / Read / Both ADDITIONALLY set BoundFramebuffer[Draw] / [Read] / both.
|
||||
// MGPipeFramebufferTarget::Named sets NEITHER: it is how a DSA entry point hands Espryt
|
||||
// a framebuffer it is about to blit into or clear WITHOUT claiming it is bound.
|
||||
//
|
||||
// FramebufferSerial advances on every applied record, Named included.
|
||||
//
|
||||
// Two refusals, both Fatal{ProtocolCorruption} and neither counted (see RefusedObjectCalls:
|
||||
// this entry point resolves nothing and can only ever fault): a target above Named, a
|
||||
// draw-buffer entry outside the record's own Color[], a slot at or above
|
||||
// kMGPipeMaxFramebufferSlots, and the NULL HANDLE - a record that named {0,0} would install
|
||||
// itself where "nothing is bound" is read, and every emitter has a handle for every
|
||||
// framebuffer it describes (kMGPipeDefaultFramebuffer {0,1} for the default one).
|
||||
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)
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -78,20 +78,30 @@
|
||||
F(Cso) F(Texture) F(InternalFormat) F(Target) F(MinLevel) F(NumLevels) F(MinLayer) F(NumLayers) \
|
||||
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) \
|
||||
F(Res) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) F(MinLod) F(MaxLod) \
|
||||
F(LodBias)
|
||||
F(Res) F(BuiltinSampler) F(BaseLevel) F(MaxLevel) F(Swizzle) F(DepthStencilMode) F(ForceResync) \
|
||||
F(SamplerResync) F(MinLod) F(MaxLod) F(LodBias)
|
||||
|
||||
#define MGP_FIELDS_MGPProgramDesc(F) \
|
||||
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
|
||||
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
|
||||
|
||||
// P4a, ID-12 / esprytobj DV-5: Pad0 became Uint16 TextureTarget. Same trip wire as
|
||||
// MGPFramebufferState's Target below - PADDING_MEMBER_RE only excludes a member still NAMED
|
||||
// Pad<n>, so the rename without this row is a pipe-gates failure, and the row without the
|
||||
// rename is one too. A meaning-carrying byte cannot enter this record silently.
|
||||
#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) F(TextureTarget)
|
||||
|
||||
// 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) \
|
||||
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) \
|
||||
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
|
||||
@@ -254,6 +264,18 @@
|
||||
F(SwapBytes) F(LSBFirst) F(RowLength) F(ImageHeight) F(SkipPixels) F(SkipRows) F(SkipImages) \
|
||||
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) \
|
||||
F(Enabled) F(SrcFactorRGB) F(DstFactorRGB) F(SrcFactorAlpha) F(DstFactorAlpha) F(ColorEquation) \
|
||||
F(AlphaEquation)
|
||||
@@ -326,7 +348,8 @@
|
||||
P(MGPDrawRange) P(MGPDrawIndirect) P(MGPGridInfo) P(MGPMemoryBarrier) P(MGPStreamOutputBegin) \
|
||||
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
|
||||
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(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
|
||||
|
||||
|
||||
@@ -94,6 +94,15 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class BufferObject;
|
||||
// P4a's five, for the BIRTH half at the tail of this header. Declarations only, exactly as
|
||||
// BufferObject is: none of the hooks below needs a definition, and this header must not
|
||||
// gain one - reaching a frontend class header from here would put the state machine's own
|
||||
// types in front of every mutator that spells MGP_NOTE_MUTATION.
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
class FramebufferObject;
|
||||
class SamplerObject;
|
||||
class ProgramObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
@@ -140,6 +149,66 @@ namespace MobileGL::MG_Pipe {
|
||||
// is asked rather than assumed.
|
||||
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 MGPipeEmitResourceRespecify(MG_State::GLState::BufferObject& buffer);
|
||||
void MGPipeEmitResourceSubData(MG_State::GLState::BufferObject& buffer, SizeT offset, SizeT size);
|
||||
@@ -151,6 +220,142 @@ namespace MobileGL::MG_Pipe {
|
||||
// Returns the coherent host pointer the resource owner donated, or null for a DECLINE -
|
||||
// which is a real answer. Every call, mint or decline, is one map-persistent roundtrip.
|
||||
void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer);
|
||||
|
||||
// ================================================================================
|
||||
// P4a: THE BIRTH HALF, one hook per client path MG_State owns (D-C .. D-I)
|
||||
// ================================================================================
|
||||
//
|
||||
// The death helpers above are half a lifetime. The other half is emitted from MG_State
|
||||
// too - a texture's create from its constructor, a renderbuffer's respecify from its
|
||||
// storage mutators, a texture's params from glTexParameter*, a sampler CSO from the
|
||||
// sampler object, a shader CSO from the program - because that is where the event
|
||||
// happens, exactly as P3a's buffer family emits from BufferObject's own dispatchers
|
||||
// (ARCHITECTURE.md 5.1 names those as the ONE exception to push-at-validate). Only the
|
||||
// texture sub-data DRAIN runs at the validate point, and even it is fed from here: the
|
||||
// drain list is appended on a level's first dirty mark.
|
||||
//
|
||||
// WHY THEY ARE DECLARED HERE. This header is the one door MG_State has into the client
|
||||
// (check_include_closure.py's mutation-header probe pins it: reaching
|
||||
// MG_Impl/Pipe/*Emit.h from a frontend mutator would pull the client's emitters into the
|
||||
// state machine that calls them). So a hook a frontend mutator calls is DECLARED here and
|
||||
// DEFINED in MG_Impl/Pipe/PipeFill.cpp, which is package A's for the whole phase - the
|
||||
// same "declaration here, definition there" split MGPipeMintResourceHandle and
|
||||
// MGPipeEmitResourceCreate use, and the reason no file is touched twice.
|
||||
//
|
||||
// WHAT EACH BODY DOES, and the division is fixed:
|
||||
// * PipeFill.cpp owns the GATE - the subsystem bit in MOBILEGL_PIPE_PUSH *and* the
|
||||
// family's own kMGPipeWired*Subsystem constant, the same pair the validate point's
|
||||
// `wants()` applies to every emission - and the four MINTS, which are pure allocator
|
||||
// work and need no family knowledge;
|
||||
// * the FAMILY EMITTER (MG_Impl/Pipe/<Family>Emit.h, owned by package B or C) owns the
|
||||
// payload build, the handle rule for its own kind and the PUBLICATION LATCH below.
|
||||
// PipeFill.cpp forwards to it through an entry point that is compiled only while that
|
||||
// family's wired constant is non-zero, so this tree links against the STUB emitters
|
||||
// and against the finished ones with no edit to PipeFill.cpp - and a family that sets
|
||||
// its constant without providing the entry point is a COMPILE ERROR in its own commit
|
||||
// rather than a surprise at the merge. The entry point each hook forwards to is named
|
||||
// beside it and spelled out in PipeFill.cpp's contract block.
|
||||
//
|
||||
// NOTHING CALLS ANY OF THEM AT THE CONTRACT COMMIT. B and C add the call sites in the
|
||||
// five MG_State directories C.7 gives them, in the SAME commit that gives the emitter its
|
||||
// body - by EDITING an existing constructor/mutator body, never by adding one (G1).
|
||||
|
||||
// ---- the publication latch (D-I1), and it is the ONE answer both halves read ----
|
||||
//
|
||||
// The create is gated at its call site and the destroy inside the death helper, so the
|
||||
// two ask the same question at two different moments. An object born while its 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 the allocator is about to hand out again. A
|
||||
// slot is NOT evidence of a record either: a backend twin table mints one through
|
||||
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client to emit a
|
||||
// create, and a delete_* on such a handle is a refused call the applier asserts on.
|
||||
//
|
||||
// So the emitter latches the answer when its create actually goes out, the death helper
|
||||
// reads the latch, and the latch is keyed by {kind, slot, gen} so a recycled slot cannot
|
||||
// inherit its predecessor's answer. Defined in PipeFill.cpp beside the six death helpers,
|
||||
// declared here because both the helpers and the five emit headers read it.
|
||||
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle);
|
||||
|
||||
// ---- the four mints (pure allocator work, no family knowledge) ----
|
||||
//
|
||||
// UNCONDITIONAL in a push build, for MGPipeMintResourceHandle's reason: a handle is CLIENT
|
||||
// state and other subsystems name these objects by handle whether or not their own family
|
||||
// is switched on - MGPSurface::Res names a Texture or a Renderbuffer out of the framebuffer
|
||||
// subsystem, MGPBoundView::Texture and MGPImageView::Res name a Texture out of the sampler
|
||||
// one. Gating the mint on the family bit would make those emit null handles in exactly the
|
||||
// A/B arm that exists to isolate the families. Each costs one free-list pop and one map
|
||||
// insert per object and emits nothing.
|
||||
void MGPipeMintTextureHandle(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeMintRenderbufferHandle(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
// A framebuffer has a handle and NO wire lifetime (D-I2): set_framebuffer_state is the only
|
||||
// call that names one, and there is no create or destroy for the kind. The mint is still
|
||||
// the object's, so the identity exists before the first validate point that pushes it.
|
||||
void MGPipeMintFramebufferHandle(MG_State::GLState::FramebufferObject& framebuffer);
|
||||
// Ordinary programs only. A program-pipeline COMPOSITE is minted by the composite resolver
|
||||
// out of the reserved band through MGPipeSlotAllocator::AllocateComposite, which is the one
|
||||
// door into it, and it is not a frontend construction event.
|
||||
void MGPipeMintShaderCsoHandle(MG_State::GLState::ProgramObject& program);
|
||||
|
||||
// ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h, package B ----
|
||||
//
|
||||
// resource_create from ITextureObject's constructor and RenderbufferObject's;
|
||||
// resource_respecify from every storage-defining entry point, including
|
||||
// RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples}, which publish
|
||||
// nothing at all today (D-D2); set_texture_params from the parameter mutators, which is
|
||||
// where the READ-attachment-only gap D-E3 closes.
|
||||
//
|
||||
// Entry points MGPipeTextureEmitter must provide, all taking the frontend object by
|
||||
// reference and returning void:
|
||||
// EmitResourceCreate(ITextureObject&) / EmitResourceRespecify(ITextureObject&)
|
||||
// EmitTextureParams(ITextureObject&)
|
||||
// NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level)
|
||||
// EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&)
|
||||
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture);
|
||||
void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture);
|
||||
// The DRAIN LIST's append, on a level's FIRST dirty mark, keyed on the STORAGE OWNER from
|
||||
// day one (D-D4: a view and its owner already share one dirty state, so an upload through
|
||||
// either lands on the same key). The record itself is emitted at the validate point by
|
||||
// MGPipeTextureEmitter::DrainTextureSubData; this is only what puts the level on the list,
|
||||
// and walking every live texture per verb is the cost it exists to avoid.
|
||||
void MGPipeNoteTextureLevelDirty(MG_State::GLState::ITextureObject& storageOwner, Uint32 uploadTarget,
|
||||
Uint32 level);
|
||||
void MGPipeEmitRenderbufferResourceCreate(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
void MGPipeEmitRenderbufferResourceRespecify(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
|
||||
// ---- sampler CSOs and sampler views: MG_Impl/Pipe/SamplerEmit.h, package C ----
|
||||
//
|
||||
// Entry points MGPipeSamplerEmitter must provide, returning void:
|
||||
// EmitSamplerCso(SamplerObject&) - D-F1's content-addressed mint-or-share at
|
||||
// capacity 256, hashed field-wise over a canonical
|
||||
// zero-initialised copy, behind the version-first
|
||||
// skip. The HANDLE RULE FOR THIS KIND IS THE
|
||||
// EMITTER'S, not this file's: two identical
|
||||
// samplers share one CSO, so there is deliberately
|
||||
// no per-object mint above, and it is the emitter
|
||||
// that decides which lifetime id (if any) owns the
|
||||
// slot the death helper will resolve.
|
||||
// EmitSamplerView(ITextureObject&) - D-F2's ONE view per texture object, minted off
|
||||
// the texture's own lifetime id and re-issued on
|
||||
// the SAME handle when the restrictions move.
|
||||
void MGPipeEmitSamplerCsoCreate(MG_State::GLState::SamplerObject& sampler);
|
||||
void MGPipeEmitSamplerViewCreate(MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
// ---- programs: MG_Impl/Pipe/ProgramEmit.h, package C ----
|
||||
//
|
||||
// Entry point MGPipeProgramEmitter must provide, returning void:
|
||||
// EmitShaderCso(ProgramObject&)
|
||||
//
|
||||
// Re-issued on the SAME handle whenever the link version moves, exactly as
|
||||
// create_vertex_elements is (Gen moves only on slot reuse). D-H4 keeps the TRACKER out of
|
||||
// it - bit 6's shutter reads GetCurrentProgram() and deliberately not GetProgramForDraw(),
|
||||
// because the tracker must not force a compile to answer "did the shader move" - so the
|
||||
// ordinary emission is the validate point's, from the join the verb was going to make
|
||||
// anyway. This hook exists for the paths that are NOT a draw: a link that completes off
|
||||
// the draw path still owns its own publication.
|
||||
void MGPipeEmitShaderCsoCreate(MG_State::GLState::ProgramObject& program);
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_NOTE_MUTATION(Field) \
|
||||
::MobileGL::MG_Pipe::MGPipeNoteFrontendMutation(::MobileGL::MG_Pipe::MGPipeInputField::Field)
|
||||
|
||||
@@ -308,8 +308,13 @@ enum class MGPipeFieldEmitter : Uint8 {
|
||||
BindRenderState,
|
||||
BindVertexElements,
|
||||
CreateRenderState,
|
||||
SetDispatchProgram,
|
||||
SetDrawProgram,
|
||||
SetDynamicState,
|
||||
SetFramebufferState,
|
||||
SetPatchState,
|
||||
SetSamplerViews,
|
||||
SetShaderImages,
|
||||
SetVertexAttribDefaults,
|
||||
};
|
||||
|
||||
@@ -318,8 +323,13 @@ inline constexpr const char* kMGPipeFieldEmitterNames[] = {
|
||||
"BindRenderState",
|
||||
"BindVertexElements",
|
||||
"CreateRenderState",
|
||||
"SetDispatchProgram",
|
||||
"SetDrawProgram",
|
||||
"SetDynamicState",
|
||||
"SetFramebufferState",
|
||||
"SetPatchState",
|
||||
"SetSamplerViews",
|
||||
"SetShaderImages",
|
||||
"SetVertexAttribDefaults",
|
||||
};
|
||||
|
||||
@@ -344,11 +354,11 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetDepthFunc
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetDepthMask
|
||||
MGPipeFieldEmitter::SetDynamicState, // GetDepthRangeIndexed
|
||||
MGPipeFieldEmitter::kNone, // GetFramebufferBindingSlot
|
||||
MGPipeFieldEmitter::kNone, // GetImageTextureBinding
|
||||
MGPipeFieldEmitter::SetFramebufferState, // GetFramebufferBindingSlot
|
||||
MGPipeFieldEmitter::SetShaderImages, // GetImageTextureBinding
|
||||
MGPipeFieldEmitter::SetDynamicState, // GetLineWidth
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetLogicOp
|
||||
MGPipeFieldEmitter::kNone, // GetMaxTouchedTextureUnit
|
||||
MGPipeFieldEmitter::SetSamplerViews, // GetMaxTouchedTextureUnit
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetMinSampleShadingValue
|
||||
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultInnerLevel
|
||||
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultOuterLevel
|
||||
@@ -359,8 +369,8 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
||||
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetFactor
|
||||
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetUnits
|
||||
MGPipeFieldEmitter::SetDynamicState, // GetPrimitiveRestartIndex
|
||||
MGPipeFieldEmitter::kNone, // GetProgramForDispatch
|
||||
MGPipeFieldEmitter::kNone, // GetProgramForDraw
|
||||
MGPipeFieldEmitter::SetDispatchProgram, // GetProgramForDispatch
|
||||
MGPipeFieldEmitter::SetDrawProgram, // GetProgramForDraw
|
||||
MGPipeFieldEmitter::kNone, // GetProgramObject
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetProvokingVertexMode
|
||||
MGPipeFieldEmitter::CreateRenderState, // GetRenderStateParameters
|
||||
@@ -371,7 +381,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
||||
MGPipeFieldEmitter::kNone, // GetTextureBindGeneration
|
||||
MGPipeFieldEmitter::kNone, // GetTextureContextId
|
||||
MGPipeFieldEmitter::kNone, // GetTextureObject
|
||||
MGPipeFieldEmitter::kNone, // GetTextureUnitObject
|
||||
MGPipeFieldEmitter::SetSamplerViews, // GetTextureUnitObject
|
||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackCapturedVertices
|
||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackGeneration
|
||||
MGPipeFieldEmitter::kNone, // GetTransformFeedbackPausedPrimitiveCounter
|
||||
@@ -388,7 +398,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
|
||||
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
|
||||
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
|
||||
};
|
||||
inline constexpr SizeT kMGPipeEmittedFieldCount = 34;
|
||||
inline constexpr SizeT kMGPipeEmittedFieldCount = 40;
|
||||
|
||||
struct MGPipeFilledState {
|
||||
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 RenderStateParameters& a, const RenderStateParameters& 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 StencilFaceState& a, const StencilFaceState& 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 <>
|
||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<SamplerParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<PerBufferBlendState> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<StencilFaceState> : std::true_type {};
|
||||
@@ -629,6 +632,11 @@ inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParamete
|
||||
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) {
|
||||
MGP_FIELDS_PerBufferBlendState(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
@@ -661,4 +669,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
|
||||
|
||||
#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
|
||||
// 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.
|
||||
//
|
||||
// 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>
|
||||
requires std::same_as<std::remove_const_t<Self>, TypeFacts>
|
||||
void VisitFields(Self& a, V&& v) {
|
||||
@@ -549,7 +560,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// ---- trip wires ----
|
||||
// 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,
|
||||
"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
|
||||
// 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
|
||||
@@ -565,12 +576,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
#endif
|
||||
#ifdef MGL_LINKARTIFACTS_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,
|
||||
"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,
|
||||
"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,
|
||||
"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
|
||||
} // 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)
|
||||
endif()
|
||||
|
||||
# P3a's two suites. Their targets and this registration are the CONTRACT commit's, for the
|
||||
# same reason the four P2 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.
|
||||
# P3a's two suites AND P4a's six, together because they take exactly the same shape. Their
|
||||
# targets and this registration are the CONTRACT commit's, for the same reason the four P2
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
foreach(pipeTest ResourceEmitTest VertexInputEmitTest)
|
||||
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
||||
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
|
||||
ProgramEmitTest CompositeResolverTest)
|
||||
add_executable(${pipeTest} ${pipeTest}.cpp)
|
||||
|
||||
target_include_directories(${pipeTest} PRIVATE
|
||||
@@ -168,7 +177,9 @@ endforeach()
|
||||
|
||||
include(GoogleTest)
|
||||
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)
|
||||
endforeach()
|
||||
gtest_discover_tests(MagmaPipeIdentityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// create_shader_state takes the two artefact structs by pointer beside the record, so a case
|
||||
// that mints a composite record needs their definitions.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the composite band (the wire commits'). The client-side resolver - the
|
||||
// signature cache keyed on ComputeDrawProgramSignature, the two release paths, the eviction -
|
||||
// is the client package's and lands beside these.
|
||||
//
|
||||
// WHY THE APPLIER HAS A BAND AT ALL. It is not because the server knows what a composite is:
|
||||
// it does not, and create / bind / delete_shader_state name one exactly as they name any other
|
||||
// program. It is because the band starts at 983040, so ONE pipeline composite in a
|
||||
// slot-indexed vector would grow that vector to ~983k records of ~240 bytes each - a 236 MB
|
||||
// spike on the first pipeline draw. Both spaces stay dense against their own high-water mark.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
using MG_State::GLState::LinkArtifacts;
|
||||
using MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
MGPProgramDesc CompositeDesc(MGPipeHandle cso, Uint32 stageMask) {
|
||||
MGPProgramDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.StageMask = stageMask;
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly ProgramHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::ShaderCso), 0};
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// The band's record lands in the band's own table and the ordinary one is not grown by it -
|
||||
// which is the whole 236 MB of it - and every entry point still names it as an ordinary
|
||||
// program.
|
||||
TEST(CompositeResolver, ACompositeRecordLandsInTheBandsOwnTableAndNeverGrowsTheOrdinaryOne) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase + 2, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty())
|
||||
<< "one composite grew the ordinary table to the band's base - that is the 236 MB spike";
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(), 3u)
|
||||
<< "the band's table is indexed by (slot - base) and stays dense against its own high water";
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos[2].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Gen, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Desc.StageMask, 0x3u);
|
||||
|
||||
// AND THE SERVER NEVER LEARNS IT IS A COMPOSITE: the ordinary bind and draw-program calls
|
||||
// resolve it exactly as they resolve any other program.
|
||||
MGPipeApplyBindShaderState(ProgramHandle(composite));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(composite));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, composite);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, composite);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
// An ordinary program lands in the other table, and the two do not see each other even
|
||||
// though the composite's record is at index 2 of its own.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{2, 1}, 0x7u), &link, &spirv);
|
||||
ASSERT_GT(MGPipeApplier().ShaderCsos.size(), 2u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderCsos[2].Desc.StageMask, 0x7u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Desc.StageMask, 0x3u)
|
||||
<< "an ordinary program at slot 2 wrote the composite at band index 2";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The composite's slot has TWO independent release paths - the pipeline cache's eviction and
|
||||
// the composite program's own destructor - and both go through one client helper. The second
|
||||
// arrival here is a refused no-op, which is what makes the double free proven rather than
|
||||
// assumed, and it clears the bindings exactly once.
|
||||
TEST(CompositeResolver, ASecondDeleteOfACompositeIsARefusedNoOpRatherThanASecondRelease) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase, 3};
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(composite));
|
||||
ASSERT_EQ(MGPipeApplier().DrawProgram, composite);
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(composite));
|
||||
EXPECT_FALSE(MGPipeApplier().CompositeShaderCsos[0].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Gen, 3u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
const Uint64 serialAfterFirst = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(composite));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialAfterFirst)
|
||||
<< "the second release moved the binding serial, so it was not a no-op";
|
||||
|
||||
// And the band's slot is re-usable afterwards: a recycled composite is a new identity and
|
||||
// starts its record over.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{composite.Slot, 4}, 0x1u), &link, &spirv);
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos[0].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Gen, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Serial, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The band is INSIDE the ShaderCso slot limit, so the bound the applier refuses at is the limit
|
||||
// itself and not the band's base - a bound below it would refuse the very slots the allocator's
|
||||
// one composite door is allowed to hand out.
|
||||
TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsNot) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
// The positive control: the LAST slot of the band is a legal composite handle.
|
||||
const MGPipeHandle last{kMGPipeShaderCsoSlotLimit - 1, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(last.Slot));
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(last, 0x3u), &link, &spirv);
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(),
|
||||
static_cast<SizeT>(kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase));
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.back().Live);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
|
||||
const MGPProgramDesc past = CompositeDesc(MGPipeHandle{kMGPipeShaderCsoSlotLimit, 1}, 0x3u);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&past, &link, &spirv]() { MGPipeApplyCreateShaderState(past, &link, &spirv); });
|
||||
|
||||
// And an ORDINARY slot at or above the band's base is out of range by definition: the
|
||||
// allocator refuses the band for an ordinary program, so nothing legal can name one.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(MGPipeHandle{kMGPipeShaderCsoCompositeSlotBase - 1, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u)
|
||||
<< "an ordinary slot below the band resolved against a record nobody created";
|
||||
#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,583 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A record with real values in every field a case might read back, so a body that stored
|
||||
// the wrong one - or stored nothing - is visible BY FIELD.
|
||||
MGPFramebufferState FramebufferRecord(MGPipeHandle fbo, MGPipeFramebufferTarget target, Uint16 width) {
|
||||
MGPFramebufferState state{};
|
||||
state.Fbo = fbo;
|
||||
state.Target = static_cast<Uint8>(target);
|
||||
state.Width = width;
|
||||
state.Height = 64;
|
||||
state.Layers = 1;
|
||||
state.Samples = 1;
|
||||
state.Complete = 1;
|
||||
state.ContentHash = 0x1234u + width;
|
||||
for (Uint32 i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
state.DrawBuffers[i] = static_cast<Int8>(i == 0 ? 0 : -1);
|
||||
}
|
||||
state.Color[0].Res = MGPipeHandle{9, 1};
|
||||
state.Color[0].InternalFormat = 0x8058u; // GL_RGBA8
|
||||
state.Color[0].Kind = 1;
|
||||
return state;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_framebuffer_state (the wire commits'). The emitter's half - the
|
||||
// resolved read surface, the draw-buffer array in the content hash, a recycled handle never
|
||||
// suppressed against its predecessor, an attachment point above the wire width refused rather
|
||||
// than truncated - is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
// THE WHOLE POINT OF THE Target BYTE. GL has two independent framebuffer bindings and this
|
||||
// record carries one Fbo and one ReadSurface, so a record says which binding it describes;
|
||||
// Both is one object bound to both and writes both. Deleting either store, or the serial bump,
|
||||
// leaves this red.
|
||||
TEST(FramebufferEmit, ADrawRecordAndAReadRecordAreKeptApartAndBothWritesBoth) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const Uint64 serialAtStart = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->DrawBuffers[0], 0);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr)
|
||||
<< "a Draw record landed in the read binding as well";
|
||||
const Uint64 afterDraw = MGPipeApplier().FramebufferSerial;
|
||||
EXPECT_GT(afterDraw, serialAtStart) << "an applied record must move the serial the twin memoises";
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 200u);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr) << "a Read record overwrote the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterDraw);
|
||||
|
||||
// Both: one record, one serial bump, two destinations.
|
||||
const Uint64 beforeBoth = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Both, 300));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, beforeBoth + 1)
|
||||
<< "a Both record is ONE record and moves the serial once";
|
||||
// The two earlier framebuffers keep their own records - the table is keyed by the handle,
|
||||
// so binding a third displaced neither (ID-19(b)).
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2})->Width, 200u);
|
||||
|
||||
// A framebuffer has a handle but NO wire lifetime, so there is no record to refuse against
|
||||
// and this entry point never counts an object refusal.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A target outside the FOUR is not a target this server has, and guessing one would put a
|
||||
// draw's attachments into the read binding or the other way round. Named (3) is legal since
|
||||
// ID-19(b) and has its own case below; the first refused value is the one above it.
|
||||
TEST(FramebufferEmit, ATargetOutsideTheThreeBindingsIsRefusedNamingTheRecord) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPFramebufferState bad = FramebufferRecord(MGPipeHandle{7, 4}, MGPipeFramebufferTarget::Draw, 100);
|
||||
bad.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Count);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=7, gen=4, target=4}: the record names no framebuffer "
|
||||
"binding target",
|
||||
[&bad]() { MGPipeApplySetFramebufferState(bad); });
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{7, 4}), nullptr)
|
||||
<< "a refused record was written into the per-object table anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "a refused record must not move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The draw-buffer array is an INDEX into this record's own Color[], and -1 is NONE. An entry
|
||||
// outside that range would have the server read a colour attachment the record does not carry,
|
||||
// which is the truncation the wire width's cap refusal exists to prevent upstream.
|
||||
TEST(FramebufferEmit, ADrawBufferEntryOutsideTheRecordsOwnArrayIsRefusedRatherThanRead) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
// The positive control first: -1 everywhere and the last legal index are both fine, so
|
||||
// what follows is refusing the value and not the loop around it.
|
||||
MGPFramebufferState legal = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 100);
|
||||
legal.DrawBuffers[7] = static_cast<Int8>(kMGPipeMaxColorAttachments - 1);
|
||||
MGPipeApplySetFramebufferState(legal);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{8, 1}));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState past = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 111);
|
||||
past.DrawBuffers[3] = static_cast<Int8>(kMGPipeMaxColorAttachments);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&past]() { MGPipeApplySetFramebufferState(past); });
|
||||
|
||||
MGPFramebufferState negative = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 222);
|
||||
negative.DrawBuffers[0] = -2;
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&negative]() { MGPipeApplySetFramebufferState(negative); });
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4, as ID-19(b) leaves it. The two framebuffer BINDINGS are per-context working state and a
|
||||
// make-current takes them - so both accessors answer null afterwards, exactly as the zeroed
|
||||
// records used to answer a null Fbo - while the per-object RECORD survives, because a
|
||||
// framebuffer that is only ever addressed BY NAME has no re-emission trigger at all. The serial
|
||||
// ADVANCES rather than restarting, because a counter that walks back through values it has
|
||||
// already stamped into a twin that outlived the switch is not a generation at all. Restoring
|
||||
// `= 0` anywhere in the reset, or clearing the table there, leaves this red.
|
||||
TEST(FramebufferEmit, AMakeCurrentClearsBothRecordsAndAdvancesTheSerialRatherThanZeroingIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], kMGPipeNullHandle);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "the per-object record is not working state and a make-current may not take it";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "the serial was carried over or restarted; the cleared window is itself a change the "
|
||||
"twin has to hear about, and no stamped value may ever recur";
|
||||
|
||||
// And the teardown scope advances it again, for the same reason.
|
||||
const Uint64 afterReset = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterReset);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TEARDOWN SCOPE DROPS THE OBJECT RECORDS, SO IT MUST DROP EVERY WORKING HANDLE THAT NAMES
|
||||
// ONE. The two framebuffer records hold eleven MGPSurface::Res naming texture and renderbuffer
|
||||
// records, and the three unit windows hold entries naming sampler-view, sampler-CSO and texture
|
||||
// records; a window left standing after the tables are emptied is a set of handles into empty
|
||||
// tables, which the next resolve either refuses and counts or - on a slot the next context
|
||||
// re-mints - resolves onto somebody else's record. Deleting any one of the eleven clears in
|
||||
// MGPipeApplierReleaseObjectRecords leaves this red.
|
||||
TEST(FramebufferEmit, AReleaseOfTheObjectRecordsAlsoClearsTheWorkingHandlesThatCouldNameThem) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
|
||||
// The three kVarTail sets, each with one entry naming a record the release is about to
|
||||
// drop, and each at a non-zero Start so the window itself is visible in the assertions.
|
||||
MGPBoundView view{};
|
||||
view.View = MGPipeHandle{3, 1};
|
||||
view.Texture = MGPipeHandle{9, 1};
|
||||
view.Unit = 2;
|
||||
MGPipeApplySetSamplerViews(MGPSamplerViews{2, 1, 0xAAAAu}, &view);
|
||||
|
||||
const MGPipeHandle samplerState{5, 1};
|
||||
MGPipeApplyBindSamplerStates(MGPSamplerStates{2, 1, 0xBBBBu}, &samplerState);
|
||||
|
||||
MGPImageView image{};
|
||||
image.Res = MGPipeHandle{9, 1};
|
||||
image.Unit = 2;
|
||||
image.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
MGPipeApplySetShaderImages(MGPShaderImages{2, 1, 0xCCCCu}, &image);
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].Res, (MGPipeHandle{9, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerViews[2].View, (MGPipeHandle{3, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerStateCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerStates[2], samplerState);
|
||||
ASSERT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundShaderImages[2].Res, (MGPipeHandle{9, 1}));
|
||||
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "a framebuffer record holding eleven MGPSurface::Res into the emptied texture and "
|
||||
"renderbuffer tables survived the teardown";
|
||||
EXPECT_TRUE(MGPipeApplier().FramebufferRecords.empty());
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[2].View, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerStates[2], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Res, kMGPipeNullHandle);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ID-19's CORRECTION, AND THE CASE THAT SAYS WHAT THE FOURTH TARGET IS FOR. Every DSA entry
|
||||
// point - BlitNamedFramebuffer and the four ClearNamedFramebuffer* - hands Espryt a framebuffer
|
||||
// BY NAME, and that framebuffer is very often bound to neither binding. With only the two bound
|
||||
// records the server had no description of it at all, bound its driver FBO with no attachments
|
||||
// and cleared or blitted into nothing (esprytobj C-1). A Named record fixes that WITHOUT lying
|
||||
// about the bindings: the record is written and addressable by handle, and BoundFramebuffer
|
||||
// does not move. Making the Named arm touch either binding leaves this red.
|
||||
TEST(FramebufferEmit, ANamedRecordDescribesTheFramebufferItNamesWithoutMovingEitherBinding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// TWO DIFFERENT FRAMEBUFFERS ON THE TWO BINDINGS FIRST, so "the bindings did not move" is an
|
||||
// assertion about values rather than about null.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState named = FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 300);
|
||||
named.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Named);
|
||||
named.Color[0].Res = MGPipeHandle{21, 1};
|
||||
MGPipeApplySetFramebufferState(named);
|
||||
|
||||
// (a) THE DSA LOOKUP FINDS IT, BY HANDLE, WITH ITS ATTACHMENTS. This is the call package D
|
||||
// makes at every named blit and clear.
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3}), nullptr)
|
||||
<< "a framebuffer handed to the server by name has no record, which is the state that "
|
||||
"clears into a driver framebuffer with no attachments";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Color[0].Res, (MGPipeHandle{21, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Target, static_cast<Uint8>(MGPipeFramebufferTarget::Named));
|
||||
|
||||
// (b) AND NEITHER BINDING MOVED.
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}))
|
||||
<< "a Named record claimed the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}))
|
||||
<< "a Named record claimed the read binding";
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], (MGPipeHandle{5, 2}));
|
||||
|
||||
// (c) The serial moves for a Named record too: a twin memoising a framebuffer's attachments
|
||||
// has to hear that they moved, and whether it is bound is a different question.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore + 1);
|
||||
|
||||
// (d) And the same framebuffer can then be BOUND, which moves the binding and restates the
|
||||
// record - the two targets are not two tables.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 400));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 400u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// STALE-GENERATION REFUSAL, ON THE ONE TABLE WHOSE OBJECT HAS NO WIRE LIFETIME. A framebuffer is
|
||||
// never destroyed on the wire, so its slot is simply overwritten by its successor - and until
|
||||
// that successor describes itself, a handle naming the DEAD one must be refused rather than
|
||||
// answered with the predecessor's attachments. That answer would be a blit or a clear into
|
||||
// somebody else's colour buffer. It is LOUD (counted, and logged once) because the only way to
|
||||
// reach it is an emitter defect, and it is counted APART from RefusedObjectCalls because this is
|
||||
// a read by the server's own sync path and not a call the applier refused.
|
||||
TEST(FramebufferEmit, AFramebufferHandleWhoseGenerationHasMovedOnIsRefusedRatherThanAnswered) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
const Uint64 staleBefore = MGPipeApplier().StaleFramebufferRecordLookups;
|
||||
|
||||
// The slot has been recycled and the successor has not described itself yet.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr)
|
||||
<< "a handle at a recycled slot was answered with its predecessor's record";
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 1);
|
||||
|
||||
// Now it does, and the predecessor's handle becomes the stale one - in the other direction.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 2}, MGPipeFramebufferTarget::Draw, 200));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2})->Width, 200u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2);
|
||||
|
||||
// THE TWO SILENT NULLS, and they are silent on purpose. "Nothing is bound to this binding"
|
||||
// is what a make-current leaves behind and arrives on every draw of a context that has not
|
||||
// described its framebuffers; "no record at this slot" is what every framebuffer looks like
|
||||
// before its first set_framebuffer_state. Counting either would bury the one that matters.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeNullHandle), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{99, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2)
|
||||
<< "an unbound binding or an undescribed slot was counted as a stale generation";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u)
|
||||
<< "the framebuffer family may never move the object-refusal counter";
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TWO REFUSALS THE PER-OBJECT TABLE ADDED. The null handle is what "nothing is bound" reads
|
||||
// as, so a record installed at {0,0} would be answered to every caller asking about an EMPTY
|
||||
// binding; and Slot is a client-supplied Uint32 that now reaches an allocator, so it takes the
|
||||
// same bound the five object tables take. Every emitter has a handle for every framebuffer it
|
||||
// describes - kMGPipeDefaultFramebuffer {0,1} for the default one - so neither value is
|
||||
// producible by a correct client, which is why both are Fatal rather than counted refusals.
|
||||
TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherThanStored) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// The positive control first: the DEFAULT framebuffer is slot 0 at generation 1 and is
|
||||
// perfectly legal, so what follows refuses the null handle and not slot 0.
|
||||
MGPipeApplySetFramebufferState(
|
||||
FramebufferRecord(kMGPipeDefaultFramebuffer, MGPipeFramebufferTarget::Both, 128));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer)->Width, 128u);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState nullHandle = FramebufferRecord(kMGPipeNullHandle, MGPipeFramebufferTarget::Draw, 300);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=0, gen=0, target=0}: the record names the null "
|
||||
"framebuffer handle",
|
||||
[&nullHandle]() { MGPipeApplySetFramebufferState(nullHandle); });
|
||||
|
||||
MGPFramebufferState pastTheBound = FramebufferRecord(
|
||||
MGPipeHandle{kMGPipeMaxFramebufferSlots, 1}, MGPipeFramebufferTarget::Draw, 400);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=65536, gen=1, target=0}: the framebuffer slot is "
|
||||
"outside the record table's bound",
|
||||
[&pastTheBound]() { MGPipeApplySetFramebufferState(pastTheBound); });
|
||||
static_assert(kMGPipeMaxFramebufferSlots == 65536u,
|
||||
"the refusal line above names the bound; move both together");
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore) << "a refused record moved the serial";
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, kMGPipeDefaultFramebuffer)
|
||||
<< "a refused record took the draw binding";
|
||||
EXPECT_LT(MGPipeApplier().FramebufferRecords.size(),
|
||||
static_cast<SizeT>(kMGPipeMaxFramebufferSlots))
|
||||
<< "an out-of-range slot resized the table instead of being refused";
|
||||
#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,342 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_shader_images (the wire commits'). The emitter's half - the
|
||||
// high-water-zero early-out, the content hash covering Access and InternalFormat, the shutter
|
||||
// keyed on the FRONTEND sampling-resolution generation - is the client package's.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
// Every field carries a value of its own, and two of them are the point: InternalFormat and
|
||||
// Access are live glBindImageTexture state that the format-less image bake keys on, so a
|
||||
// body that dropped either would leave the server baking against a format the shader was
|
||||
// not built for.
|
||||
MGPImageView ImageAt(Uint32 unit, Uint32 internalFormat, Uint8 access) {
|
||||
MGPImageView view{};
|
||||
view.Res = MGPipeHandle{unit + 1, 1};
|
||||
view.Unit = unit;
|
||||
view.InternalFormat = internalFormat;
|
||||
view.Layer = 3;
|
||||
view.Level = 2;
|
||||
view.Layered = 1;
|
||||
view.Access = access;
|
||||
return view;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// The window rule, one field at a time: the entries land where the header says and nowhere
|
||||
// else, and every field of an entry survives. Deleting the copy loop, the two window
|
||||
// assignments or the serial bump leaves this red.
|
||||
TEST(ImageEmit, TheImageSetLandsInItsWindowWithEveryFieldTheShaderWasBuiltAgainst) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// Access is a Uint8 on the wire - the client's own read/write/read-write encoding, not a
|
||||
// GL enum - and InternalFormat is the application's, which the server recasts.
|
||||
const MGPImageView entries[2] = {ImageAt(2, 0x8814u /* GL_RGBA32F */, 2 /* write only */),
|
||||
ImageAt(3, 0x8230u /* GL_RG32F */, 3 /* read write */)};
|
||||
MGPShaderImages header{};
|
||||
header.Start = 2;
|
||||
header.Count = 2;
|
||||
header.ContentHash = 0x5150u;
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
// The other two sets' serials, taken AFTER the fixture: a reset and a teardown each advance
|
||||
// every working serial, so "unchanged" is measured from here rather than from zero.
|
||||
const Uint64 samplerViewsSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
|
||||
MGPipeApplySetShaderImages(header, entries);
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Res, (MGPipeHandle{3, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].InternalFormat, 0x8814u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Access, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].Access, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Level, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Layer, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Layered, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].InternalFormat, 0x8230u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[1].Res)) << "the set wrote below its window";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[4].Res)) << "the set wrote above its window";
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
|
||||
// "The last set as received": a narrower set says nothing about what it does not name.
|
||||
MGPShaderImages narrow{};
|
||||
narrow.Start = 2;
|
||||
narrow.Count = 1;
|
||||
MGPipeApplySetShaderImages(narrow, entries);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].InternalFormat, 0x8230u)
|
||||
<< "the entry outside the new window was cleared";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, samplerViewsSerial)
|
||||
<< "the image set moved another set's serial; the three are independent";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The window gate, at the bound and one past it, and the null-tail arm. The image-unit space
|
||||
// is the same merged 192 the sampler units are.
|
||||
TEST(ImageEmit, AnImageWindowPastTheImageUnitSpaceIsRefusedRatherThanTruncated) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(0, 0x8058u, 2);
|
||||
|
||||
MGPShaderImages exact{};
|
||||
exact.Start = kMGPipeMaxImageUnits - 1;
|
||||
exact.Count = 1;
|
||||
MGPipeApplySetShaderImages(exact, &entry);
|
||||
ASSERT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPShaderImages past{};
|
||||
past.Start = kMGPipeMaxImageUnits;
|
||||
past.Count = 1;
|
||||
past.ContentHash = 9;
|
||||
ExpectRefusedNaming("set_shader_images {start=192, count=1, hash=9}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&past, &entry]() { MGPipeApplySetShaderImages(past, &entry); });
|
||||
|
||||
MGPShaderImages noTail{};
|
||||
noTail.Start = 0;
|
||||
noTail.Count = 1;
|
||||
ExpectRefusedNaming("set_shader_images {start=0, count=1, hash=0}: a non-empty set carries no entries",
|
||||
[&noTail]() { MGPipeApplySetShaderImages(noTail, nullptr); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImagesSerial, serialBefore) << "a refused set moved the serial";
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, kMGPipeMaxImageUnits - 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
// An EMPTY set is not a refusal: it is what a program with no image uniforms publishes, and it
|
||||
// still moves the serial, because "no images" is a state the twin has to hear about.
|
||||
TEST(ImageEmit, AnEmptySetIsAppliedRatherThanRefusedAndStillMovesTheSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(0, 0x8058u, 1);
|
||||
MGPShaderImages filled{};
|
||||
filled.Count = 1;
|
||||
MGPipeApplySetShaderImages(filled, &entry);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPShaderImages empty{};
|
||||
MGPipeApplySetShaderImages(empty, nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[0].InternalFormat, 0x8058u)
|
||||
<< "an empty window cleared entries it never named";
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4: the image set is per-context WORKING state, so a make-current takes it and ADVANCES
|
||||
// its serial rather than restarting it.
|
||||
TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(1, 0x8058u, 1);
|
||||
MGPShaderImages header{};
|
||||
header.Start = 1;
|
||||
header.Count = 1;
|
||||
MGPipeApplySetShaderImages(header, &entry);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 0u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[1].Res));
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
#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,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Includes.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_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Pipe;
|
||||
@@ -157,6 +166,239 @@ TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
|
||||
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
|
||||
// describes one framebuffer OBJECT and Target says which binding(s), if any, it also
|
||||
// sets, and that costs a byte the struct already had. Named (ID-19) cost nothing at all -
|
||||
// it is a fourth value of a byte that was already there, which is why the applier could
|
||||
// be given a per-object table without a wire change.
|
||||
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);
|
||||
// Named = 3, and it is pinned by VALUE rather than merely by existence: the applier
|
||||
// validates a record with `Target >= Count`, so an enumerator inserted ahead of Named
|
||||
// would silently re-point every Named record the client already emits at Draw or Read -
|
||||
// and a Draw record for a framebuffer that is not bound is the exact corruption Named
|
||||
// exists to prevent (a DSA clear/blit landing on an unattached driver framebuffer).
|
||||
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Named), 3u);
|
||||
// Count is the applier's refusal bound and it is 4 now, not 3: a wire that still refused
|
||||
// 3 would drop every DSA record on the floor.
|
||||
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Count), 4u);
|
||||
// The byte must be able to hold every value, since Target is a Uint8 in the record and
|
||||
// the enum is the only thing that says what fits.
|
||||
EXPECT_LE(static_cast<Uint32>(MGPipeFramebufferTarget::Count), 256u);
|
||||
EXPECT_EQ(sizeof(MGPFramebufferState::Target), 1u);
|
||||
// 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));
|
||||
}
|
||||
|
||||
// P4a, D-D3 / ID-12: MGPSubData::Target is TWO facts in one Uint16 - the low byte says which
|
||||
// KIND of storage the destination is, the high byte which cube face / upload target the level
|
||||
// belongs to - and the packing is the contract's, not each emitter's.
|
||||
//
|
||||
// The property this case exists for is the COLLISION the packing prevents.
|
||||
// TextureUploadTarget::Texture1D is 0 and the applier's buffer branch tests the WHOLE field
|
||||
// == 0, so a texture record carrying the bare upload enumerator would be indistinguishable
|
||||
// from a buffer record exactly when its owner is a 1D texture, and that texture's upload
|
||||
// would be dispatched into the buffer path. Nothing else in the tree would have said so.
|
||||
TEST(PipeCatalogue, SubDataTargetPacksAResourceTargetAndAnUploadTarget) {
|
||||
// Both halves must fit their byte, or the encoding is not an encoding.
|
||||
static_assert(static_cast<Uint32>(MGPipeResourceTarget::Count) <= 0x100u);
|
||||
static_assert(static_cast<Uint32>(TextureUploadTarget::TextureUploadTargetCount) <= 0x100u);
|
||||
|
||||
// 0 first, and deliberately: it is the enumerator that makes the collision possible. Then
|
||||
// the plain 2D upload, the first and last cube face, and the largest enumerator the enum
|
||||
// has, which is what proves the byte is wide enough in practice and not just in principle.
|
||||
const Uint32 uploadTargets[] = {
|
||||
0u,
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture2D),
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX),
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapNegativeZ),
|
||||
static_cast<Uint32>(TextureUploadTarget::TextureUploadTargetCount) - 1u,
|
||||
};
|
||||
for (Uint32 resource = 0; resource < static_cast<Uint32>(MGPipeResourceTarget::Count);
|
||||
++resource) {
|
||||
for (const Uint32 upload : uploadTargets) {
|
||||
const Uint16 packed = MGPipePackSubDataTarget(resource, upload);
|
||||
EXPECT_EQ(MGPipeSubDataResourceTargetOf(packed), static_cast<Uint8>(resource))
|
||||
<< "resource target " << resource << " upload target " << upload;
|
||||
EXPECT_EQ(MGPipeSubDataUploadTargetOf(packed), static_cast<Uint8>(upload))
|
||||
<< "resource target " << resource << " upload target " << upload;
|
||||
}
|
||||
}
|
||||
|
||||
// THE BUFFER INVARIANT, at compile time in MGPipeTypes.h and again here so a failure names
|
||||
// itself: a buffer record's Target is exactly kMGPipeResourceTargetBuffer, whole field,
|
||||
// upload byte and all, so P3a's records are unchanged on the wire.
|
||||
static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u) ==
|
||||
kMGPipeResourceTargetBuffer);
|
||||
EXPECT_EQ(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u), kMGPipeResourceTargetBuffer);
|
||||
EXPECT_EQ(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer,
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture1D)),
|
||||
kMGPipeResourceTargetBuffer);
|
||||
MGPSubData zeroed{};
|
||||
EXPECT_EQ(zeroed.Target, kMGPipeResourceTargetBuffer);
|
||||
|
||||
// ...and the other side of it: a 1D texture's upload target IS 0, and packed it still
|
||||
// cannot be mistaken for a buffer, because no texture's resource target is 0.
|
||||
EXPECT_EQ(static_cast<Uint32>(TextureUploadTarget::Texture1D), 0u);
|
||||
for (Uint32 resource = 1; resource < static_cast<Uint32>(MGPipeResourceTarget::Count);
|
||||
++resource) {
|
||||
EXPECT_NE(MGPipePackSubDataTarget(resource, 0u), kMGPipeResourceTargetBuffer)
|
||||
<< "resource target " << resource << " collides with a buffer record";
|
||||
}
|
||||
EXPECT_NE(MGPipePackSubDataTarget(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture1D),
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture1D)),
|
||||
kMGPipeResourceTargetBuffer);
|
||||
|
||||
// What a real cube-face record reads back as, through the field rather than a local.
|
||||
MGPSubData record{};
|
||||
record.Target =
|
||||
MGPipePackSubDataTarget(MGPipeResourceTargetForTextureTarget(TextureTarget::TextureCubeMap),
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapNegativeY));
|
||||
EXPECT_EQ(MGPipeSubDataResourceTargetOf(record.Target),
|
||||
static_cast<Uint8>(MGPipeResourceTarget::TexCube));
|
||||
EXPECT_EQ(MGPipeSubDataUploadTargetOf(record.Target),
|
||||
static_cast<Uint8>(TextureUploadTarget::CubeMapNegativeY));
|
||||
// Six faces share one resource target: the high byte is the only thing that tells them
|
||||
// apart, which is why it cannot be dropped.
|
||||
EXPECT_EQ(MGPipeSubDataResourceTargetOf(
|
||||
MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::TexCube),
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX))),
|
||||
MGPipeSubDataResourceTargetOf(record.Target));
|
||||
EXPECT_NE(MGPipeSubDataUploadTargetOf(
|
||||
MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::TexCube),
|
||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX))),
|
||||
MGPipeSubDataUploadTargetOf(record.Target));
|
||||
}
|
||||
|
||||
// P4a, ID-12: the three constants MGPSurface::Kind is spelled with, the texture target the
|
||||
// record grew where its Pad0 was, and MGPTextureParams::DepthStencilMode's two numbers.
|
||||
//
|
||||
// All three were UNSTATED in the contract and were being re-invented on both sides of the
|
||||
// boundary - which is the way a wire field acquires two meanings. The values themselves are
|
||||
// unremarkable; what this case pins is that there is exactly one spelling of each.
|
||||
TEST(PipeCatalogue, SurfaceNamesItsKindItsTextureTargetAndItsDepthStencilAspect) {
|
||||
// MGPipeKind is REUSED rather than a second three-value enum minted beside the field.
|
||||
EXPECT_EQ(kMGPipeSurfaceKindNone, static_cast<Uint8>(MGPipeKind::None));
|
||||
EXPECT_EQ(kMGPipeSurfaceKindTexture, static_cast<Uint8>(MGPipeKind::Texture));
|
||||
EXPECT_EQ(kMGPipeSurfaceKindRenderbuffer, static_cast<Uint8>(MGPipeKind::Renderbuffer));
|
||||
EXPECT_NE(kMGPipeSurfaceKindTexture, kMGPipeSurfaceKindRenderbuffer);
|
||||
// None == 0 is load-bearing: it is what makes a zero-initialised record already BE the
|
||||
// empty attachment point, which every emitter and every reader relies on.
|
||||
EXPECT_EQ(kMGPipeSurfaceKindNone, 0u);
|
||||
|
||||
// Pad0 -> Uint16 TextureTarget. THE SIZE DID NOT MOVE - the two bytes were already there -
|
||||
// and neither did anything in front of it.
|
||||
EXPECT_EQ(sizeof(MGPSurface), 24u);
|
||||
EXPECT_EQ(offsetof(MGPSurface, UploadTarget), 20u);
|
||||
EXPECT_EQ(offsetof(MGPSurface, TextureTarget), 22u);
|
||||
// The sentinel is TextureTarget::Unknown widened, so it is a value no real target has.
|
||||
EXPECT_EQ(kMGPipeSurfaceNoTextureTarget, 0xFFFFu);
|
||||
EXPECT_EQ(kMGPipeSurfaceNoTextureTarget, static_cast<Uint16>(TextureTarget::Unknown));
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
||||
EXPECT_NE(static_cast<Uint16>(i), kMGPipeSurfaceNoTextureTarget);
|
||||
}
|
||||
|
||||
// A ZEROED MGPSurface CARRIES TextureTarget 0, AND 0 IS TextureTarget::Texture1D, NOT THE
|
||||
// SENTINEL. That is documented rather than defended, and it is why the field's contract is
|
||||
// "consulted only when Kind == kMGPipeSurfaceKindTexture": a zeroed record is Kind == None
|
||||
// and names no texture at all, so a reader that gates on Kind can never see the 0. A
|
||||
// reader that does not gate would read Texture1D out of an empty attachment point.
|
||||
MGPSurface empty{};
|
||||
EXPECT_EQ(empty.TextureTarget, 0u);
|
||||
EXPECT_EQ(static_cast<Uint16>(TextureTarget::Texture1D), 0u);
|
||||
EXPECT_EQ(empty.Kind, kMGPipeSurfaceKindNone);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(empty.Res));
|
||||
|
||||
// A renderbuffer point names no texture and says so with the sentinel, which is what
|
||||
// distinguishes "not a texture" from "a 1D texture" for a reader that looks anyway.
|
||||
MGPSurface renderbuffer{};
|
||||
renderbuffer.Kind = kMGPipeSurfaceKindRenderbuffer;
|
||||
renderbuffer.TextureTarget = kMGPipeSurfaceNoTextureTarget;
|
||||
EXPECT_NE(renderbuffer.TextureTarget, static_cast<Uint16>(TextureTarget::Texture1D));
|
||||
|
||||
// The half a compiler cannot catch: the PipeFields.def row. MGPSurface still asserts its
|
||||
// size whether or not the field list names TextureTarget, so a comparator blind to the
|
||||
// field would pass a target-only divergence under MOBILEGL_PIPE_VERIFY - and the field is
|
||||
// exactly what the four cross-object masks key on.
|
||||
MGPSurface a{};
|
||||
MGPSurface b{};
|
||||
const char* field = nullptr;
|
||||
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
||||
a.TextureTarget = static_cast<Uint16>(TextureTarget::TextureCubeMap);
|
||||
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
||||
EXPECT_STREQ(field, "TextureTarget");
|
||||
|
||||
// DepthStencilMode: 0 = GL_DEPTH_COMPONENT, 1 = GL_STENCIL_INDEX. Depth is 0 because it is
|
||||
// the GL initial value and a texture that never asks for the stencil aspect never emits
|
||||
// the call, so a zeroed record has to decode to what an untouched texture already has.
|
||||
EXPECT_EQ(kMGPipeDepthStencilModeDepth, 0u);
|
||||
EXPECT_EQ(kMGPipeDepthStencilModeStencil, 1u);
|
||||
EXPECT_NE(kMGPipeDepthStencilModeDepth, kMGPipeDepthStencilModeStencil);
|
||||
MGPTextureParams params{};
|
||||
EXPECT_EQ(params.DepthStencilMode, kMGPipeDepthStencilModeDepth);
|
||||
}
|
||||
|
||||
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
|
||||
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
|
||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
|
||||
@@ -352,9 +594,19 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
||||
// 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
|
||||
// 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) {
|
||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 71u);
|
||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 72u);
|
||||
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||
@@ -363,6 +615,7 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<MGPVertexAttribWire>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<MGPVertexBindingPointWire>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<SamplerParameters>::value);
|
||||
PixelStoreParameters p{};
|
||||
PixelStoreParameters q{};
|
||||
const char* field = nullptr;
|
||||
@@ -377,6 +630,38 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||
t.Offset = 8;
|
||||
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
||||
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.
|
||||
@@ -580,6 +865,158 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
|
||||
mutableStore.Width = 64u * 1024u;
|
||||
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.
|
||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ResourceRespecify), 3);
|
||||
|
||||
// P4a, ID-18 M4. The metadata-update rule is a PROSE contract stated beside the predicate
|
||||
// above - it compares an incoming descriptor against the applier's stored one, which this
|
||||
// header cannot do - so what is pinnable here is the thing that would make the prose lie:
|
||||
// a field added to MGPResourceDesc and classified into neither list. The size is the
|
||||
// tripwire, and the two metadata fields are named so the classification cannot be lost to
|
||||
// a rename either.
|
||||
EXPECT_EQ(sizeof(MGPResourceDesc), 88u);
|
||||
EXPECT_EQ(sizeof(MGPResourceDesc::BindMask), 2u);
|
||||
EXPECT_EQ(sizeof(MGPResourceDesc::ImageBindableHint), 1u);
|
||||
// HasDefinedContent sits next to ImageBindableHint and is deliberately on the OTHER side
|
||||
// of the line: glBufferData(size, NULL) at an unchanged size is an orphaning
|
||||
// reallocation, so a record that moves only it must still clear, and must never be read
|
||||
// as a mask change.
|
||||
EXPECT_NE(offsetof(MGPResourceDesc, HasDefinedContent),
|
||||
offsetof(MGPResourceDesc, ImageBindableHint));
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// THE ShaderCso COMPOSITE BAND IS A SECOND SPACE, AND THE ALLOCATOR REPORTS IT SEPARATELY.
|
||||
//
|
||||
// The band's base is 983040, so a composite handle passes every bound an ordinary one does and
|
||||
// a slot-indexed table that forgets the band allocates ~983k entries for one program pipeline.
|
||||
// That is why the allocator keeps two dense tables - and it is also why the two must be
|
||||
// COUNTED apart: a high-water mark that folded them would be pinned at ~983k from the first
|
||||
// composite mint onward, and every "the high-water mark did not move over N churn rounds"
|
||||
// assertion about ORDINARY ShaderCso slots - the shape that catches a dense table that never
|
||||
// shrinks, i.e. the ~1.3 KB-per-record leak the P3a final review found - would be vacuously
|
||||
// true for the rest of the process. One merged number is one real assertion and one that
|
||||
// cannot go red; two numbers are two real assertions, which is what the per-kind leak cases
|
||||
// need.
|
||||
//
|
||||
// This case pins both halves: a leaked COMPOSITE moves the band's marks and not the ordinary
|
||||
// one, and an ordinary leak still moves the ordinary mark with a composite outstanding.
|
||||
TEST(PipeCatalogue, TheCompositeShaderBandIsCountedApartFromTheOrdinarySpace) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MGPipeSlotAllocator slots;
|
||||
|
||||
const Uint32 ordinaryBefore = slots.HighWater(MGPipeKind::ShaderCso);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase)
|
||||
<< "the band's high-water mark starts at its base, so it is monotone from the first mint";
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
||||
EXPECT_EQ(slots.CompositeFreeCount(), 0u);
|
||||
|
||||
// A COMPOSITE MOVES THE BAND'S MARKS AND ONLY THOSE.
|
||||
const MGPipeHandle composite = slots.AllocateComposite(9001);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(composite));
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore)
|
||||
<< "a composite mint moved the ORDINARY high-water mark, so the ordinary space's leak "
|
||||
"assertion is vacuous from here on";
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 1u);
|
||||
// A live composite IS a live ShaderCso: the merged count is deliberate and stays.
|
||||
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
||||
|
||||
// AND THE ORDINARY MARK STILL MOVES WITH A COMPOSITE OUTSTANDING - the half that stopped
|
||||
// existing when one number carried both spaces.
|
||||
const MGPipeHandle ordinary = slots.Allocate(MGPipeKind::ShaderCso);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(ordinary));
|
||||
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(ordinary.Slot));
|
||||
EXPECT_GT(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u)
|
||||
<< "an ordinary mint moved the BAND's high-water mark";
|
||||
|
||||
// The slot goes back to the BAND's free list, and the high-water marks do not come back
|
||||
// down - which is exactly what makes them a leak witness rather than a live count.
|
||||
const Uint32 ordinaryHighWater = slots.HighWater(MGPipeKind::ShaderCso);
|
||||
slots.Free(MGPipeKind::ShaderCso, composite);
|
||||
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
||||
EXPECT_EQ(slots.CompositeFreeCount(), 1u);
|
||||
EXPECT_EQ(slots.FreeCount(MGPipeKind::ShaderCso), 1u);
|
||||
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
||||
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryHighWater);
|
||||
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client slot allocator in a pull build";
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// The applier takes the two artefact structs BY POINTER beside the record, so a case that
|
||||
// drives create_shader_state needs their definitions - the applier's own header deliberately
|
||||
// only forward-declares them.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the program family (the wire commits'): the shader CSO record, the
|
||||
// three bindings and the default uniform block. The emitter's half - the join at the validate
|
||||
// point, the never-uploaded sentinel that must never be emitted, the composite resolver - is
|
||||
// the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
using MG_State::GLState::LinkArtifacts;
|
||||
using MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
MGPProgramDesc ProgramDesc(MGPipeHandle cso, Uint32 stageMask, Uint32 globalUboSize) {
|
||||
MGPProgramDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.StageMask = stageMask;
|
||||
desc.GlobalUboSize = globalUboSize;
|
||||
desc.ReservedNumSamplesOffset = 32;
|
||||
desc.SpirvStatus = 1;
|
||||
desc.NativeFloat64 = 1;
|
||||
desc.PointSizeDemoted = 1;
|
||||
desc.EnableSpirvValidation = 1;
|
||||
// ALL SEVEN BLOB REFS ARE DECLARED WITH Size 0 - "this record does not declare its
|
||||
// blob" - which is exactly what a monolith emission is: the artefacts ride beside the
|
||||
// record through the two companion pointers and the codec is never called.
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly ProgramHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::ShaderCso), 0};
|
||||
}
|
||||
|
||||
MGPGlobalConstants GlobalConstants(MGPipeHandle cso, Uint32 version) {
|
||||
MGPGlobalConstants record{};
|
||||
record.ShaderCso = cso;
|
||||
record.Version = version;
|
||||
return record;
|
||||
}
|
||||
|
||||
const MGPipeShaderCsoRecord& ProgramRecordOf(Uint32 slot) {
|
||||
EXPECT_GT(MGPipeApplier().ShaderCsos.size(), static_cast<SizeT>(slot));
|
||||
return MGPipeApplier().ShaderCsos[slot];
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// A create starts the record over and leaves Serial at 0; a RE-ISSUE on the same handle is how
|
||||
// a relink travels, and it takes the default uniform block with it - a block sized to a layout
|
||||
// that no longer exists is worse than no block, and the sentinel is the value that says
|
||||
// "nothing has been uploaded for this program".
|
||||
TEST(ProgramEmit, ACreateStoresTheDescriptorAndARelinkCountsUpAndDropsTheBlockKeyedToTheOldLayout) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{5, 2};
|
||||
const Uint8 block[64] = {};
|
||||
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 64), &link, &spirv);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).Live);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 2u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a create is not a mutation";
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x3u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.GlobalUboSize, 64u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.ReservedNumSamplesOffset, 32u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.NativeFloat64, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).GlobalConstantsVersion, ~Uint32{0})
|
||||
<< "a fresh record starts at the never-uploaded sentinel";
|
||||
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 7), block);
|
||||
ASSERT_EQ(ProgramRecordOf(5).GlobalConstants.size(), 64u);
|
||||
const Uint64 blockSerial = ProgramRecordOf(5).GlobalConstantsSerial;
|
||||
|
||||
// The relink.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x7u, 32), &link, &spirv);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x7u);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).GlobalConstants.empty())
|
||||
<< "a block sized to the layout the relink replaced survived it";
|
||||
EXPECT_EQ(ProgramRecordOf(5).GlobalConstantsVersion, ~Uint32{0});
|
||||
EXPECT_GT(ProgramRecordOf(5).GlobalConstantsSerial, blockSerial)
|
||||
<< "the clearing was not announced, so a twin can still match what it uploaded before";
|
||||
|
||||
// A RECYCLED SLOT STARTS OVER: inheriting one field of the previous occupant is how a
|
||||
// program at a recycled slot inherits its predecessor's reflection.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(MGPipeHandle{5, 3}, 0x1u, 16), &link, &spirv);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 3u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a recycled slot kept its predecessor's serial";
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The three refusals a create can produce: no artefacts at all behind seven undeclared blobs, a
|
||||
// default uniform block no program can have, and a slot outside the record table's bound.
|
||||
TEST(ProgramEmit, ACreateWithNoArtefactsAnOversizedBlockOrACorruptSlotIsRefusedNamingTheProgram) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
const MGPProgramDesc desc = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &spirv]() { MGPipeApplyCreateShaderState(desc, nullptr, &spirv); });
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &link]() { MGPipeApplyCreateShaderState(desc, &link, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
|
||||
const MGPProgramDesc huge = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, kMGPipeMaxGlobalConstantsBytes + 1);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the default uniform block is larger than any "
|
||||
"program may declare",
|
||||
[&huge, &link, &spirv]() { MGPipeApplyCreateShaderState(huge, &link, &spirv); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty()) << "the table was grown by a refused record";
|
||||
|
||||
const MGPProgramDesc pastTheBound = ProgramDesc(MGPipeHandle{kMGPipeMaxShaderCsoSlots, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&pastTheBound, &link, &spirv]() {
|
||||
MGPipeApplyCreateShaderState(pastTheBound, &link, &spirv);
|
||||
});
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Three bindings, one serial, and each of them follows its OWN handle: set_draw_program and
|
||||
// set_dispatch_program are two calls because the frontend has two joins. A null handle is legal
|
||||
// and means "nothing bound"; a dead one leaves the previous binding standing and is counted.
|
||||
TEST(ProgramEmit, TheThreeBindingsFollowTheirOwnHandleAndADeadOneLeavesThePreviousBindingStanding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle draw{2, 1};
|
||||
const MGPipeHandle dispatch{3, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(draw, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(dispatch, 0x20u, 0), &link, &spirv);
|
||||
|
||||
const Uint64 serialBefore = MGPipeApplier().ProgramBindingSerial;
|
||||
MGPipeApplyBindShaderState(ProgramHandle(draw));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(draw));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(dispatch));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DispatchProgram, dispatch);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 3);
|
||||
|
||||
// A dead handle: previous binding untouched, and COUNTED - a no-op nobody can see is a
|
||||
// dropped bind nobody can see.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(MGPipeHandle{2, 9}));
|
||||
MGPipeApplyBindShaderState(ProgramHandle(MGPipeHandle{99, 1}));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(MGPipeHandle{3, 9}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 3);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, draw);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DispatchProgram, dispatch);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 3) << "a refused bind moved the serial";
|
||||
|
||||
// The null handle is a state, not an error.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(kMGPipeNullHandle));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 4);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 3) << "a null bind was counted as a refusal";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A delete drops the record whole, keeps the generation, and clears EVERY binding that named
|
||||
// it - unlike the unit sets, which are "the last set as received". A binding left pointing at a
|
||||
// dropped record would make the next verb refuse a state the applier itself created.
|
||||
TEST(ProgramEmit, ADeleteDropsTheRecordAndClearsEveryBindingThatNamedIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{6, 4};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(cso));
|
||||
const Uint64 serialBefore = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(cso));
|
||||
EXPECT_FALSE(ProgramRecordOf(6).Live);
|
||||
EXPECT_EQ(ProgramRecordOf(6).Gen, 4u) << "a destroy keeps the generation";
|
||||
EXPECT_EQ(ProgramRecordOf(6).Desc.StageMask, 0u)
|
||||
<< "a stale read of a deleted slot must find nothing, not the program that used to be there";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DispatchProgram));
|
||||
EXPECT_GT(MGPipeApplier().ProgramBindingSerial, serialBefore);
|
||||
|
||||
// THE SECOND NOTICE IS A REFUSED NO-OP. A composite's slot has two independent release
|
||||
// paths and both arrive here; the second finding nothing is what makes the double free
|
||||
// proven rather than assumed.
|
||||
const Uint64 serialAfter = MGPipeApplier().ProgramBindingSerial;
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialAfter);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The default uniform block lands on the PROGRAM's record - it is (ShaderCso, Version) keyed
|
||||
// and belongs to the program, not to the context that uploaded it - and the length it is held
|
||||
// to is the program's own GlobalUboSize, which the create already bounded.
|
||||
TEST(ProgramEmit, TheDefaultUniformBlockLandsOnTheProgramsRecordAndTheSentinelIsRefused) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{7, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 8), &link, &spirv);
|
||||
|
||||
Uint8 block[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 11), block);
|
||||
ASSERT_EQ(ProgramRecordOf(7).GlobalConstants.size(), 8u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstants[7], 8u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 11u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsSerial, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).Serial, 0u) << "a block upload is not a relink";
|
||||
|
||||
// A DECLARED blob length that agrees is fine; one that does not is refused, and so is the
|
||||
// sentinel the backends read as "never uploaded".
|
||||
MGPGlobalConstants declared = GlobalConstants(cso, 12);
|
||||
declared.Blob.Size = 8;
|
||||
MGPipeApplySetGlobalConstants(declared, block);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 12u);
|
||||
|
||||
MGPGlobalConstants lying = GlobalConstants(cso, 13);
|
||||
lying.Blob.Size = 9;
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: the declared blob length is not the "
|
||||
"program's own default uniform block size",
|
||||
[&lying, &block]() { MGPipeApplySetGlobalConstants(lying, block); });
|
||||
|
||||
const MGPGlobalConstants sentinel = GlobalConstants(cso, ~Uint32{0});
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: the version is the backends' "
|
||||
"never-uploaded sentinel",
|
||||
[&sentinel, &block]() { MGPipeApplySetGlobalConstants(sentinel, block); });
|
||||
|
||||
const MGPGlobalConstants noBytes = GlobalConstants(cso, 14);
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: a non-empty block carries no bytes",
|
||||
[&noBytes]() { MGPipeApplySetGlobalConstants(noBytes, nullptr); });
|
||||
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 12u) << "a refused block was stored anyway";
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsSerial, 2u);
|
||||
|
||||
// And a block for a program this applier does not have is the ordinary counted refusal.
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(MGPipeHandle{7, 2}, 15), block);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4 for this family: the program record is share-group state and survives a make-current -
|
||||
// re-emitting create_shader_state for a record the applier still holds would move its serial
|
||||
// for nothing - while the three bindings are working state and do not.
|
||||
TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{8, 1};
|
||||
const Uint8 block[4] = {9, 9, 9, 9};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 4), &link, &spirv);
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 21), block);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
const Uint64 bindingSerial = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
ASSERT_TRUE(ProgramRecordOf(8).Live) << "a make-current dropped a share-group program record";
|
||||
EXPECT_EQ(ProgramRecordOf(8).GlobalConstantsVersion, 21u);
|
||||
EXPECT_EQ(ProgramRecordOf(8).GlobalConstants.size(), 4u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_GT(MGPipeApplier().ProgramBindingSerial, bindingSerial)
|
||||
<< "the binding serial was carried over or restarted rather than advanced";
|
||||
|
||||
// The bind that follows the switch still resolves, which is the whole point of the rule.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, cso);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
#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;
|
||||
}
|
||||
@@ -1323,6 +1323,169 @@ namespace {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P4a: the four resource entry points now BRANCH ON THE DESCRIPTOR'S TARGET.
|
||||
//
|
||||
// The slot spaces of kinds Buffer, Texture and Renderbuffer are independent - the client
|
||||
// allocator is per kind - so one slot-indexed table would alias three live objects onto one
|
||||
// record. These cases are about the branch and nothing else: which table a call lands in,
|
||||
// that the three do not see each other, and that a target or a kind the catalogue does not
|
||||
// name is refused rather than routed to whichever table came first. The texture family's
|
||||
// own behaviour (parameters, the sub-data validator, the pending-upload set) is in
|
||||
// TextureEmitTest beside the emitter cases it belongs with.
|
||||
// =====================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MGPResourceDesc TargetedDesc(MGPipeHandle res, MGPipeResourceTarget target, Uint32 width, Uint32 glName) {
|
||||
MGPResourceDesc desc = BufferDesc(res, width, glName);
|
||||
desc.Target = static_cast<Uint8>(target);
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly KindHandle(MGPipeHandle res, MGPipeKind kind) {
|
||||
return MGPHandleOnly{res, static_cast<Uint32>(kind), 0};
|
||||
}
|
||||
#endif
|
||||
|
||||
// ONE SLOT NUMBER, THREE LIVE OBJECTS, THREE RECORDS. This is the case that fails the
|
||||
// instant the applier goes back to one table: every assertion below is about slot 7 being
|
||||
// three different things at once, which is exactly what the client allocator hands out.
|
||||
TEST(ResourceEmit, TheThreeResourceKindsKeepTheirOwnSlotSpaceAndDoNotSeeEachOther) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle shared{7, 3};
|
||||
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Buffer, 0, 11));
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Tex2D, 0, 22));
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Renderbuffer, 0, 33));
|
||||
|
||||
ASSERT_GT(MGPipeApplier().Resources.size(), 7u);
|
||||
ASSERT_GT(MGPipeApplier().TextureResources.size(), 7u);
|
||||
ASSERT_GT(MGPipeApplier().RenderbufferResources.size(), 7u);
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Desc.GlNameForDiag, 11u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.GlNameForDiag, 22u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Desc.GlNameForDiag, 33u);
|
||||
|
||||
// A respecify of one of them moves ONE record's serial and one record's extent.
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(shared, MGPipeResourceTarget::Tex2D, 256, 22), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.Width, 256u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Serial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Desc.Width, 0u) << "a texture respecify moved the buffer";
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Serial, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Serial, 0u);
|
||||
|
||||
// A renderbuffer restorage is the publication D-D2 asks for: the frontend raises no
|
||||
// version for it, so the emission IS the notice, and the applier holds the new extent.
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(shared, MGPipeResourceTarget::Renderbuffer, 1024, 33),
|
||||
nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Desc.Width, 1024u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Serial, 1u);
|
||||
|
||||
// And a destroy takes the record its KIND names, and only that one.
|
||||
MGPipeApplyResourceDestroy(KindHandle(shared, MGPipeKind::Texture));
|
||||
EXPECT_FALSE(MGPipeApplier().TextureResources[7].Live);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Gen, 3u) << "a destroy keeps the generation";
|
||||
EXPECT_TRUE(MGPipeApplier().Resources[7].Live) << "a texture destroy dropped the buffer's record";
|
||||
EXPECT_TRUE(MGPipeApplier().RenderbufferResources[7].Live);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Neither branch may fall through to a table it was not named. A target or a kind outside
|
||||
// the catalogue would otherwise land in whichever table the code happened to reach first,
|
||||
// and destroy a live object of a kind the call was never about.
|
||||
TEST(ResourceEmit, AResourceTargetOrKindTheCatalogueDoesNotNameIsRefusedRatherThanRouted) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle res{5, 1};
|
||||
|
||||
MGPResourceDesc unnamed = BufferDesc(res, 0, 44);
|
||||
unnamed.Target = static_cast<Uint8>(MGPipeResourceTarget::Count);
|
||||
ExpectRefusedNaming("resource_create {slot=5, gen=1, glName=44}: the descriptor names no resource "
|
||||
"target",
|
||||
[&unnamed]() { MGPipeApplyResourceCreate(unnamed); });
|
||||
EXPECT_TRUE(MGPipeApplier().Resources.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().TextureResources.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().RenderbufferResources.empty());
|
||||
|
||||
ExpectRefusedNaming("resource_respecify {slot=5, gen=1, glName=44}: the descriptor names no "
|
||||
"resource target",
|
||||
[&unnamed]() { MGPipeApplyResourceRespecify(unnamed, nullptr); });
|
||||
|
||||
const MGPHandleOnly wrongKind = KindHandle(res, MGPipeKind::SamplerCso);
|
||||
ExpectRefusedNaming("resource_destroy {slot=5, gen=1}: the handle names no resource kind",
|
||||
[&wrongKind]() { MGPipeApplyResourceDestroy(wrongKind); });
|
||||
|
||||
// AND unmap_persistent GIVES THE SAME VERDICT, because it is the only one of the four
|
||||
// buffer-only calls that carries a discriminator at all. An assertion here is not a
|
||||
// check: MOBILEGL_ASSERT compiles out at INFO, which is what all three gate builds and
|
||||
// every shipped build are, so a texture-kinded record used to walk into ResolveResource
|
||||
// and alias whatever BUFFER holds that slot - which is exactly what the destroy's Fatal
|
||||
// above exists to stop. The live buffer at slot 5 is what makes the aliasing reachable.
|
||||
MGPipeApplyResourceCreate(BufferDesc(res, 0, 44));
|
||||
MGPipeApplyResourceRespecify(BufferDesc(res, 256, 44), nullptr);
|
||||
ASSERT_TRUE(MGPipeApplier().Resources[5].Live);
|
||||
const MGPHandleOnly textureKind = KindHandle(res, MGPipeKind::Texture);
|
||||
ExpectRefusedNaming("unmap_persistent {slot=5, gen=1}: the persistent donation is the buffer "
|
||||
"family's and the handle names another kind",
|
||||
[&textureKind]() { MGPipeApplyUnmapPersistent(textureKind); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u)
|
||||
<< "a corrupt record is not a dropped call and must not be counted as one";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A texture's resource calls reach NO backend function pointer, and that is the structural
|
||||
// decision the phase rests on rather than an omission: nothing in the texture family
|
||||
// dispatches at GL-call time today, so the record IS the publication. A spy table that saw
|
||||
// one of them would mean P4a had grown an op-table path nobody designed.
|
||||
TEST(ResourceEmit, NoTextureOrRenderbufferResourceCallReachesTheBackendOpTable) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
g_spy = SpyState{};
|
||||
MGPipeSetResourceOps(&kSpyOps);
|
||||
const MGPipeHandle texture{3, 1};
|
||||
const MGPipeHandle renderbuffer{4, 1};
|
||||
const Uint8 texels[64] = {};
|
||||
|
||||
MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 55));
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 8, 55), nullptr);
|
||||
MGPipeApplyResourceCreate(TargetedDesc(renderbuffer, MGPipeResourceTarget::Renderbuffer, 0, 66));
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(renderbuffer, MGPipeResourceTarget::Renderbuffer, 8, 66),
|
||||
nullptr);
|
||||
MGPSubData upload{};
|
||||
upload.Res = texture;
|
||||
upload.Target = static_cast<Uint16>(MGPipeResourceTarget::Tex2D);
|
||||
upload.UnionBox = MGPBox{0, 0, 0, 4, 4, 1};
|
||||
MGPipeApplyResourceSubData(upload, texels);
|
||||
MGPipeApplyResourceDestroy(KindHandle(texture, MGPipeKind::Texture));
|
||||
MGPipeApplyResourceDestroy(KindHandle(renderbuffer, MGPipeKind::Renderbuffer));
|
||||
|
||||
EXPECT_EQ(g_spy.Creates, 0u);
|
||||
EXPECT_EQ(g_spy.Respecifies, 0u);
|
||||
EXPECT_EQ(g_spy.SubDatas, 0u);
|
||||
EXPECT_EQ(g_spy.Destroys, 0u);
|
||||
|
||||
// The same five calls on a BUFFER still dispatch, which is what proves the count above
|
||||
// is the branch working rather than the table being uninstalled.
|
||||
const MGPipeHandle buffer{3, 1};
|
||||
MGPipeApplyResourceCreate(BufferDesc(buffer, 0, 77));
|
||||
MGPipeApplyResourceRespecify(BufferDesc(buffer, 64, 77), nullptr);
|
||||
MGPipeApplyResourceSubData(BufferWrite(buffer, 0, 16), texels);
|
||||
MGPipeApplyResourceDestroy(BufferHandle(buffer));
|
||||
EXPECT_EQ(g_spy.Creates, 1u);
|
||||
EXPECT_EQ(g_spy.Respecifies, 1u);
|
||||
EXPECT_EQ(g_spy.SubDatas, 1u);
|
||||
EXPECT_EQ(g_spy.Destroys, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 REQUIRES THE PULL AND PUSH ctest NAME SETS TO BE IDENTICAL, name for name, so a
|
||||
// push-only case cannot be ABSENT from a pull build - it has to be there and SKIP. This
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the sampler family (the wire commits'): the CSO record, the
|
||||
// identity-addressed view record and its back-pointer, and the two unit sets. The emitter's
|
||||
// half - the content-addressed 256-entry cache, the canonical zero-initialised copy the hash
|
||||
// and the memcmp run over, the padding that cannot change the hash, borderColorForm crossing -
|
||||
// is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
// Every field carries a value of its own, INCLUDING borderColorForm and all three border
|
||||
// representations: they are always numerically populated, so the value alone cannot say
|
||||
// which driver entry point to use and the form is what does.
|
||||
SamplerParameters SamplerValues(Float lodBias, BorderColorForm form) {
|
||||
SamplerParameters params{};
|
||||
params.wrapS = SamplerWrapMode::ClampToEdge;
|
||||
params.wrapT = SamplerWrapMode::MirroredRepeat;
|
||||
params.minFilter = SamplerFilterMode::Linear;
|
||||
params.magFilter = SamplerFilterMode::Nearest;
|
||||
params.mipmapMode = SamplerMipmapMode::Nearest;
|
||||
params.lodBias = lodBias;
|
||||
params.maxAnisotropy = 4.0f;
|
||||
params.compareMode = SamplerCompareMode::CompareToTexture;
|
||||
params.borderColor = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||
params.borderColorI = {-1, 2, -3, 4};
|
||||
params.borderColorUI = {5u, 6u, 7u, 8u};
|
||||
params.borderColorForm = form;
|
||||
return params;
|
||||
}
|
||||
|
||||
MGPSamplerDesc SamplerDesc(MGPipeHandle cso, Uint64 declaredBlobSize) {
|
||||
MGPSamplerDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.Parameters.Size = declaredBlobSize;
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPSamplerView ViewOf(MGPipeHandle cso, MGPipeHandle texture, Uint16 minLevel) {
|
||||
MGPSamplerView view{};
|
||||
view.Cso = cso;
|
||||
view.Texture = texture;
|
||||
view.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
view.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
view.MinLevel = minLevel;
|
||||
view.NumLevels = 4;
|
||||
view.MinLayer = 0;
|
||||
view.NumLayers = 1;
|
||||
view.Samples = 1;
|
||||
return view;
|
||||
}
|
||||
|
||||
MGPHandleOnly SamplerHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::SamplerCso), 0};
|
||||
}
|
||||
MGPHandleOnly ViewHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::SamplerViewCso), 0};
|
||||
}
|
||||
|
||||
MGPResourceDesc TextureDesc(MGPipeHandle res, Uint32 glName) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
desc.GlNameForDiag = glName;
|
||||
return desc;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// A create starts the record over and leaves Serial at 0 - so a fresh backend twin that starts
|
||||
// its own synced serial at 0 agrees without either side publishing anything - while a re-issue
|
||||
// on a LIVE identity counts up, which is how a value change travels on a handle whose
|
||||
// generation moves only on slot reuse.
|
||||
TEST(SamplerEmit, ACreateStoresTheParametersByValueAndAReissueOnALiveIdentityCountsUp) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle cso{6, 2};
|
||||
const SamplerParameters first = SamplerValues(0.5f, BorderColorForm::Int);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, 0), &first);
|
||||
|
||||
ASSERT_GT(MGPipeApplier().SamplerCsos.size(), 6u);
|
||||
const MGPipeSamplerCsoRecord& record = MGPipeApplier().SamplerCsos[6];
|
||||
EXPECT_TRUE(record.Live);
|
||||
EXPECT_EQ(record.Gen, 2u);
|
||||
EXPECT_EQ(record.Serial, 0u) << "a create is not a mutation";
|
||||
EXPECT_EQ(record.Params.wrapT, SamplerWrapMode::MirroredRepeat);
|
||||
EXPECT_EQ(record.Params.magFilter, SamplerFilterMode::Nearest);
|
||||
EXPECT_EQ(record.Params.compareMode, SamplerCompareMode::CompareToTexture);
|
||||
EXPECT_FLOAT_EQ(record.Params.lodBias, 0.5f);
|
||||
EXPECT_FLOAT_EQ(record.Params.borderColor.z(), 0.75f);
|
||||
EXPECT_EQ(record.Params.borderColorI.x(), -1);
|
||||
EXPECT_EQ(record.Params.borderColorUI.w(), 8u);
|
||||
EXPECT_EQ(record.Params.borderColorForm, BorderColorForm::Int)
|
||||
<< "borderColorForm crosses; without it the backend cannot choose an entry point";
|
||||
|
||||
const SamplerParameters second = SamplerValues(1.5f, BorderColorForm::Uint);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, sizeof(SamplerParameters)), &second);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Serial, 1u);
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[6].Params.lodBias, 1.5f);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Params.borderColorForm, BorderColorForm::Uint);
|
||||
|
||||
// A RECYCLED SLOT STARTS OVER. Inheriting one field of the previous occupant - a serial, a
|
||||
// filter - is precisely how a sampler at a recycled slot inherits its predecessor's state.
|
||||
const SamplerParameters third = SamplerValues(2.5f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(MGPipeHandle{6, 3}, 0), &third);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Gen, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Serial, 0u) << "a recycled slot kept its predecessor's serial";
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[6].Params.lodBias, 2.5f);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The one Blob rule, on this family's own blob: a non-zero declared length must be exactly one
|
||||
// SamplerParameters, a zero means "this record does not declare its blob" - which is what a
|
||||
// monolith emission is - and either way the bytes read are bounded by the TYPE.
|
||||
TEST(SamplerEmit, ARecordThatDoesNotDescribeItsOwnParametersIsRefusedNamingTheLength) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const SamplerParameters values = SamplerValues(0.0f, BorderColorForm::Float);
|
||||
|
||||
const MGPSamplerDesc lying = SamplerDesc(MGPipeHandle{4, 1}, sizeof(SamplerParameters) + 1);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=4, gen=1}: the declared blob length is not one "
|
||||
"SamplerParameters",
|
||||
[&lying, &values]() { MGPipeApplyCreateSamplerState(lying, &values); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty());
|
||||
|
||||
const MGPSamplerDesc undeclared = SamplerDesc(MGPipeHandle{4, 1}, 0);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=4, gen=1}: the record declares no parameters and "
|
||||
"carries none",
|
||||
[&undeclared]() { MGPipeApplyCreateSamplerState(undeclared, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty());
|
||||
|
||||
// And the slot bound, which is the one number in the family that reaches an allocator.
|
||||
const MGPSamplerDesc pastTheBound = SamplerDesc(MGPipeHandle{kMGPipeMaxSamplerCsoSlots, 1}, 0);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=65536, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&pastTheBound, &values]() { MGPipeApplyCreateSamplerState(pastTheBound, &values); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty()) << "the table was grown by a corrupt slot";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A death notice on a record the applier does not have is the ONE refusal a legal sequence
|
||||
// produces - the teardown order - so it stays a defined no-op, and it is COUNTED because
|
||||
// MOBILEGL_ASSERT compiles out at INFO and every build that matters is one.
|
||||
TEST(SamplerEmit, ADeleteDropsTheRecordAndAStaleNoticeIsCountedRatherThanSilentlyDropped) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle cso{3, 7};
|
||||
const SamplerParameters values = SamplerValues(0.0f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, 0), &values);
|
||||
ASSERT_TRUE(MGPipeApplier().SamplerCsos[3].Live);
|
||||
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(cso));
|
||||
EXPECT_FALSE(MGPipeApplier().SamplerCsos[3].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[3].Gen, 7u) << "a destroy keeps the generation";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[3].Params.wrapT, SamplerWrapMode::Repeat)
|
||||
<< "a stale read of a deleted slot must find nothing, not the state that used to be there";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
// The second notice - the one a teardown produces - is refused and counted.
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(MGPipeHandle{3, 8}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 2u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A sampler view is IDENTITY-addressed one per texture object, minted off that object's
|
||||
// lifetime id, so a restriction change is a re-issue on the same handle rather than a new one.
|
||||
// The texture's own back-pointer is written here and cleared by the delete, and both are silent
|
||||
// lookups: the texture bit and the sampler bit are independent, so a view arriving without its
|
||||
// texture is an ordering fact and not a refusal.
|
||||
TEST(SamplerEmit, AViewIsReissuedOnTheSameHandleAndKeepsItsTexturesBackPointerInStep) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{5, 1};
|
||||
const MGPipeHandle view{9, 2};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 42));
|
||||
|
||||
MGPipeApplyCreateSamplerView(ViewOf(view, texture, 0));
|
||||
ASSERT_GT(MGPipeApplier().SamplerViewCsos.size(), 9u);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[9].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Serial, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.NumLevels, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[5].ViewCso, view)
|
||||
<< "the texture's back-pointer to its one view was not written";
|
||||
|
||||
// A restriction change: same handle, serial up, nothing started over.
|
||||
MGPipeApplyCreateSamplerView(ViewOf(view, texture, 2));
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Serial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.MinLevel, 2u);
|
||||
|
||||
// A view whose texture this applier has not been told about is stored anyway - refusing it
|
||||
// would make one legal A/B arm drop every view - and it counts no refusal.
|
||||
MGPipeApplyCreateSamplerView(ViewOf(MGPipeHandle{10, 1}, MGPipeHandle{77, 1}, 0));
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[10].Live);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
MGPipeApplyDeleteSamplerView(ViewHandle(view));
|
||||
EXPECT_FALSE(MGPipeApplier().SamplerViewCsos[9].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Gen, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[5].ViewCso, kMGPipeNullHandle)
|
||||
<< "the texture kept a back-pointer to a view that is gone";
|
||||
MGPipeApplyDeleteSamplerView(ViewHandle(view));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED: a set that names four units
|
||||
// has said nothing about the other 188, and clearing them would unbind textures the client
|
||||
// never mentioned. Deleting the entry loop, the window gate or either serial bump leaves this
|
||||
// red.
|
||||
TEST(SamplerEmit, TheTwoUnitSetsLandInTheirWindowAndLeaveEverythingOutsideItAlone) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
MGPBoundView views[2] = {};
|
||||
views[0].View = MGPipeHandle{1, 1};
|
||||
views[0].Texture = MGPipeHandle{2, 1};
|
||||
views[0].Unit = 4;
|
||||
views[1].View = kMGPipeNullHandle; // a unit the program does not resolve is legal
|
||||
views[1].Texture = MGPipeHandle{3, 1};
|
||||
views[1].Unit = 5;
|
||||
MGPSamplerViews viewHeader{};
|
||||
viewHeader.Start = 4;
|
||||
viewHeader.Count = 2;
|
||||
viewHeader.ContentHash = 0xABCDu;
|
||||
|
||||
const Uint64 viewSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
MGPipeApplySetSamplerViews(viewHeader, views);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[4].View, (MGPipeHandle{1, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[5].Texture, (MGPipeHandle{3, 1}));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[5].View));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[3].View)) << "the set wrote below its window";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[6].View)) << "the set wrote above its window";
|
||||
EXPECT_GT(MGPipeApplier().SamplerViewsSerial, viewSerial);
|
||||
|
||||
MGPipeHandle states[2] = {MGPipeHandle{8, 1}, kMGPipeNullHandle};
|
||||
MGPSamplerStates stateHeader{};
|
||||
stateHeader.Start = 4;
|
||||
stateHeader.Count = 2;
|
||||
const Uint64 stateSerial = MGPipeApplier().SamplerStatesSerial;
|
||||
MGPipeApplyBindSamplerStates(stateHeader, states);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerStates[4], (MGPipeHandle{8, 1}));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerStates[5]))
|
||||
<< "a unit with no sampler object carries a null CSO and the texture's built-in one applies";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 2u);
|
||||
EXPECT_GT(MGPipeApplier().SamplerStatesSerial, stateSerial);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, viewSerial + 1)
|
||||
<< "one set moved the other set's serial; the three are independent";
|
||||
|
||||
// A NARROWER SET DOES NOT CLEAR WHAT IT DOES NOT NAME - "the last set as received".
|
||||
MGPSamplerViews narrow{};
|
||||
narrow.Start = 4;
|
||||
narrow.Count = 1;
|
||||
MGPipeApplySetSamplerViews(narrow, views);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[5].Texture, (MGPipeHandle{3, 1}))
|
||||
<< "the entry outside the new window was cleared";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The window gate itself, at the bound and one past it, plus the null-tail arm. A header that
|
||||
// describes more than its destination can hold is the same class of fault as a blob outside
|
||||
// its segment, and the destination here is the merged 192-unit space.
|
||||
TEST(SamplerEmit, AUnitWindowPastTheMergedUnitSpaceIsRefusedRatherThanTruncated) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPBoundView entry{};
|
||||
entry.Texture = MGPipeHandle{2, 1};
|
||||
|
||||
// The positive control: a window ending EXACTLY at the bound is fine.
|
||||
MGPSamplerViews exact{};
|
||||
exact.Start = kMGPipeMaxTextureUnits - 1;
|
||||
exact.Count = 1;
|
||||
MGPipeApplySetSamplerViews(exact, &entry);
|
||||
ASSERT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
const Uint64 serialBefore = MGPipeApplier().SamplerViewsSerial;
|
||||
|
||||
MGPSamplerViews past{};
|
||||
past.Start = kMGPipeMaxTextureUnits - 1;
|
||||
past.Count = 2;
|
||||
past.ContentHash = 7;
|
||||
ExpectRefusedNaming("set_sampler_views {start=191, count=2, hash=7}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&past, &entry]() { MGPipeApplySetSamplerViews(past, &entry); });
|
||||
|
||||
MGPSamplerStates statesPast{};
|
||||
statesPast.Start = 0;
|
||||
statesPast.Count = kMGPipeMaxTextureUnits + 1;
|
||||
ExpectRefusedNaming("bind_sampler_states {start=0, count=193, hash=0}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&statesPast]() {
|
||||
MGPipeHandle one = kMGPipeNullHandle;
|
||||
MGPipeApplyBindSamplerStates(statesPast, &one);
|
||||
});
|
||||
|
||||
MGPSamplerViews noTail{};
|
||||
noTail.Start = 0;
|
||||
noTail.Count = 3;
|
||||
ExpectRefusedNaming("set_sampler_views {start=0, count=3, hash=0}: a non-empty set carries no entries",
|
||||
[&noTail]() { MGPipeApplySetSamplerViews(noTail, nullptr); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, serialBefore) << "a refused set moved the serial";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, kMGPipeMaxTextureUnits - 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4 for this family: the CSO and the view are OBJECT records and survive a make-current;
|
||||
// the two unit sets are WORKING state and do not, and their serials advance rather than
|
||||
// restarting.
|
||||
TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStanding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const SamplerParameters values = SamplerValues(3.0f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(MGPipeHandle{2, 1}, 0), &values);
|
||||
MGPipeApplyCreateSamplerView(ViewOf(MGPipeHandle{3, 1}, MGPipeHandle{4, 1}, 1));
|
||||
MGPBoundView entry{};
|
||||
entry.Texture = MGPipeHandle{4, 1};
|
||||
MGPSamplerViews header{};
|
||||
header.Count = 1;
|
||||
MGPipeApplySetSamplerViews(header, &entry);
|
||||
const Uint64 viewsSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
const Uint64 statesSerial = MGPipeApplier().SamplerStatesSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos[2].Live) << "a make-current dropped a share-group CSO record";
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[2].Params.lodBias, 3.0f);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[3].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u) << "the unit set is per context and must be cleared";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[0].Texture));
|
||||
EXPECT_GT(MGPipeApplier().SamplerViewsSerial, viewsSerial);
|
||||
EXPECT_GT(MGPipeApplier().SamplerStatesSerial, statesSerial);
|
||||
#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,814 @@
|
||||
// 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 <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#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
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // 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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the texture family (the wire commits'): set_texture_params on the
|
||||
// texture's own record, and the sub-data validator plus the pending-upload set that replaces
|
||||
// the frontend dirty flags the client clears at emission. The emitter's half - the descriptor
|
||||
// builder for every target, the sticky bind mask, the drain list, the level-shadow strides -
|
||||
// is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
constexpr Uint16 kTex2D = static_cast<Uint16>(MGPipeResourceTarget::Tex2D);
|
||||
|
||||
MGPResourceDesc TextureDesc(MGPipeHandle res, Uint32 width, Uint32 glName) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
desc.Width = width;
|
||||
desc.Height = width;
|
||||
desc.GlNameForDiag = glName;
|
||||
return desc;
|
||||
}
|
||||
|
||||
// Every field carries a value of its own so a body that stored the wrong one is visible BY
|
||||
// FIELD, which is what the family's descriptor-consistency control needs of it.
|
||||
MGPTextureParams TextureParams(MGPipeHandle res, MGPipeHandle builtinSampler, Uint16 baseLevel) {
|
||||
MGPTextureParams params{};
|
||||
params.Res = res;
|
||||
params.BuiltinSampler = builtinSampler;
|
||||
params.BaseLevel = baseLevel;
|
||||
params.MaxLevel = 7;
|
||||
params.Swizzle[0] = 1;
|
||||
params.Swizzle[1] = 2;
|
||||
params.Swizzle[2] = 3;
|
||||
params.Swizzle[3] = 4;
|
||||
params.DepthStencilMode = 5;
|
||||
params.ForceResync = 1;
|
||||
params.SamplerResync = 1;
|
||||
params.MinLod = -2.0f;
|
||||
params.MaxLod = 9.0f;
|
||||
params.LodBias = 0.5f;
|
||||
return params;
|
||||
}
|
||||
|
||||
MGPSubData TextureUpload(MGPipeHandle res, Uint16 level, const MGPBox& box, Uint32 regionCount) {
|
||||
MGPSubData record{};
|
||||
record.Res = res;
|
||||
record.Target = kTex2D;
|
||||
record.Level = level;
|
||||
record.SourceIsVerbatimLevelShadow = 1;
|
||||
record.UnionBox = box;
|
||||
record.RegionCount = regionCount;
|
||||
return record;
|
||||
}
|
||||
|
||||
MGPSubRegion Region(Int32 x, Int32 y, Uint32 w, Uint32 h) {
|
||||
MGPSubRegion region{};
|
||||
region.X = x;
|
||||
region.Y = y;
|
||||
region.Z = 0;
|
||||
region.W = w;
|
||||
region.H = h;
|
||||
region.D = 1;
|
||||
region.SrcOffset = static_cast<Uint64>(y) * 64 + static_cast<Uint64>(x) * 4;
|
||||
region.SrcRowStride = 256;
|
||||
region.SrcSliceStride = 0;
|
||||
return region;
|
||||
}
|
||||
|
||||
const MGPipeResourceRecord& TextureRecordOf(Uint32 slot) {
|
||||
EXPECT_GT(MGPipeApplier().TextureResources.size(), static_cast<SizeT>(slot));
|
||||
return MGPipeApplier().TextureResources[slot];
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// set_texture_params IS ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole reason
|
||||
// the call exists: 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. Deleting
|
||||
// the store or the ParamsSerial bump leaves this red.
|
||||
TEST(TextureEmit, ATexturesParametersLandOnItsOwnRecordAndMoveOnlyTheirOwnSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{7, 3};
|
||||
const MGPipeHandle sampler{2, 1};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 41));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 41), nullptr);
|
||||
ASSERT_EQ(TextureRecordOf(7).ParamsSerial, 0u) << "a create and a respecify are not a parameter push";
|
||||
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, sampler, 2));
|
||||
|
||||
const MGPipeResourceRecord& record = TextureRecordOf(7);
|
||||
EXPECT_EQ(record.Params.BuiltinSampler, sampler);
|
||||
EXPECT_EQ(record.Params.BaseLevel, 2u);
|
||||
EXPECT_EQ(record.Params.MaxLevel, 7u);
|
||||
EXPECT_EQ(record.Params.Swizzle[2], 3u);
|
||||
EXPECT_EQ(record.Params.DepthStencilMode, 5u);
|
||||
EXPECT_EQ(record.Params.ForceResync, 1u);
|
||||
EXPECT_EQ(record.Params.SamplerResync, 1u) << "the second resync bit is carried, not dropped";
|
||||
EXPECT_FLOAT_EQ(record.Params.MinLod, -2.0f);
|
||||
EXPECT_FLOAT_EQ(record.Params.LodBias, 0.5f);
|
||||
EXPECT_EQ(record.ParamsSerial, 1u);
|
||||
EXPECT_EQ(record.Serial, 1u) << "a parameter push is not a storage mutation and must not move Serial";
|
||||
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, sampler, 3));
|
||||
EXPECT_EQ(TextureRecordOf(7).Params.BaseLevel, 3u);
|
||||
EXPECT_EQ(TextureRecordOf(7).ParamsSerial, 2u);
|
||||
|
||||
// A stale generation resolves to nothing: the call is a DEFINED no-op and it is COUNTED,
|
||||
// because MOBILEGL_ASSERT compiles out at INFO and a no-op nobody can see is a dropped
|
||||
// parameter push nobody can see.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
MGPipeApplySetTextureParams(TextureParams(MGPipeHandle{7, 4}, sampler, 6));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 1);
|
||||
EXPECT_EQ(TextureRecordOf(7).Params.BaseLevel, 3u) << "a stale handle wrote the live record";
|
||||
EXPECT_EQ(TextureRecordOf(7).ParamsSerial, 2u);
|
||||
|
||||
// And a BUFFER of the same slot is not a texture: the two tables are independent, so this
|
||||
// is a refusal rather than a parameter push onto somebody else's record.
|
||||
MGPipeApplySetTextureParams(TextureParams(MGPipeHandle{9, 1}, sampler, 1));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 2);
|
||||
#endif
|
||||
}
|
||||
|
||||
// EVERY ITextureObject OWNS A SamplerObject, so a null built-in sampler CSO is not "no
|
||||
// sampler" - it is a record that would have the backend sample with whatever filter and wrap
|
||||
// state the unit last left behind. It is the corrupt-record verdict rather than the dropped-
|
||||
// call one, so it must NOT be counted as a refusal.
|
||||
TEST(TextureEmit, ARecordWithNoBuiltinSamplerCsoIsRefusedNamingTheTexture) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{6, 2};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 77));
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, MGPipeHandle{2, 1}, 1));
|
||||
ASSERT_EQ(TextureRecordOf(6).ParamsSerial, 1u);
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
|
||||
const MGPTextureParams noSampler = TextureParams(texture, kMGPipeNullHandle, 4);
|
||||
ExpectRefusedNaming("set_texture_params {slot=6, gen=2, glName=77}: the record names no built-in "
|
||||
"sampler CSO",
|
||||
[&noSampler]() { MGPipeApplySetTextureParams(noSampler); });
|
||||
EXPECT_EQ(TextureRecordOf(6).Params.BaseLevel, 1u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(TextureRecordOf(6).ParamsSerial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore)
|
||||
<< "a corrupt record is not a dropped call and must not be counted as one";
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-D5's safety net. The client clears its own dirty flags AT EMISSION and the backend's
|
||||
// upload loop has bail arms that would otherwise lose exactly those texels, so the emitted
|
||||
// shape accumulates SERVER-SIDE: boxes union, rect lists concatenate, and the moment either
|
||||
// side says "box only" the entry becomes box only - which is the frontend's own model, where
|
||||
// zero rects means "upload the union box instead" and covers every reason at once.
|
||||
TEST(TextureEmit, AnAccumulatedUploadUnionsItsBoxesAndCollapsesToTheBoxWhenARectListCannotDescribeIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{4, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 88));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 88), nullptr);
|
||||
|
||||
const MGPSubRegion first[2] = {Region(0, 0, 4, 4), Region(8, 8, 4, 4)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 12, 12, 1}, 2), texels, first);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UploadTarget, kTex2D);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Level, 0u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 12u);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads[0].Regions.size(), 2u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions[1].X, 8);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions[1].SrcRowStride, 256u)
|
||||
<< "the strides are CARRIED, never inferred: the pointer comparison they replace cannot "
|
||||
"survive a split";
|
||||
EXPECT_EQ(TextureRecordOf(4).Serial, 2u) << "an accepted upload moves the record's serial";
|
||||
|
||||
// A second emission behind a backend bail: the boxes union and the lists concatenate.
|
||||
const MGPSubRegion second[1] = {Region(16, 0, 8, 8)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{16, 0, 0, 8, 8, 1}, 1), texels, second);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u) << "the (target, level) key split in two";
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.X, 0);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 24u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.H, 12u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions.size(), 3u);
|
||||
|
||||
// A contribution with NO regions means "the box is the whole story", and the accumulated
|
||||
// entry has to say the same thing afterwards or the box would cover texels the rect list
|
||||
// does not name.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u);
|
||||
EXPECT_TRUE(TextureRecordOf(4).PendingUploads[0].Regions.empty())
|
||||
<< "a box-only contribution left a rect list that no longer covers the box";
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 64u);
|
||||
|
||||
// A different level is a different key, and a different upload target would be too.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 3, MGPBox{0, 0, 0, 8, 8, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 2u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[1].Level, 3u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 64u) << "level 3 rewrote level 0's box";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The texture half of the sub-data validator. Each of its four statements is about a record
|
||||
// that would make the server upload texels it was never told about, or read a tail it was not
|
||||
// given; removing any one of them leaves this red.
|
||||
TEST(TextureEmit, TheSubDataValidatorRefusesALevelABoxAndARegionTheRecordCannotDescribe) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{5, 2};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 99));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 99), nullptr);
|
||||
|
||||
// Positive control: the last legal level, a whole-level box and a region exactly filling
|
||||
// it are all fine, so what follows is refusing the value and not the arithmetic round it.
|
||||
const MGPSubRegion exact[1] = {Region(0, 0, 8, 8)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 31, MGPBox{0, 0, 0, 8, 8, 1}, 1), texels, exact);
|
||||
ASSERT_EQ(TextureRecordOf(5).PendingUploads.size(), 1u);
|
||||
const Uint64 serialBefore = TextureRecordOf(5).Serial;
|
||||
|
||||
const MGPSubData deepLevel = TextureUpload(texture, 32, MGPBox{0, 0, 0, 8, 8, 1}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the level is above the bound any "
|
||||
"texture's storage can have",
|
||||
[&deepLevel, &texels]() { MGPipeApplyResourceSubData(deepLevel, texels); });
|
||||
|
||||
const MGPSubData negative = TextureUpload(texture, 0, MGPBox{-1, 0, 0, 8, 8, 1}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the union box has a negative origin "
|
||||
"or runs past the bound one record can encode",
|
||||
[&negative, &texels]() { MGPipeApplyResourceSubData(negative, texels); });
|
||||
|
||||
const MGPSubData missingTail = TextureUpload(texture, 0, MGPBox{0, 0, 0, 8, 8, 1}, 2);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the record declares sub-regions and "
|
||||
"carries none",
|
||||
[&missingTail, &texels]() { MGPipeApplyResourceSubData(missingTail, texels); });
|
||||
|
||||
// THE ONE INVARIANT THAT MATTERS: the union box IS the union of the regions. The server
|
||||
// picks the upload shape from the pair, so a region outside the box means the box misses
|
||||
// its texels and the region writes where the box never said it would.
|
||||
const MGPSubRegion outside[1] = {Region(16, 0, 4, 4)};
|
||||
const MGPSubData escapes = TextureUpload(texture, 0, MGPBox{0, 0, 0, 8, 8, 1}, 1);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: a sub-region is not inside the union "
|
||||
"box the record declares",
|
||||
[&escapes, &texels, &outside]() {
|
||||
MGPipeApplyResourceSubData(escapes, texels, outside);
|
||||
});
|
||||
|
||||
const MGPSubData nothing = TextureUpload(texture, 0, MGPBox{0, 0, 0, 0, 0, 0}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the record describes no texels at all",
|
||||
[¬hing, &texels]() { MGPipeApplyResourceSubData(nothing, texels); });
|
||||
|
||||
EXPECT_EQ(TextureRecordOf(5).PendingUploads.size(), 1u)
|
||||
<< "a refused record was accumulated anyway";
|
||||
EXPECT_EQ(TextureRecordOf(5).Serial, serialBefore) << "not one refusal may move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A WHOLE-RESOURCE respecify - a null MGPRespecifiedLevel*, which is every glBufferData,
|
||||
// glBufferStorage, glTexStorage* and texture view - redefines every level at once, so the boxes
|
||||
// and rects against all of them go with it: a box kept across a shrink would have the backend
|
||||
// upload past the end of the new level. THE SCOPE IS THE WHOLE POINT: this case proves the
|
||||
// whole-resource arm ONLY, and its per-level twin below proves that the other arm may not do
|
||||
// this.
|
||||
TEST(TextureEmit, ARespecifyDropsThePendingUploadsAgainstTheStorageItReplaces) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{3, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 55));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 55), nullptr);
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels);
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(3).PendingUploads.size(), 2u);
|
||||
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 8, 55), nullptr);
|
||||
EXPECT_TRUE(TextureRecordOf(3).PendingUploads.empty())
|
||||
<< "a 64-wide box survived onto an 8-wide store";
|
||||
EXPECT_EQ(TextureRecordOf(3).Desc.Width, 8u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// C1, AND IT IS THE CANONICAL MIP-BUILDING SEQUENCE. A mutable texture defines its levels one
|
||||
// glTexImage*D at a time, and MG_State's AllocateStorage / MarkStorageDirty are per
|
||||
// (uploadTarget, level) - so defining level 1 re-marks LEVEL 1 AND NOTHING ELSE. Level 0's
|
||||
// client dirty flag was cleared at its own emission (D-D5 step 1) and the applier's entry is
|
||||
// the only thing that still owes those texels, because Espryt's incomplete-texture bail is
|
||||
// exactly the arm this set exists for. A blanket clear here destroys them silently, in every
|
||||
// build, with no counter and no log line: this case goes red the moment the level scoping is
|
||||
// dropped and green with it.
|
||||
TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{10, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 121));
|
||||
|
||||
// EVERY DESCRIPTOR BELOW CARRIES THE LEVEL COUNT THE CALL IT MODELS WOULD CARRY, and that
|
||||
// is not decoration. A mutable mip build grows MipmapStorage's level count as it defines
|
||||
// levels, so package B's descriptor (TextureEmit.h: `desc.Levels =
|
||||
// mipmap->GetMipmapLevelCount()`) MOVES on the glTexImage2D that adds level 1 - which is
|
||||
// also why B's own memcmp dedupe emits that respecify at all. A respecify whose
|
||||
// storage-defining fields are all unchanged is a METADATA update (ID-18 M4) and drops
|
||||
// nothing whatever level it names, so a case that fed the same descriptor three times would
|
||||
// be exercising that arm rather than this one.
|
||||
auto levelDesc = [&](Uint16 levels, Uint32 internalFormat) {
|
||||
MGPResourceDesc desc = TextureDesc(texture, 64, 121);
|
||||
desc.Levels = levels;
|
||||
desc.InternalFormat = internalFormat;
|
||||
return desc;
|
||||
};
|
||||
|
||||
// glTexImage2D(level 0, data): the respecify names the level it defines, and the drain then
|
||||
// emits level 0's shape, which the applier accepts.
|
||||
const MGPRespecifiedLevel levelZero{kTex2D, 0};
|
||||
MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u /*GL_RGBA8*/), nullptr, &levelZero);
|
||||
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels));
|
||||
// A SECOND FACE OF THE SAME LEVEL, keyed the way the packed Target keys it (ID-12: high
|
||||
// byte = the cube-face upload target, low byte = the resource target), so what survives is
|
||||
// a SET and not one lucky entry - and so that the level number alone cannot be what matched.
|
||||
const Uint16 secondFace = MGPipePackSubDataTarget(kTex2D, 1u);
|
||||
const MGPRespecifiedLevel faceOfLevelZero{secondFace, 0};
|
||||
MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u), nullptr, &faceOfLevelZero);
|
||||
MGPSubData otherFace = TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0);
|
||||
otherFace.Target = secondFace;
|
||||
ASSERT_TRUE(MGPipeApplyResourceSubData(otherFace, texels));
|
||||
ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u);
|
||||
|
||||
// Espryt BAILS - the texture is not mipmap-complete for its min filter - so both entries
|
||||
// are still owed when the next GL call arrives.
|
||||
//
|
||||
// glTexImage2D(level 1, data): this redefines level 1 of the (kTex2D, *) face only, and the
|
||||
// level count moves 1 -> 2 with it.
|
||||
const MGPRespecifiedLevel levelOne{kTex2D, 1};
|
||||
MGPipeApplyResourceRespecify(levelDesc(2, 0x8058u), nullptr, &levelOne);
|
||||
|
||||
ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u)
|
||||
<< "a respecify of level 1 dropped the pending uploads of levels it never redefined - "
|
||||
"those texels are lost for good, because their dirty flags were cleared at emission";
|
||||
EXPECT_EQ(TextureRecordOf(10).PendingUploads[0].UploadTarget, kTex2D);
|
||||
EXPECT_EQ(TextureRecordOf(10).PendingUploads[0].Level, 0u);
|
||||
EXPECT_EQ(TextureRecordOf(10).PendingUploads[0].UnionBox.W, 64u);
|
||||
EXPECT_EQ(TextureRecordOf(10).PendingUploads[1].UploadTarget, secondFace);
|
||||
|
||||
// And the key it DOES name goes, because that level's coordinate system has been replaced.
|
||||
// The redefinition modelled here is glTexImage2D(level 1) with a NEW internal format - a
|
||||
// legal thing to do to a mutable texture, and a real redefinition of that level's storage,
|
||||
// so the descriptor moves and the metadata arm does not claim it.
|
||||
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels));
|
||||
ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 3u);
|
||||
MGPipeApplyResourceRespecify(levelDesc(2, 0x8051u /*GL_RGB8*/), nullptr, &levelOne);
|
||||
ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u)
|
||||
<< "the level the respecify DOES redefine kept its box across the redefinition";
|
||||
for (const auto& entry : TextureRecordOf(10).PendingUploads) {
|
||||
EXPECT_FALSE(entry.UploadTarget == kTex2D && entry.Level == 1u)
|
||||
<< "the redefined (upload target, level) survived";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-D5 step 1 says the client clears its dirty flag "only for levels whose record the applier
|
||||
// ACCEPTED", and the call is the only thing that can say so: a dead or stale handle is a
|
||||
// counted no-op and a corrupt record is a Fatal that deliberately moves NO counter, so in a
|
||||
// shipped push build a refused upload and an accumulated one are otherwise identical from the
|
||||
// call site. An emitter that clears on the strength of having emitted loses those texels.
|
||||
TEST(TextureEmit, TheSubDataCallAnswersWhetherTheRecordWasAcceptedSoTheClientCanClearItsFlag) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{8, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 131));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 131), nullptr);
|
||||
|
||||
EXPECT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels))
|
||||
<< "an accumulated upload answered 'not accepted' and the client would re-send it forever";
|
||||
ASSERT_EQ(TextureRecordOf(8).PendingUploads.size(), 1u);
|
||||
|
||||
// A stale generation is the refusal that is NOT a Fatal, so it is the one the answer has to
|
||||
// carry: the record is gone, the texels were never taken, and the flag may not be cleared.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedResourceCalls;
|
||||
EXPECT_FALSE(
|
||||
MGPipeApplyResourceSubData(TextureUpload(MGPipeHandle{8, 2}, 0, MGPBox{0, 0, 0, 8, 8, 1}, 0), texels))
|
||||
<< "a refused upload answered 'accepted' and the client would clear a flag nothing owes";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, refusedBefore + 1);
|
||||
EXPECT_EQ(TextureRecordOf(8).PendingUploads.size(), 1u);
|
||||
|
||||
// The buffer half answers on the same terms, and its acceptance does not depend on a
|
||||
// backend table being registered - a unit process has none.
|
||||
const MGPipeHandle buffer{8, 1};
|
||||
MGPResourceDesc bufferDesc{};
|
||||
bufferDesc.Resource = buffer;
|
||||
bufferDesc.Target = kMGPipeResourceTargetBuffer;
|
||||
bufferDesc.Width = 256;
|
||||
bufferDesc.GlNameForDiag = 132;
|
||||
MGPipeApplyResourceCreate(bufferDesc);
|
||||
MGPipeApplyResourceRespecify(bufferDesc, nullptr);
|
||||
MGPSubData write{};
|
||||
write.Res = buffer;
|
||||
write.Target = kMGPipeResourceTargetBuffer;
|
||||
ASSERT_TRUE(MGPipeSetSubDataBufferRange(write, 0, 64));
|
||||
EXPECT_TRUE(MGPipeApplyResourceSubData(write, texels));
|
||||
write.Res = MGPipeHandle{8, 9};
|
||||
EXPECT_FALSE(MGPipeApplyResourceSubData(write, texels));
|
||||
#endif
|
||||
}
|
||||
|
||||
// m3. MGPSubData::Target is PACKED - low byte = MGPipeResourceTarget, high byte = the cube-face
|
||||
// upload target (ID-12) - so MGPipeResourceTarget::Renderbuffer is a perfectly well-formed
|
||||
// value for it, and without a gate a renderbuffer record would route to TextureResources and
|
||||
// accumulate a pending upload onto whatever TEXTURE holds that slot. It is the same argument
|
||||
// ResourceTableForTarget makes for returning null on an unknown enumerator, and the same
|
||||
// verdict: acting outside the storage the record names is corruption, not a dropped call.
|
||||
TEST(TextureEmit, ASubDataRecordWhoseResourceTargetNamesNoTextureIsRefusedRatherThanRouted) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{9, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 141));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 141), nullptr);
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(9).PendingUploads.size(), 1u);
|
||||
const Uint64 serialBefore = TextureRecordOf(9).Serial;
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedResourceCalls;
|
||||
|
||||
MGPSubData renderbuffer = TextureUpload(texture, 0, MGPBox{0, 0, 0, 8, 8, 1}, 0);
|
||||
renderbuffer.Target = static_cast<Uint16>(MGPipeResourceTarget::Renderbuffer);
|
||||
ExpectRefusedNaming("resource_subdata {slot=9, gen=1}: the record's resource target names no texture "
|
||||
"to upload into",
|
||||
[&renderbuffer, &texels]() { MGPipeApplyResourceSubData(renderbuffer, texels); });
|
||||
|
||||
// And so is a value at or above the catalogue, and so is the buffer target arriving with a
|
||||
// non-zero upload-target half - which the whole-field buffer test above cannot see.
|
||||
MGPSubData pastTheCatalogue = renderbuffer;
|
||||
pastTheCatalogue.Target = static_cast<Uint16>(MGPipeResourceTarget::Count);
|
||||
ExpectRefusedNaming("resource_subdata {slot=9, gen=1}: the record's resource target names no texture "
|
||||
"to upload into",
|
||||
[&pastTheCatalogue, &texels]() {
|
||||
MGPipeApplyResourceSubData(pastTheCatalogue, texels);
|
||||
});
|
||||
|
||||
MGPSubData packedBuffer = renderbuffer;
|
||||
packedBuffer.Target = static_cast<Uint16>(0x0100u | kMGPipeResourceTargetBuffer);
|
||||
ExpectRefusedNaming("resource_subdata {slot=9, gen=1}: the record's resource target names no texture "
|
||||
"to upload into",
|
||||
[&packedBuffer, &texels]() { MGPipeApplyResourceSubData(packedBuffer, texels); });
|
||||
|
||||
EXPECT_EQ(TextureRecordOf(9).PendingUploads.size(), 1u)
|
||||
<< "a record naming no texture was accumulated onto the texture holding that slot";
|
||||
EXPECT_EQ(TextureRecordOf(9).Serial, serialBefore) << "not one refusal may move the serial";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, refusedBefore)
|
||||
<< "a corrupt record is not a dropped call and must not be counted as one";
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4, for the kind that made the rule matter: a TEXTURE lives in a share group exactly as a
|
||||
// buffer does, so its record - and the parameters and the pending uploads that ride on it -
|
||||
// outlives a make-current, and only the applier's own teardown takes it.
|
||||
TEST(TextureEmit, TheTextureRecordAndItsParamsAndPendingUploadsSurviveAMakeCurrent) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{2, 5};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 66));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 32, 66), nullptr);
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, MGPipeHandle{1, 1}, 2));
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
ASSERT_TRUE(TextureRecordOf(2).Live) << "a make-current dropped a share-group object's record";
|
||||
EXPECT_EQ(TextureRecordOf(2).Desc.Width, 32u);
|
||||
EXPECT_EQ(TextureRecordOf(2).Params.BaseLevel, 2u);
|
||||
EXPECT_EQ(TextureRecordOf(2).ParamsSerial, 1u);
|
||||
ASSERT_EQ(TextureRecordOf(2).PendingUploads.size(), 1u)
|
||||
<< "the pending uploads are the safety net for a backend bail and cannot be per context";
|
||||
|
||||
// The write that follows the switch still lands, which is the whole point of the rule.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 16, 16, 1}, 0), texels);
|
||||
EXPECT_EQ(TextureRecordOf(2).PendingUploads.size(), 2u);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
|
||||
// And the teardown scope - the only other thing that clears a record - does take it.
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_TRUE(MGPipeApplier().TextureResources.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
// ID-18 M4, AND IT IS THE ONE ARM AN IMMUTABLE TEXTURE HAS. A sticky BindMask /
|
||||
// ImageBindableHint bit has exactly one way onto the wire - a respecify - and glTexStorage2D
|
||||
// leaves a texture with no further respecify to carry it, so for the canonical order (allocate,
|
||||
// THEN bind as an image or attach) the hint that exists to prevent a texture re-mint would never
|
||||
// arrive at all. B therefore republishes the descriptor when the mask moves, and the applier has
|
||||
// to tell that call apart from a redefinition: it replaces the descriptor and moves the serial,
|
||||
// and it drops NOTHING - the storage it is against was not replaced, so no level's coordinate
|
||||
// system moved. A mask change landing between a glTexSubImage2D and the sync that consumes it
|
||||
// must not eat those texels, which is C1's bug with a different trigger and just as silent.
|
||||
TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsThePendingUploads) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{11, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 151));
|
||||
|
||||
// glTexStorage2D: an IMMUTABLE store, which is the whole reason this arm exists.
|
||||
MGPResourceDesc allocated = TextureDesc(texture, 64, 151);
|
||||
allocated.Immutable = 1;
|
||||
allocated.Levels = 1;
|
||||
allocated.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
allocated.BindMask = static_cast<Uint16>(kMGPipeBindSampler);
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(allocated, nullptr));
|
||||
|
||||
// glTexSubImage2D: texels whose client-side dirty flag was cleared at THIS emission, so the
|
||||
// applier's entry is the only thing that still owes them.
|
||||
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels));
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u);
|
||||
const Uint64 serialBefore = TextureRecordOf(11).Serial;
|
||||
|
||||
// glBindImageTexture: the mask moves and nothing about the storage does.
|
||||
MGPResourceDesc masked = allocated;
|
||||
masked.BindMask = static_cast<Uint16>(allocated.BindMask | kMGPipeBindShaderImage);
|
||||
masked.ImageBindableHint = 1;
|
||||
ASSERT_FALSE(MGPipeResourceRespecifyNeedsAck(masked))
|
||||
<< "a metadata respecify must never ask for a reallocation acknowledgement";
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(masked, nullptr));
|
||||
|
||||
EXPECT_EQ(TextureRecordOf(11).Desc.BindMask, masked.BindMask)
|
||||
<< "the mask this call exists to carry did not reach the record";
|
||||
EXPECT_EQ(TextureRecordOf(11).Desc.ImageBindableHint, 1);
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
|
||||
<< "a respecify that redefined no storage ate the texels standing against it";
|
||||
EXPECT_EQ(TextureRecordOf(11).PendingUploads[0].UnionBox.W, 64u);
|
||||
EXPECT_GT(TextureRecordOf(11).Serial, serialBefore)
|
||||
<< "the serial is the whole publication of a metadata update - the twin re-derives its "
|
||||
"storage flags from the new mask on the strength of it";
|
||||
|
||||
// AND THE LEVEL POINTER DOES NOT CHANGE THE ANSWER. This is where ID-18 M4 refines C1:
|
||||
// C1's rule drops the uploads against the storage a call REPLACES, and a call that replaces
|
||||
// no storage replaces no level's coordinate system either, whatever level it names.
|
||||
const MGPRespecifiedLevel levelZero{kTex2D, 0};
|
||||
MGPResourceDesc maskedAgain = masked;
|
||||
maskedAgain.BindMask = static_cast<Uint16>(masked.BindMask | kMGPipeBindRenderTarget);
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelZero));
|
||||
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
|
||||
<< "a metadata update dropped the level it named";
|
||||
EXPECT_EQ(TextureRecordOf(11).Desc.BindMask, maskedAgain.BindMask);
|
||||
|
||||
// THE NEGATIVE CONTROL, in the same case: move ONE storage-defining field and the same call
|
||||
// is a redefinition again, which takes the level it names with it.
|
||||
MGPResourceDesc reallocated = maskedAgain;
|
||||
reallocated.Width = 32;
|
||||
reallocated.Height = 32;
|
||||
ASSERT_TRUE(MGPipeApplyResourceRespecify(reallocated, nullptr, &levelZero));
|
||||
EXPECT_TRUE(TextureRecordOf(11).PendingUploads.empty())
|
||||
<< "a 64-wide box survived a redefinition onto a 32-wide level";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-D5 step 1 again, for the two calls that DEFINE the storage an upload lands in (ID-18 M3).
|
||||
// The emitter cannot see either refusal from its call site: a dead or stale handle is a counted
|
||||
// no-op and a corrupt record is a Fatal that deliberately moves no counter, so a create or a
|
||||
// respecify the applier dropped is indistinguishable from one it took. A client that goes on to
|
||||
// clear a level's dirty flags, or to advance its own descriptor dedupe, on the strength of
|
||||
// having emitted has lost those texels for good.
|
||||
TEST(TextureEmit, TheCreateAndRespecifyCallsAnswerWhetherTheRecordWasAccepted) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{12, 1};
|
||||
EXPECT_TRUE(MGPipeApplyResourceCreate(TextureDesc(texture, 0, 161)));
|
||||
EXPECT_TRUE(MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 161), nullptr));
|
||||
ASSERT_TRUE(TextureRecordOf(12).Live);
|
||||
|
||||
// THE RESERVED SLOT is refused by the create, and the answer says so.
|
||||
EXPECT_FALSE(MGPipeApplyResourceCreate(TextureDesc(MGPipeHandle{0, 1}, 0, 162)))
|
||||
<< "resource_create answered accepted for the reserved slot 0";
|
||||
|
||||
// A STALE GENERATION is the one refusal that is not a Fatal, so it is the only one the
|
||||
// return can carry, and it is counted on the way out.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedResourceCalls;
|
||||
MGPipeHandle recycled = texture;
|
||||
recycled.Gen = 2;
|
||||
EXPECT_FALSE(MGPipeApplyResourceRespecify(TextureDesc(recycled, 64, 161), nullptr))
|
||||
<< "resource_respecify answered accepted for a handle it refused";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, refusedBefore + 1);
|
||||
EXPECT_EQ(TextureRecordOf(12).Desc.Width, 64u) << "a refused respecify moved the record anyway";
|
||||
|
||||
// AND THE BUFFER HALF ANSWERS ON THE SAME TERMS WITH NO BACKEND TABLE REGISTERED. Whether a
|
||||
// backend installed MGPipeResourceOps is a property of the BUILD and not of the record; an
|
||||
// emitter that read "not accepted" off an unregistered table would re-send a call the
|
||||
// applier has already taken responsibility for.
|
||||
MGPResourceDesc buffer{};
|
||||
buffer.Resource = MGPipeHandle{13, 1};
|
||||
buffer.Target = kMGPipeResourceTargetBuffer;
|
||||
buffer.GlNameForDiag = 163;
|
||||
EXPECT_TRUE(MGPipeApplyResourceCreate(buffer));
|
||||
buffer.Width = 256;
|
||||
EXPECT_TRUE(MGPipeApplyResourceRespecify(buffer, nullptr));
|
||||
MGPResourceDesc deadBuffer = buffer;
|
||||
deadBuffer.Resource.Gen = 7;
|
||||
EXPECT_FALSE(MGPipeApplyResourceRespecify(deadBuffer, nullptr));
|
||||
#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;
|
||||
}
|
||||
@@ -76,6 +76,10 @@ namespace {
|
||||
X(TrackerWalk, TheIndexBufferBitDoesNotFireOnAnUnrelatedBufferWrite) \
|
||||
X(TrackerWalk, TheIndexBufferBitFiresWhenTheSlotVersionWrapsOntoADifferentBuffer) \
|
||||
X(TrackerWalk, ABaseInstanceSurvivesTheFirstWalkOnAFreshContext) \
|
||||
X(TrackerWalk, ASamplerBindAloneFiresTheSamplerStateBit) \
|
||||
X(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) \
|
||||
X(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) \
|
||||
X(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) \
|
||||
X(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AnIntWriteCarriesTheIntWordsAndNamesItsClass) \
|
||||
X(TrackerAttribPayload, AUintWriteCarriesTheUintWordsAndNamesItsClass) \
|
||||
@@ -320,15 +324,29 @@ namespace {
|
||||
// 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
|
||||
// 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
|
||||
// against is now kMGPipeDirtyEmittedAtP3a - and a bit that gained an arm without gaining
|
||||
// an emitter, or the reverse, still fails here.
|
||||
// skip both rest on. P3a took the vertex-input family over and P4a takes seven more bits
|
||||
// across four subsystems, so the set it compares against is now kMGPipeDirtyEmittedAtP4a -
|
||||
// and a bit that gained an arm without gaining an emitter, or the reverse, still fails
|
||||
// here.
|
||||
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++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];
|
||||
}
|
||||
// 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::NewPixelPack), kMGPipeSubsystemPixelPack);
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
|
||||
@@ -339,6 +357,26 @@ namespace {
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements), kMGPipeSubsystemVertexInput);
|
||||
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers), 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) {
|
||||
@@ -584,6 +622,135 @@ namespace {
|
||||
m_cache.Reset();
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// P4a c0d: the two under-fires that only a bit WITH an emitter can be hurt by
|
||||
// ===================================================================================
|
||||
|
||||
// BIT 13 IS WHAT bind_sampler_states IS EMITTED OFF (PipeFill.cpp gates EmitSamplerStates
|
||||
// on NEW_SAMPLERS), and glBindSampler moved neither half of what its shutter used to read:
|
||||
// GL_Sampler.cpp's BindSampler_State calls NoteTextureUnitTouched and then
|
||||
// TextureUnit::SetSamplerObject, and BOTH of those bump the TEXTURE BIND generation, which
|
||||
// is bit 12's. The only writers of the sampling-resolution generation are parameter
|
||||
// changes. This case makes those two calls, in that order, with nothing else moving.
|
||||
TEST_F(TrackerWalk, ASamplerBindAloneFiresTheSamplerStateBit) {
|
||||
ASSERT_TRUE(Ctx().CreateSamplerObject(1) != nullptr);
|
||||
ASSERT_TRUE(Ctx().CreateSamplerObject(2) != nullptr);
|
||||
|
||||
Ctx().NoteTextureUnitTouched(3);
|
||||
Ctx().GetTextureUnitObject(3).SetSamplerObject(Ctx().GetSamplerObject(1));
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
// The one and only change: unit 3 now carries a DIFFERENT sampler object, whose
|
||||
// parameters happen to differ from the first one's. No glSamplerParameter*, no
|
||||
// glTexParameter*, so the sampling-resolution generation cannot have moved.
|
||||
Ctx().NoteTextureUnitTouched(3);
|
||||
Ctx().GetTextureUnitObject(3).SetSamplerObject(Ctx().GetSamplerObject(2));
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplers), 0u)
|
||||
<< "bind_sampler_states is emitted off NEW_SAMPLERS and never saw the sampler bind";
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u)
|
||||
<< "the view set is re-resolved on a sampler bind too - completeness depends on the "
|
||||
"effective sampler - and that half was already right";
|
||||
EXPECT_EQ(Walk(), 0u) << "the widened shutter fires forever";
|
||||
}
|
||||
|
||||
// BITS 6/7/8 UNDER A SEPARABLE PROGRAM PIPELINE. GetCurrentProgram() is null for the whole
|
||||
// life of a bound pipeline, so all three shutters used to latch 0 and never move again
|
||||
// after the first walk: glUseProgramStages would rebuild the composite and EmitShaderState
|
||||
// would never be called, leaving the new composite with no ShaderCso handle at all.
|
||||
TEST_F(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) {
|
||||
Vector<Uint> names;
|
||||
Ctx().GenProgramPipelineNames(1, names);
|
||||
ASSERT_EQ(names.size(), 1u);
|
||||
Ctx().CreateProgramPipelineObject(names[0]);
|
||||
Ctx().BindProgramPipelineObject(names[0]);
|
||||
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
||||
ASSERT_TRUE(pipeline != nullptr);
|
||||
ASSERT_TRUE(Ctx().GetCurrentProgram() == nullptr)
|
||||
<< "the premise of this case is that the program family has no current program";
|
||||
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
// What glUseProgramStages does at the end of its validation: one stage changes.
|
||||
const Uint vertex = Ctx().CreateProgram();
|
||||
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, Ctx().GetProgramObject(vertex));
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
||||
<< "the re-composited pipeline would never get a ShaderCso handle";
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u)
|
||||
<< "set_global_constants would never be sent for a pipeline draw";
|
||||
EXPECT_EQ(Walk(), 0u) << "the pipeline arm fires forever";
|
||||
}
|
||||
|
||||
// The same three bits, moved by the OTHER event that changes what a pipeline draws: a
|
||||
// stage program's relink. The stage set does not move at all here - only the link version
|
||||
// the composite cache is keyed on, which is what makes GetProgramForDraw build a new one.
|
||||
TEST_F(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) {
|
||||
Vector<Uint> names;
|
||||
Ctx().GenProgramPipelineNames(1, names);
|
||||
ASSERT_EQ(names.size(), 1u);
|
||||
Ctx().CreateProgramPipelineObject(names[0]);
|
||||
Ctx().BindProgramPipelineObject(names[0]);
|
||||
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
||||
ASSERT_TRUE(pipeline != nullptr);
|
||||
|
||||
const Uint vertex = Ctx().CreateProgram();
|
||||
const SharedPtr<MG_State::GLState::ProgramObject> stage = Ctx().GetProgramObject(vertex);
|
||||
ASSERT_TRUE(stage != nullptr);
|
||||
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, stage);
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
// A real relink, through the entry point glLinkProgram drives. It fails - the
|
||||
// program has no shaders attached - and that is deliberate: Link()'s PROLOGUE is where
|
||||
// the link-observable versions are bumped, before any early-out, precisely so that
|
||||
// every memo keyed on them reads stale from the instant the relink is enqueued.
|
||||
stage->Link();
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u);
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u);
|
||||
EXPECT_EQ(Walk(), 0u);
|
||||
}
|
||||
|
||||
// AND THE HANDOVER, which is the shape an application actually writes: a program is in
|
||||
// use, a pipeline is bound underneath it, and glUseProgram(0) hands the draw to the
|
||||
// pipeline (GL 4.6 core 7.4). The program in use wins while there is one - so the bits
|
||||
// must move when the SOURCE changes - and the pipeline must drive them afterwards.
|
||||
TEST_F(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) {
|
||||
Vector<Uint> names;
|
||||
Ctx().GenProgramPipelineNames(1, names);
|
||||
ASSERT_EQ(names.size(), 1u);
|
||||
Ctx().CreateProgramPipelineObject(names[0]);
|
||||
Ctx().BindProgramPipelineObject(names[0]);
|
||||
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
||||
ASSERT_TRUE(pipeline != nullptr);
|
||||
|
||||
const Uint installed = Ctx().CreateProgram();
|
||||
Ctx().UseProgram(installed);
|
||||
ASSERT_TRUE(Ctx().GetCurrentProgram() != nullptr);
|
||||
Walk();
|
||||
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
||||
|
||||
Ctx().UseProgram(0);
|
||||
const Uint32 handover = Walk();
|
||||
EXPECT_NE(handover & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
||||
<< "the draw's program source changed and bit 6 did not fire";
|
||||
ASSERT_EQ(Walk(), 0u);
|
||||
|
||||
// The pipeline is the source now, so a stage change has to reach the same bits.
|
||||
const Uint vertex = Ctx().CreateProgram();
|
||||
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, Ctx().GetProgramObject(vertex));
|
||||
const Uint32 dirty = Walk();
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
||||
<< "with no program in use the bound pipeline has to drive the program family";
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
||||
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u);
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// set_vertex_attrib_defaults' payload (P2 brief D10)
|
||||
// ===================================================================================
|
||||
|
||||
@@ -296,3 +296,26 @@ target_link_libraries(
|
||||
)
|
||||
|
||||
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
|
||||
// grep would break.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -281,6 +292,18 @@ namespace {
|
||||
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::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
|
||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
|
||||
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-ubo-named", "stage-vertex-client", "stage-index-client",
|
||||
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
"cso-blob-bytes",
|
||||
#endif
|
||||
};
|
||||
const char* const kCallClassNames[kCallClassCount] = {
|
||||
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
|
||||
"tex-upload-jobs",
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
"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
|
||||
};
|
||||
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",
|
||||
};
|
||||
// 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",
|
||||
"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"};
|
||||
|
||||
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
|
||||
// that says an adoption is happening per draw rather than per storage definition.
|
||||
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
|
||||
line += "] gates[";
|
||||
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
|
||||
// class is minted now so the counter names never churn; it stays at 0 until P2.
|
||||
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
|
||||
};
|
||||
|
||||
@@ -124,6 +139,23 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
// first window. Counted at the client emitter, behind the usual Enabled() predicate;
|
||||
// no timer anywhere.
|
||||
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
|
||||
Count
|
||||
};
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
|
||||
CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
|
||||
|
||||
**[deviation] D-G1(P3a 落地):vertex-elements CSO 在 P3a 是身份寻址,不是内容寻址。** Espryt 根本没有 vertex-elements CSO,它有的是**逐 VAO 的 twin**(`BackendVertexArrayObject`,`MobileGL/MG_Backend/DirectGLES/Managers.h:957-1163`),twin 持有一个驱动 VAO 名(`:1096`)、32 个 client-array scratch buffer id(`:1097`)与 32 个 fp64 scratch id(`:1101`);两个格式相同的前端 VAO 不能共享它,因为驱动 VAO 同时持有 element-array 绑定与逐属性缓冲绑定,共享 CSO 会把它们变成每次 `BindVertexElements` 都要重发——严格比今天更慢。所以 P3a 逐前端 `VertexArrayObject` 铸一个 `VertexElementsCso` 句柄(配置变化时**在同一句柄上重发** `CreateVertexElements`,`MGPipeHandle::Gen` 只在槽位复用时递增),**这是一个命中率恒为 1 的合法内容寻址缓存**。上面那张 1024 项的内容寻址表是 **P7** 的活——Magma 的 `VertexInputStateFactory` 接管 CSO 时,`VkPipelineVertexInputStateCreateInfo` 要的正是内容寻址;它加在 P3a 同一组 `CreateVertexElements`/`BindVertexElements`/`DeleteVertexElements` 与同一个 slot 分配器**之上**,P3a 的线上形状与 applier 记录都不妨碍它。
|
||||
|
||||
## 3. 调用目录(P0 已落地)
|
||||
|
||||
### 3.1 单一真相源
|
||||
@@ -127,7 +129,7 @@ Flags:`kNeedsAck`(调用方等 server 确认;目录里目前无条目携
|
||||
|---|---|---|
|
||||
| `MGPResourceDesc` | 88 | buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;`BindMask` 的 `ELEMENT_ARRAY` 位是索引镜像的开关;`ImageBindableHint` 预防性分配 image-bindable 存储;`ViewOf` 是纹理视图的存储属主(server 侧 keep-alive);`BufferForTexBuffer/BufOffset/BufSize` 实时解析(`kMGPipeWholeBuffer = ~0`)。Renderbuffer 保持独立类(自己的 format-capability target、`ComponentSizes`、twin) |
|
||||
| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / **12** / 32 | §5.3 |
|
||||
| `MGPVertexElements` | 40 | blob 同时带解析后的 `VertexAttribute[]` **和** `VertexBufferBindingPoint[]`,缺一不可(pointer 调用的 stride 0 = element size,binding 模型的 stride 0 = 每顶点读同一 element);`IsLong` 与 `Type == Float64` 分开携带;仅供查询的 `LegacyStride/LegacyPointer` 留在 client |
|
||||
| `MGPVertexElements` | 40 | blob 同时带解析后的 `MGPVertexAttribWire[]` **和** `MGPVertexBindingPointWire[]`(P3a 落地的两个 POD 线上形,24 B / 16 B,`MobileGL/MG_Pipe/MGPipeValueTypes.h:568-600`)。**两个视图都过线的理由是记录自洽,不是 stride 消歧**:前端已经把 pointer 调用的 stride 0 解析成 element size,一个活到 `VertexAttribute::Stride` 的 0 只可能来自 binding 模型(`MobileGL/MG_Pipe/MGPipeValueTypes.h:496-503`),后端从来不读 binding point(`MG_Backend` 里 `VertexBufferBindingPoint` / `GetAttributeBindingIndex` / `GetAttributeRelativeOffset` 零命中);真正承重的是 `MGPVertexElements` **声明**了 `BindingPointCount`,一条不描述自己 blob 的记录会让 applier 的边界门永远无法收口。代价按配置变化付一次、不按 draw 付(blob 只搭 `CreateVertexElements`),裁掉第二个视图是 P13 的重调项。`IsLong` 与 `Type == Float64` 分开携带;`Divisor` 不在属性视图里(走 `MGPVertexBuffer::Divisor`);仅供查询的 `LegacyStride/LegacyPointer` 留在 client |
|
||||
| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`**(三种 border color 表示永远都被数值填满,没有它后端无法在 `Iiv`/`fv` 或 `VkBorderColor` 家族间选择) |
|
||||
| `MGPSamplerView` / `MGPTextureParams` | 36 / 32 | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上 |
|
||||
| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + 四个状态字节,§7 |
|
||||
@@ -281,7 +283,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
|
||||
| `OnLog(level, text)` | ≤WARN 有损,≥ERROR 无损 + 速率限制 |
|
||||
| `OnXfbScatterReady(scratch, packedStride, vertices)` | §8.5 |
|
||||
|
||||
95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账(零消息);后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendResource` 删除(server 拥有资源表);`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payload(D-B8)。
|
||||
95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账(零消息);后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendResource` 删除(server 拥有资源表)——**[deviation] D-K(P3a):P3a 并没有删它**。`PipeResource::m_backend`、`SetBackendResource`、`ReleaseBackend`、`BackendBufferResource` 在 push 下只是**不再被写**(buffer twin 已经搬进第七张 slot 表,§9.5),真删掉会移动 pull 构建里 `sizeof(BufferObject)`,那是直接的 G1 破坏;这组删除随 pull 路径在 **P13** 退役。`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payload(D-B8)。
|
||||
|
||||
### 8.2 有序性是正确性要求
|
||||
|
||||
@@ -290,7 +292,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
|
||||
### 8.3 错误、ack 与日志
|
||||
|
||||
- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*` 只 `MarkStorageDirty`,Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。
|
||||
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack:41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。目录里目前没有条目携带 `kNeedsAck`(`ResourceRespecify` 是 `kNone`),标记随 P3a 的 buffer 路径落地。
|
||||
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack:41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。**P3a 起 `ResourceRespecify` 携带 `kNeedsAck`,并且带一个逐记录谓词**:flag 是**逐调用**的静态属性,而 `ResourceRespecify` 同时服务 `glBufferData` 与 `glBufferStorage`,裸 flag 会把 Minecraft 的整块 chunk 上传变成每 store 一次往返。所以真正拍板的是 `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)`(`MobileGL/MG_Pipe/MGPipeTypes.h:680`),`PipeCalls.def:18-24` 的图例把这条规则写在目录里(调用行 `:82`),`MG_Test/Pipe/PipeCatalogueTest.cpp:552-585` 的 `ResourceRespecifyAcksOnlyImmutableStorage` 对两种惯用法各钉一次。monolith 下 ack 是 `((void)0)`(applier 只隔一次函数调用),P5 把门铃接到这个谓词上。
|
||||
- 其余错误一律晚到,走有序的 `OnGlError`。
|
||||
- `OnLog` 分级:≤WARN 有损(覆盖最旧 + `eventDropped` 计数);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed";`MGLOG_E_ONCE` 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。
|
||||
|
||||
@@ -366,6 +368,12 @@ Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一
|
||||
|
||||
`MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)在 P3a/P4a 期间保留 registry / `TwinLookupMemo` 实现活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役(各阶段 +1 天维护)。
|
||||
|
||||
**P3a 的臂,逐项点名**(两个子系统的 arm 判定在 `MobileGL/MG_Backend/DirectGLES/Managers.cpp:2292`(resources,位 7)与 `:2312`(vertex input,位 8),逐进程各打一行 `MGLOG_D`):
|
||||
|
||||
- **buffer 家族的 pre-handle 臂是 `Ops_*` 表加 `g_glesBufferBackendOps`,它们无条件编译**,不在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下——所以位 7 单独清零永远有一条真臂可跑,`NoArm` 对这个家族不可达(`Managers.cpp:2294-2298` 把这句写在代码里)。前端的十一处分发点(`MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:45`、`:71`、`:87`、`:103`、`:237`、`:375`、`:438`、`:450`、`:493`、`:602`、`:656`)按 `MGPipeResourceSubsystemEnabled()` 二选一。
|
||||
- **VAO twin 的前端读取臂是有条件的**:pre-handle 的 `SyncToBackend` 本体与它读的那组 memo(`m_syncedIndexBufferVersion` / `m_syncedIndexBufferObject` / `m_hasSyncedConfigVersion` / `m_syncedConfigVersion` / `m_syncedAttributeVersions`)都在 `MOBILEGL_PIPE_LEGACY_MEMOS` 里(`MobileGL/MG_Backend/DirectGLES/Managers.h:1109-1130`)。位 8 清零 + `LEGACY_MEMOS=0` 是**没有任何 vertex-input 臂**的配置,`Managers.cpp:2343-2351` 明确报 `PipeLegacyMemosDisabled` 而不是静默。位 8 还要求位 7(属性的缓冲 id 经资源 slot 表解析),`0x17f` 会点名拒绝并回落到 legacy 臂。
|
||||
- **base-instance 的 ambient 作用域同在其中**:`SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance` 与 `DirectGLES.cpp:5293` 的三个 scope(`Managers.h:1189-1200`)。句柄臂改由 `MGPipeApplierState::VertexFetchBaseInstance` 供给,但这三个声明**不能删**:删掉会从 pull 构建移走两个符号(G1)。
|
||||
|
||||
P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()` 与 `MG_State` include**(D-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。
|
||||
|
||||
## 10. server 侧
|
||||
@@ -474,6 +482,8 @@ server 没有第二份 `BufferObject`,所以不存在"staging → server 侧 s
|
||||
| T3 — host pointer 导入(`VK_EXT_external_memory_host`) | | Adreno 无扩展;Mali 只读(GPU 写对宿主映射不可见) |
|
||||
| T2 — 拒绝(永久正确回退) | `AcquirePersistentMap` 返回 `nullptr`,前端已在三处容忍 | 此档下 client 侧推送强制 |
|
||||
|
||||
**`map-persistent-roundtrips`(`mpr`)的定义,P3a 拍板并落地**:它数的是**每一次 `MapPersistent` 发射,铸成还是拒绝都算**(`MobileGL/MG_Impl/Pipe/PipeFill.cpp:736`,client 侧发射器,`CallClass::MapPersistentRoundtrips`,`MobileGL/MG_Util/Metrics/PipeStats.h:126`,摘要行印 `mpr=`)。理由是 `ROADMAP.md:7` 的"每个门必须能因它存在的理由变红":定义成"真正发生的往返次数"在 monolith 下按构造恒为 0,永远红不了。按"每次获取尝试"计数则两种模式下**数字相同**,恰好等于上表 T1 那句"每次存储定义一次 round trip",在 monolith 下就非零、就可断言,而一个改成逐 draw 获取的回归立刻现形。门是 `StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw`(`MobileGL/MG_IntegrationTest/Scenarios/StorageBufferRegrowScenario.cpp:255`)与 `LargeArenaAdoptionScenario.AnAdoptionCostsExactlyOneMapPersistentRoundtrip`(`MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp:450`)。
|
||||
|
||||
`MOBILEGL_IPC_ADOPT_TIER`(`auto`/0/1/2)做负面对照;与 `MOBILEGL_IPC_RESPAWN` 互斥(被采纳的 store 是 server 拥有的内存)。
|
||||
|
||||
**client 侧 persistent map 推送三件套**(T2 档强制,P5):
|
||||
@@ -585,7 +595,7 @@ CMake:
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`;**push 构建 `0x7f`**(`kMGPipeSubsystemsMigratedAtP2`,`MobileGL/ConfigLoader.cpp:254`) | 子系统位图,十进制或 `0x`;位按 ROADMAP 顺序分配、永不复用(`MobileGL/MG_Pipe/MGPipe.h:72-85`):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch state、`0x08` vertex attrib defaults、`0x10` residual values、`0x20` Espryt slots(Track H)、`0x40` Magma vertex input(Track H);位 7..62 留给后续阶段。**位 63 不是子系统而是行为**:`kMGPipeBehaviourNoCsoContentAddressing` 关掉 client 侧 CSO 内容寻址(每次 pipeline 版本变化都铸新 CSO、永不探测 map),即 P2 的负面对照。`0` = 全 pull,但 P2 之后只有在 `MOBILEGL_PIPE_LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 |
|
||||
| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`;**push 构建 `0x1ff`**(`kMGPipeSubsystemsMigratedAtP3a`,`MobileGL/MG_Pipe/MGPipe.h:95`,读入点 `MobileGL/ConfigLoader.cpp:257`;P2 的默认是 `0x7f` = `kMGPipeSubsystemsMigratedAtP2`,`MGPipe.h:94`,保留作分阶段对照) | 子系统位图,十进制或 `0x`;位按 ROADMAP 顺序分配、永不复用(`MobileGL/MG_Pipe/MGPipe.h:72-95`):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch state、`0x08` vertex attrib defaults、`0x10` residual values、`0x20` Espryt slots(Track H)、`0x40` Magma vertex input(Track H)、**`0x80` 位 7 resources(P3a:`resource_*` 家族,`kMGPipeSubsystemResources`,`MGPipe.h:84`)**、**`0x100` 位 8 vertex input(P3a:vertex elements / vertex buffers / index buffer,`kMGPipeSubsystemVertexInput`,`MGPipe.h:85`)**;位 9..62 留给后续阶段。**位 8 依赖位 7**:属性的缓冲 id 经资源 slot 表解析,只有位 7 填那张表,所以 `0x17f` 会打一行 ERROR 点名拒绝位 8 并回落到 legacy vertex-input 臂(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:2312`)。**位 63 不是子系统而是行为**:`kMGPipeBehaviourNoCsoContentAddressing`(`MGPipe.h:90`)关掉 client 侧 CSO 内容寻址(每次 pipeline 版本变化都铸新 CSO、永不探测 map),即 P2 的负面对照。`0` = 全 pull,但 P2 之后只有在 `MOBILEGL_PIPE_LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 |
|
||||
| `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` | 0 | 负面对照 C(push 构建才有,`MobileGL/Config.h:360-371`):故意打掉句柄身份,让 `HandleRecycle` 的 ABA 臂重现旧的 A-B-A 污染。它变绿即为控制失效 |
|
||||
| `MOBILEGL_PIPE_VERIFY` | 0 | 逐 draw 逐字段影子比对 |
|
||||
| `MOBILEGL_PIPE_STATS` | 0 | 边界计数器(§附 B) |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 实测记录(P0、P1、P2)
|
||||
# 实测记录(P0、P1、P2、P3a)
|
||||
|
||||
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74C,Adreno 830,Android 16;`3B159D009VZ00000` = Oppo PLG110,Mali,Android 16(ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
|
||||
|
||||
@@ -166,6 +166,8 @@ python3 tools/trace_replay/run_android_retrace_local.py \
|
||||
|
||||
## 10. 设备配对 A/B:逐线程 CPU(D.4.2)
|
||||
|
||||
> **勘误(2026-09-08,P3a 收尾时发现)**:本节两机 64 次运行所用的两臂 APK 是**未优化构建**——其 `libMobileGL.so` 43.2 MB、`.text` 21.8 MB,而同一 clang 18.0.4 的 Release 库是 15.5 MB / `.text` 9.4 MB(两者都无 `.debug_info`),同一 `improved-transparency-minecraft-26.3` 用例在 Oppo 上 Espryt pull 的 CPU p50 为 42.1 ms 对 Release 的 9.7 ms。因此本节的**绝对值与 +8–18% 的差值都是 -O0 读数**(tracker 这类内联密集的代码在 -O0 下付出最多),不能作为性能基准线。**基准线以 §20 的 P3a 两机 A/B(3e298c9a 的 Release APK)为准**:其 pull 臂与 P2 的 pull 路径符号一致(P3a 全程 G1 0/0/0/0,与 44c2b5cf 只差 d7655247 的 5 行),其 `MOBILEGL_PIPE_PUSH=0x7f` 臂就是 P2 边界在 Release 下的代价。本节其余内容(协议、计数器、harness 误杀)仍然有效。构建脚本 `~/w7/notes/tools/wsl_build_trace_apks.sh` 从此打印库尺寸(Release `.text` ≈ 9.4 MB)。
|
||||
|
||||
协议:reboot-clean、同热窗口、按项目协议定频(大核 1.96 / 小核 1.55 GHz、GPU 拉满、40 °C 门),两臂背靠背同一会话,一次一台设备(`run_android_retrace_local.py` 每棵树共用一个结果根,见 §5.2),`--benchmark-no-finish` 为主臂(P2 问的是 CPU)。数字取 `benchmark.json` 的 `frameCpuTimesMs[]` 尾 200 帧,主机侧算 p50/p99。`minecraft-1.21.1-neoforge-create-indirect-in-world` 不在设备 A/B 里(§5.5,基线就坏)。
|
||||
|
||||
每格取 runner 自己的 "best of 3"(三次重复里平均墙钟帧时间最低的那次;`run_android_retrace_local.py` 的约定),p50 按设备的中位数规则、p99 为 nearest-rank,都在同一尾窗口上算;`tools/device_bench/pin_device.sh check` 在每次运行前后各跑一次,判定记在最后一列(P = 三个节点都在钉住的频率上;D = 大核被热管理钳在 1689600 kHz,脚本钉的是 1958400——用例前后两臂同一状态才可比)。APK 两臂出自 `55d2af9b`(P2 集成后、ABA 对照补丁前;差异只在负面对照臂)。
|
||||
@@ -307,3 +309,220 @@ $ ctest --test-dir build-push -R 'HandleRecycle' --no-tests=error -j 4 --output-
|
||||
|
||||
修好之后 32/32(多出来的四条是新增的 `AbaControlHandles` 臂,让对照能够到 P2 出货的 `{slot, gen}` 臂而不只是 pre-handle 臂),并且逐臂把判决打出来:`arm=Handles expected=FRESH observed=FRESH`、`arm=AbaControl expected=STALE observed=STALE`,两个臂都如此。**对照确实承重**:把 `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` 关掉,两个臂都变成 `observed=FRESH` 并**失败**——污染由被打掉的身份产生,别无他因,而退役的旧守卫与出货的 `{slot, gen}` 都能拦住它。
|
||||
|
||||
---
|
||||
|
||||
# P3a 实测(`feat/disaggregated@fde5fda3`)
|
||||
|
||||
> 基线:`5cb826b0`(P2 的 `44c2b5cf` 加上 `dev@9eae9858` 的合并);两个版本号提交之后 G1 的 pull 基线在 `b9eaa474` 重取。四个包 = contract/wire、client、espryt、gates,集成顺序 contract → wire → client → espryt → gates。
|
||||
>
|
||||
> **口径不变**:性能对着 pull 臂**记录**、不作阻塞门;正确性门照旧是硬门。所以下面 §16 是门,§19–§21 是记录与遗留判定。
|
||||
|
||||
## 15. 用户的口径规则,逐字
|
||||
|
||||
2026-09-08 的用户规则,`docs/Disaggregated/` 里此前没有一处写下它,所以逐字抄在这里:
|
||||
|
||||
> (a) advance the roadmap as fast as possible without a large performance regression; performance is RECORDED against the pull baseline, never a blocking gate in P3a; correctness gates stay.
|
||||
|
||||
落到 P3a 的后果:五部分门的**第 4 部分**(`ARCHITECTURE.md:514` 的 monolith 性能)与 `ROADMAP.md:19` 里那句"MC 26.3 在 Adreno 上 p99 不变"都变成**测了就发布**的义务,不是通过/不通过的判据;第 1、2、3、5 部分仍是硬门。一次回归要写下数字与怀疑的成因并带进 P4a 的排期——**唯一不可谈判的是数字必须真的采到**:没测到的回归不算记录在案。
|
||||
|
||||
## 16. P3a 五部分门(本地 `~/w7/pipe`,HEAD `3e298c9a`,基线 `44c2b5cf`)
|
||||
|
||||
**第 1 部分 —— 接口纯度**
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| 三个构建(pull / push / verify) | rc 0 / 0 / 0。fail-fast 规则原样保留:任一非零就打印 `BUILD FAILED - gate aborted (no stale-binary verdicts)` 并退出,绝不拿陈旧二进制下判决 |
|
||||
| A 门 include 闭包 | 4 个探针,0 skip,**0 problem** |
|
||||
| C 门 `MG_Backend` 里的 `pGLContext` | 空 |
|
||||
| G13 `PipeApply.h` 的 ops 块里的 `MG_State` | 空 |
|
||||
| **G1** pull 构建符号(P3a 的认定 resize 集**为空**) | `.text` 10806323 → **10806323(+0,+0.000%)**;27811 → 27811 个已定义符号,**0 增 / 0 删 / 0 resize / 0 重命名** |
|
||||
| **G5** 十个函数逐字节相同 | rc 0,对 `44c2b5cf` **与** `5cb826b0` 都成立(九个 pool / 延迟释放 / ring,加 ID-11 追加的 `FlushPendingRangesNow`) |
|
||||
| `RenderStateImpl` 段 sha(P2 的不变量) | `d8fd1c48716056c53675`,逐字相同 |
|
||||
| **G8** `HandleRecycle`(verify 构建) | **60/60**,含新增的 buffer 用例三臂 |
|
||||
|
||||
**第 2 部分 —— 语义影子比对(决定性的一条)**
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| `integration-verify` | **842/842**,`Fatal{` 行 **0** |
|
||||
| 79 例 retrace,`MOBILEGL_PIPE_VERIFY=1` | **79/79 通过**,**79/79 全部 armed**(每份日志都带 `MGPipe verify:` 行),带 `Fatal{` 的日志 **0**——即零分歧、零未迁移读 |
|
||||
|
||||
**第 3 部分 —— 行为 A/B**
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| **G2** pull 与 push 的 ctest 名集合 | 差 **0** 行 |
|
||||
| **G14** 测试名 | **0 删除 / +58** |
|
||||
| 单元 | **1619** 全绿 × {pull, push, verify} |
|
||||
| `integration-gpu` pull / push | **958/958** / **958/958** |
|
||||
| `MOBILEGL_PIPE_PUSH=0`(全 pull 臂) | **958/958** |
|
||||
| `MOBILEGL_PIPE_PUSH=0x7f`(P3a 两个子系统关闭,**G12**) | **958/958** |
|
||||
| `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1`(具名的 kill-switch 臂,见 §21) | **958/958** |
|
||||
| buffer/VAO 族(`LargeArenaAdoption`、`StorageBufferRegrow`、`VertexAttribBinding`、`MultiDraw`、`PrimitiveRestart`、`CrossFrameBuffer`、`ResidentIndex`、`BufferTexture`、`AtomicCounter`、`XfbCaptureBufferReuse`、`PackedWordReadback`、`DoublePrecision`、`VertexArrayEnableDisable`、`DrawParameters`) | **202/202** |
|
||||
| P2 的 G7 setter 一致性阴性对照 | rc 0,按设计变红后树又恢复绿 |
|
||||
| **G7** vertex-input 阴性对照 | rc 0 —— **变红并点名 `IsBgra`**,随后恢复、重建、再变绿 |
|
||||
| `CsoContentAddressing` + `ResourceSubsystemControl`(**G12**:开关真的改变行为,不是死代码) | **10/10** |
|
||||
| verify 构建的对照组(`PoisonOmitted`、`VerifyCorrupted`、`HandleRecycle`、`AbaControl*`、`ResourceSubsystem*`、`MapPersistentRoundtrips*`) | **64/64** |
|
||||
| 79 例 retrace(push) | **79/79 通过**(`failed: []`),每条都在自己的 SSIM 阈值之上 |
|
||||
| **G3b** 具名五条(`create-indirect`、`create-instancing`、`rd12-odinlite`、`improved-transparency-minecraft-26.3`、`fabric-sodium`) | **桌面侧全绿,但要看清是怎么绿的。** 五条**都在上面那次 79 例 push 扫描里通过**——它们全都在 `tools/trace_replay/trace_cases.json` 的语料内,而那次扫描是 79/79。**单独跑的那次具名扫描自己选中了 0 例**(`passed 0 / 0`):`retrace_gate.py` 的 `--only` 收的是**正则**,门脚本传的却是逗号分隔的清单,于是过滤器一条都没匹配上。这是门脚本的缺陷、不是本阶段的失败,重跑要写成 `--only 'create-indirect\|create-instancing\|rd12-odinlite\|improved-transparency-minecraft-26\.3\|fabric-sodium'`。**设备侧**按 `ROADMAP.md` 开放问题 17 把 `create-indirect` 排除在 A/B 之外,所以 **G3b 整体记为"桌面全绿、设备侧部分(受阻于 `dev` 的开放问题 17)"**,不当作完整通过 |
|
||||
|
||||
**第 4 部分 —— 性能:记录,不设门**(规则 (a))。见 §20。
|
||||
|
||||
**第 5 部分 —— 覆盖、poison、句柄纪律**
|
||||
|
||||
| 门 | 结果 |
|
||||
|---|---|
|
||||
| `gen_pipe.py --check` / `--self-test` | 生成物是最新的;self-test **7 个阴性对照全部触发**,正对照 OK |
|
||||
| `gen_pipe_dirty_surface.py --check`(**G9**,扫描根已扩到 `MG_State/GLState`) | **75 个 mutator 全映射、无陈旧行**;45 条 render-state 答案由 `RenderState.cpp` 推导并吻合,另 10 条 (mutator, bit) 答案由各自在 `Tracker.h` 里的快门推导,**0 COARSE / 0 UNDECIDED** |
|
||||
| `gen_pipe_dirty_surface.py --self-test` | **21 个阴性对照全部触发**,正对照 OK |
|
||||
| `RenderStateSpans` / `Residual` / **`VertexInputEmit`(G6)** / **`ResourceEmit`** | **48/48** |
|
||||
| `p3a_untouched_regions.sh --self-test`(**G5** 自己的阴性对照) | rc 0 —— 3 个正对照 + **2 个阴性对照**(扰动 `ClearBufferPool` 与 `FlushPendingRangesNow` 各被点名) |
|
||||
|
||||
**G15(完整 `gl44to46` caselist,约 56,271 例,两台设备)**:P3a 是五个架构边界之一,按 `ROADMAP.md:34` 排在 `dev` 合并时、**关键路径之外**;`MOBILEGL_PIPE_POISON_OMIT` 的清扫(§14 那 8 条静态过近似填充行)搭同一次 caselist 运行。
|
||||
|
||||
## 17. D.2 的"重键前红"证据:buffer 类
|
||||
|
||||
`ROADMAP.md:7` 要求每个门必须能因它存在的理由变红。P2 已经为 VAO 类留下了证据(§14 末尾);P3a 为 **buffer 类**留下这一份,取自 `p3a/contract` 树上、package C 还没注册 `MGPipeResourceOps` 的时刻,`ctest --test-dir build-verify -R HandleRecycle`:
|
||||
|
||||
```
|
||||
16/60 Test #2488: DirectGLES.HandleRecycle.Handles.HandleRecycleScenario
|
||||
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ......***Skipped
|
||||
22/60 Test #2494: DirectVulkan.HandleRecycle.Handles.HandleRecycleScenario
|
||||
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ......***Skipped
|
||||
28/60 Test #2500: DirectGLES.HandleRecycle.Legacy.HandleRecycleScenario
|
||||
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ...... Passed
|
||||
40/60 Test #2512: DirectVulkan.HandleRecycle.AbaControl.HandleRecycleScenario
|
||||
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ...... Passed
|
||||
100% tests passed, 0 tests failed out of 60
|
||||
```
|
||||
|
||||
读法与预期逐条吻合:**`.AbaControl` 通过**——那个臂**断言的就是被污染的内容**(把句柄身份打掉之后,复用地址上的新 buffer 读到前一个的字节),这正是"重键前红"的正面证据;**`.Legacy` 通过**(pre-handle 守卫拦得住);**`.Handles` 可见地 skip** 并写明缺什么,而不是一条消失的测试。package C 注册 `MGPipeResourceOps` 之后这两条 skip 转为断言,最终树上 `HandleRecycle` 是 **60/60**(§16 第 1 部分)。完整日志见 `~/w7/notes/p3a/p3a-results/handlerecycle-before.log`(另有 `-verbose.log`,含 22 行逐臂判决)。
|
||||
|
||||
## 18. 验证轮在真实流量下找到的三个缝隙缺陷
|
||||
|
||||
句柄路径第一次对着**真的发射器与真的 applier 本体**跑起来,是 espryt 的验证轮。默认位图那条 integration 通道当时留下 13 条红,其中 11 条是真的,掩码二分把它们干净地劈开(`0x7f`:0 条;`0xff`(只开位 7):5 条;`0x1ff`:11 条)。三个缺陷都在本包的文件清单内,都有实测的前后对比。**这一轮是承重的**——三个都是别的门看不见的形状:
|
||||
|
||||
| # | 缺陷 | 位置 | 表现 |
|
||||
|---|---|---|---|
|
||||
| **F1** | vertex buffer 条目按属性的 **GL 绑定点**解析,而不是按**属性下标** | `MobileGL/MG_Backend/DirectGLES/Managers.cpp` 的查找助手(`VertexBufferForBindingIndex` → `VertexBufferForAttributeIndex`)与它的调用点 | `MGPVertexBuffer::BindingIndex` 与 `MGPVertexAttribWire::BindingIndex` 是两个不同的数:Espryt 消费的是**已解析**的属性,所以 client 把 `set_vertex_buffers` 发成"逐属性槽一条",`BindingIndex == 属性下标`;另一个是 `glVertexAttribBinding` 绑上去的 GL 绑定点,而那个视图这条臂从不读。两者在所有 `glVertexAttribPointer` 配出来的属性上恰好相等——这就是别的场景全绿的原因;它们只在 `KHR-GL43.vertex_attrib_binding` 的题材上分叉,于是 `VertexAttribBindingScenario` 的 6 条读到禁用/零默认值。单这一个修完 6 → 3 |
|
||||
| **F2** | ensure 路径去问一个**没有任何内容调用会刷新**的描述符,来判断影子里有没有字节 | `Managers.cpp` 的 `EnsureBufferResourceForHandle` | `MGPResourceDesc::HasDefinedContent` 陈述的是**上一次 `resource_respecify` 当时**的事实,此后 `NotifySubData` / `NotifyFlushMappedRange` / `NotifyContentWrite` / `MarkGpuWritten` / `LandBytesIntoResidentStore` 都只改前端的 `m_hasDefinedContent`、不重发描述符(逐 `glBufferSubData` 重发是新线上流量,设计上禁止)。于是在语料里最常见的惯用法 `glBufferData(size, NULL)` + `glBufferSubData(data)` 之后,句柄臂用**应用的字节**配上**applier 的描述符**,重定义出一个"已声明为当前"的空 store,应用的字节被丢掉且没有任何诊断——**静默的错像素**,不是崩溃。改为在持有前端对象时读 `HasDefinedContent()`(无对象的纯句柄排水仍读描述符)。关掉了全部 4 条 `XfbCaptureBufferReuse` 与 `LargeArenaAdoption.RespecifiedIndexArenaKeepsVaoBinding`,即所有 `0xff` 红 |
|
||||
| **F3** | 惰性生成的 twin 从不发布影子基址,于是 fp64 收窄拒绝每一个 draw | `Managers.cpp` 的 `EnsureBufferResourceForHandle` | `GLESBufferResource::hostBytes` 是那些**不持有**前端对象的读者读的(回读队列排空、flush-range 的 kill-switch map 臂、`SyncFloat64AttributeAsFloat32ByHandle`)。`ResourceCreate` 是 no-op,twin 因此是**惰性**建的,第一次 `resource_respecify` 找不到 twin,把基址丢在地上;对"`glGenBuffers` + `glBufferData(size, data)` 之后再无内容调用"的普通静态顶点数组,`hostBytes` 就此终身为 null。整份重传没事(它走另一个取基址的路径),fp64 收窄却因 `sourceBase == nullptr` 直接失败 → Adreno workaround 禁用该属性 → 着色器读到 `(0, 0, 0, 1)`。修法是在已经算出基址、且每个用到该 store 的 draw 之前都会跑到的那一处补发布,且**只对非采纳资源**(采纳臂更早返回,所以 adopted store 的 `hostBytes` 仍为 null、字节仍走 `persistentPtr`)。关掉最后 3 条 `VertexAttribBinding` |
|
||||
|
||||
三个修完之后那条通道在 espryt 自己的树上是 920/920,最终集成树上是 §16 的 958/958。诊断用的五行临时 `MGLOG_E("TRACE …")` 在任何提交之前就已删除。
|
||||
|
||||
同一轮里的两条方法学记录。**包的 worktree 里 `tools/trace_replay/fixtures` 是 LFS 指针**(131/133 字节的 stub 对 `~/w7/pipe` 的 803 MB),对着指针文件跑 retrace 会报 `passed 2 / 79`、每条 Minecraft 用例 `ssim=None`,读起来跟"整体回归"一模一样——那是**假红**,建树脚本不物化 fixture。**base-instance 对照在 llvmpipe 上要先关掉原生 base-instance 才可证伪**:llvmpipe 暴露 `GL_EXT_base_instance`,原生门因此为真、`fetchBaseInstance` 被钳成 0,而属性偏移仿真才是 `MGPipeApplierState::VertexFetchBaseInstance` 在整个后端里**唯一**的消费者。把两个原生门都强制为假之后,对照两个方向都成立:三个调用点在时 10/10 绿,抠掉调用点时 8/10。**这条门是 `VertexAttribBindingScenario` 的 `BaseInstanceMovesTheInstancedArraysStartElement` 与 `BaseInstanceLeavesPerVertexArraysWhereTheyWere` 两条,不是 `DrawParametersScenario` 的八条**——后者观察的是 `gl_BaseInstance` / `gl_DrawID`,来自 `SetCurrentBaseInstance`,与取数偏移正交,两个方向都绿是对的。
|
||||
|
||||
## 19. Track H 单位成本普查(`ROADMAP.md:49` 的判据)
|
||||
|
||||
**日历口径:P3a 全部四个包 = 1 天**(2026-09-08)。contract/wire、client、espryt、gates 四个包各一轮实现加一轮对抗性评审加至多两轮返工,连同集成与本地五部分门,全部落在同一个日历日内。对着 `ROADMAP.md:66` 的 **27 天绊线**是 **1 / 27**,估时是 18–23 天,**没有超出估计的 50%**,不触发重定基线,也不需要 `ROADMAP.md:70` 的 `inproc` 证伪数字。§14 那条"Track H 单位成本的日历口径:未记录"到此补上一半——P3a 自己的口径有了,P2 两个包的仍然没有。
|
||||
|
||||
**diff 规模**(`git diff --stat 5cb826b0 3e298c9a`,区间内 44 个提交):
|
||||
|
||||
```
|
||||
37 files changed, 9273 insertions(+), 230 deletions(-)
|
||||
```
|
||||
|
||||
**memo 账**(`ARCHITECTURE.md:363` 那份 21 条普查;P2 付了 11 条直接删除里的全部 11 条与 7 条重键里的 2 条):
|
||||
|
||||
| 类别 | P3a 付了什么 |
|
||||
|---|---|
|
||||
| 直接删除(普查 11 条里的最后一条) | `ConvertedVertexStreamKey` 的 `sourcePin`(`ConvertedFloat64Stream::sourceLifetimeId`) |
|
||||
| 句柄臂上另外退役的 twin 成员 | `m_hasSyncedConfigVersion`、`m_syncedConfigVersion`、`m_syncedAttributeVersions`(`Array<VertexAttributeVersion, 32>`,本阶段最大的一处)、`m_syncedIndexBufferVersion`、`m_syncedIndexBufferObject`(裸前端指针,堵回绕洞的那个身份补丁) |
|
||||
| 重键 | VAO twin 的索引槽 memo(回绕 `Uint16` + 裸 `BufferObject*` → 一个 server `Serial`);`ResolvedDrawBuffers`(`configVersion` → `{elementsHandle, elementsSerial, buffersSerial}`,`Entry` 与 `iboFrontend` 各加一个 `MGPipeHandle`);twin 的同步门(config version + 32 组逐属性版本 → `{elementsHandle, elementsSerial}` + `VertexBuffersSerial`);`ConvertedFloat64Stream`(前端 lifetime id + change serial → buffer `{slot, gen}` + applier `Serial`);`GLESBufferResource::syncedChangeSerial` 从镜像 `BufferObject::GetChangeSerial()` 改为镜像 applier 的 `Serial` |
|
||||
| **保留**(`MOBILEGL_PIPE_LEGACY_MEMOS`) | 上面每一条"退役"都只是**句柄臂上**的:成员仍在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下编译(`MobileGL/MG_Backend/DirectGLES/Managers.h:1109-1130`、`:1067-1075`),pull 构建强制该开关 ON,所以 `sizeof` 一处不动——**G1 的 0/0/0/0 就是这条的度量** |
|
||||
| **保留,且计划里的"grep 为空"不可达** | `g_pendingFetchBaseInstance` / `SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance` 与它的三个 scope(`MobileGL/MG_Backend/DirectGLES/Managers.h:1189-1200`、`MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5293`)。句柄臂改由 `MGPipeApplierState::VertexFetchBaseInstance` 供给,但**真删会从 pull 构建移走两个符号**,是直接的 G1 破坏;所以计划里那条"`grep` 结果为空"在 P3a **不可达**,每一处残留命中都在一个 `MOBILEGL_PIPE_LEGACY_MEMOS` 臂里。这是记录在案的偏差,随 pull 路径在 P13 退役 |
|
||||
| **保留(D-K)** | `PipeResource::m_backend`、`SetBackendResource`、`ReleaseBackend`、`BackendBufferResource`:push 下不再被写,删除会移动 pull 构建里的 `sizeof(BufferObject)`,同样随 P13 退役(`ARCHITECTURE.md:286`) |
|
||||
|
||||
## 20. 设备配对 A/B、MC 26.3 的 p99、DriverBench(记录项)
|
||||
|
||||
协议与 §10 完全相同:reboot-clean、同热窗口、按 `tools/device_bench/pin_device.sh` 定频(大核 1.96 / 小核 1.55 GHz、GPU 拉满、40 °C 门,每次运行前后各 `check` 一次,DRIFT 的那次作废重跑)、两臂背靠背、一次一台设备、`benchmark.json` 的 `frameCpuTimesMs[]` 尾 200 帧在主机侧算 p50/p99(p99 用 nearest rank,p50 按设备自己的中位数规则)。臂 = {pull APK, push APK} × {`--benchmark-no-finish`(主臂,P3a 问的是 CPU), finish(GPU 时间没动的 sanity)} × 4 用例 × 2 后端。用例:`improved-transparency-minecraft-26.3`、`minecraft-1.21.4-rd12-odinlite-in-world`、`minecraft-1.21.4-fabric-sodium-in-world`、`minecraft-1.21.1-neoforge-create-instancing-in-world`(一个 `coherent_as_flush` fixture)。**`minecraft-1.21.1-neoforge-create-indirect-in-world` 排除在外**,理由与 P2 相同:它在 `dev@81b17c0b` 的基线 APK 上就在两台设备上失败(§5.5、`ROADMAP.md` 开放问题 17),不是本分支的回归;它留在桌面 SSIM 语料里。同一批运行里读出六个 memo 门的 hit/miss、`stage-*` 字节类与 **`mpr`**。
|
||||
|
||||
**读数纪律,必须写在数字上面而不是下面**:`CallClass::AccessorCalls` 是约 10 个热入口上的**静态计数**(`MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份站点清单本身就是契约),所以一个只把读**挪了位置**的改动会拿到更低的 `acc/draw` 而并没有减少工作量。带头看的应当是**六个 memo 门的 hit/miss** 与 **CPU 时间序列**;引用 `acc/draw` 必须同时复核那些 tally 常量。
|
||||
|
||||
**两机配对 A/B(D.4.2),三臂**:APK 都是 `3e298c9a` 的 Release trace 构建(`wsl_build_trace_apks.sh`,`.text` 9.4 MB,debug 签名);pull 臂 = pull 库;`P2` 臂 = 同一个 push APK 跑 `--env MOBILEGL_PIPE_PUSH=0x7f`(只开 P2 的七个子系统,P3a 关);`P3a` 臂 = push APK 默认掩码 `0x1ff`。协议同 P2(reboot-clean、`pin_device.sh` 三次校验、每次运行前后 check、`--benchmark-repeats 3 --benchmark-tail-frames 200`、runner 的 best-of-3、`MOBILEGL_PIPE_STATS_PERIOD=120`);表取 `--benchmark-no-finish` 臂,单位 ms/帧,p50 括号内为相对 pull 的增量。
|
||||
|
||||
| 设备 | trace | 后端 | pull p50 | P2 臂 p50 | P3a 臂 p50 | pull p99 | P3a p99 | 备注 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 小米 Adreno 830 | `improved-transparency-minecraft-26.3` | Espryt | 10.810 | 11.485(+6.2%) | 11.697(**+8.2%**) | 25.457 | 26.322 | finish 开:10.789 → 11.705(+8.5%) |
|
||||
| 小米 | 同上 | Magma | 10.675 | 11.473(+7.5%) | 11.459(**+7.3%**) | 25.014 | 26.035 | finish 开:10.710 → 11.486 |
|
||||
| 小米 | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 8.220 | 9.117(+10.9%) | 10.650(**+29.6%**) | 21.895 | 24.487 | finish 开:8.241 → 10.656 |
|
||||
| 小米 | 同上 | Magma | — | — | — | — | — | 三臂都在启动数秒后 SIGABRT(scudo map error,见下),既有问题 |
|
||||
| 小米 | `minecraft-1.21.4-fabric-sodium-in-world` | Espryt | 1.292 | 1.349(+4.4%) | 1.382(**+7.0%**) | 2.418 | 2.443 | |
|
||||
| 小米 | 同上 | Magma | 0.485 | 0.504 | 1.010 | 1.469 | 2.187 | 亚毫秒帧,pull 自己两模式相差 2×(0.485 / 0.984),噪声,不读 |
|
||||
| 小米 | `minecraft-1.21.1-neoforge-create-instancing-in-world` | Espryt | 944.4 | 945.2 | 949.9(+0.6%) | — | — | fixture 只有两帧(p50 = mean),是正确性 fixture 不是基准 |
|
||||
| 小米 | 同上 | Magma | 1003.9 | 1019.5 | 1015.6(+1.2%) | — | — | 同上 |
|
||||
| Oppo Mali | `improved-transparency-minecraft-26.3` | Espryt | 9.741 | 10.570(+8.5%) | 10.542(**+8.2%**) | 26.312 | 27.022 | finish 开:9.625 → 10.698(+11.1%) |
|
||||
| Oppo | 同上 | Magma | 8.162 | 8.920(+9.3%) | 8.973(**+9.9%**) | 22.851 | 23.638 | finish 开:8.120 → 9.064 |
|
||||
| Oppo | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 9.938 | 11.009(+10.8%) | 12.963(**+30.4%**) | 27.137 | 29.765 | finish 开:9.944 → 12.852 |
|
||||
| Oppo | 同上 | Magma | 8.001 | 8.933(+11.6%) | 10.161(**+27.0%**) | 24.723 | 27.355 | finish 开:7.718 → 10.226 |
|
||||
| Oppo | `minecraft-1.21.4-fabric-sodium-in-world` | Espryt | 1.896 | 1.770 | 1.841(−2.9%) | 2.993 | 3.079 | 亚 2 ms 帧,噪声内 |
|
||||
| Oppo | 同上 | Magma | 0.399 | 0.431 | 0.437(+9.5%) | 1.206 | 1.409 | 亚毫秒帧 |
|
||||
| Oppo | `minecraft-1.21.1-neoforge-create-instancing-in-world` | Espryt | 1075.1 | 1045.1 | 1060.1(−1.4%) | — | — | 两帧 fixture |
|
||||
| Oppo | 同上 | Magma | 758.3 | 731.0 | 760.0(+0.2%) | — | — | 两帧 fixture |
|
||||
|
||||
所有入表运行前后 `pin_device.sh check` 都是 PINNED(小米 rd12/Magma 崩溃后 GPU pwrlevel 被重置,其后的 sodium/create-instancing 行两臂同状态)。
|
||||
|
||||
**读法。** (1) **P2 的边界在 Release 下的真实代价是 +6–12%**(`0x7f` 臂),两机两后端一致,比 -O0 表的 +8–18% 小但同量级;(2) **P3a 在 26.3 与 sodium 上几乎不再加价**(P3a 臂与 P2 臂在 26.3 上相差 −0.1 ~ +2 个百分点),**但在 rd12 上把差距从 +11% 推到 +27–30%**——rd12(Odin Lite 世界)每帧的 VAO/buffer 绑定切换远多于 26.3(26.3 的 1350 draw/帧大多复用同一 VAO),每次切换都走一遍 `set_vertex_buffers` 构造 + `ContentHash` + applier 记录 + Espryt 侧逐属性走查;Magma 上没有句柄消费者也多 15 个百分点,说明 client 侧发射本身就是大头;(3) **MC 26.3 在 Adreno 上的 p99**(`ROADMAP.md:19` 点名的那个数):pull 25.46 ms → P3a 26.32 ms(+3.4%),finish 开 25.48 → 26.29;对着 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms)仍在 21–26 ms 档,没有回到采纳前的形态——按口径记录,不判门;(4) `mpr`(map-persistent-roundtrips,按窗口累加):26.3 两机都是 8(首窗 4,之后两次 2——都是 ≥16 MiB store 定义时的采纳),sodium 1,rd12 与 create-instancing 0;P2 臂上恒 0(子系统关)——G10 在设备上成立;(5) `CreateVertexElements` 每帧字节数:统计行没有这一类(`vtxc` 是 client 数组),**未测**,留给 P4a 给汇总行加类;(6) 计数器(`acc/draw`、六个 memo 门、`resid=`、`csom/csob`)在 pull/P2/P3a 三臂间逐字相同——它们数的是代码路径,P3a 没有改它们的定义。
|
||||
|
||||
**小米 rd12 + Magma 的崩溃**:三臂(含 pull)都在启动后数秒 `SIGABRT`:`scudo::reportMapError` ← `remapImpl` ← `scudo_calloc` ← `libMobileGL.so`(0x818b14 / 0x7e2c44,已剥符号),当时 MemAvailable 6.1 GB——一次巨大或负尺寸的 calloc,在 Adreno 830 + Magma + 这条 fixture 上;pull 库与 P3a 前的 Magma 路径符号一致,所以是**既有 bug**,不入 P3a 账,已开独立任务(先符号化再修)。Oppo/Magma 与小米/Espryt 上同一 fixture 正常。
|
||||
|
||||
**harness 与设备陷阱(本轮新增)**:Oppo ColorOS 的安装确认页是 `topResumedActivity=…InstallGuideActivity`(`mCurrentFocus` 显示 systemui 窗口,按它 grep 永远匹配不到),首次安装不点会等到 harness 超时;在 streamed install 进行中点它会让设备从 adb 掉线几秒(那一轮的三次重复全失败,需要重跑);小米 32 次运行后立即 reboot+pin 必因热钳失败(大核钳 1689600 或 GPU 读 1050 MHz),要等 cpuss-0-0 <42 °C;一次崩溃会把 GPU pwrlevel 范围重置成 2..5。
|
||||
|
||||
对照的既有基线是本文件 `:87`——`dev` 上 MC 26.3 在 Adreno 上把 ≥16 MiB 可变 store 采纳为 coherent persistent map 之后的 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB。P3a 的 `AcquirePersistentMap` 一个语句都没动(`ARCHITECTURE.md:474` 的 D-B4),`FlushPendingRangesNow` 的三档排水在 G5 的逐字节集合里,所以这条 p99 是"没动过的东西是否真的没动"最直接的读数。
|
||||
|
||||
**DriverBench(桌面 llvmpipe / lavapipe,`~/w7/notes/tools/wsl_p3a_bench.sh`:Release,240 帧,每臂 5 次重复取中位数;pull 库 = `build-linux`,push 库 = `build-push`,都是 `c20e2f2b`)**,`ns_per_op`:
|
||||
|
||||
| 臂 | `mc_vanilla_draw`(ns/draw) | `mc_state_toggle`(ns/开关对) | `mc_pass_switch`(ns/pass) |
|
||||
|---|---|---|---|
|
||||
| native | 4759 | 22715 | 438175 |
|
||||
| Espryt pull | 5115 | 23287 | 435815 |
|
||||
| Espryt push,默认 `0x1ff` | 6162 | 24496 | 435975 |
|
||||
| Espryt push,`MOBILEGL_PIPE_PUSH=0x7f`(只开 P2 子系统) | 5463 | 24576 | 436031 |
|
||||
| Espryt push,`MOBILEGL_PIPE_PUSH=0` | 5693 | 24465 | 436598 |
|
||||
| Espryt push,位 63(无 CSO 内容寻址) | 6156 | 24856 | 435276 |
|
||||
| Magma pull | 17099 | 32612 | 436748 |
|
||||
| Magma push,默认 `0x1ff` | 17709 | 33537 | 437314 |
|
||||
| Magma push,`0x7f` | 17287 | 33550 | 436458 |
|
||||
| Magma push,`0` | 17739 | 33800 | 440446 |
|
||||
| Magma push,位 63 | 17729 | 34222 | 436465 |
|
||||
|
||||
分解(`mc_vanilla_draw`,ns/draw;T2 在本阶段定义为 `0x7f` 臂 = P2 的边界):
|
||||
|
||||
| | Espryt | Magma |
|
||||
|---|---|---|
|
||||
| **T1** = push(`0x1ff`) − pull(整个边界) | **+1048**(pull 的 +20.5%) | **+611**(+3.6%) |
|
||||
| **T2** = push(`0x7f`) − pull(P2 的边界) | +349 | +189 |
|
||||
| **T1 − T2 = P3a 自己加的** | **+699** | **+422** |
|
||||
| push(`0`) − pull(P1 残余填充,全部拉取) | +578 | +640 |
|
||||
| 位 63 − push(CSO 内容寻址净值) | −6(≈ 0) | +20(≈ 0) |
|
||||
| blend-toggle(ns/开关对) | +1209(+5.2%) | +925(+2.8%) |
|
||||
| pass switch | +160(≈ 0) | +566(≈ 0) |
|
||||
|
||||
读法(记录项,不设门):P3a 的 buffer/VAO 句柄路径在**每个 draw** 上多花 Espryt 699 / Magma 422 ns——这是 P3a 到目前为止最大的一笔边界成本,来源是每 draw 的 vertex-input 发射(`set_vertex_buffers` 的 `MGPVertexBuffer[]` 构造与 `ContentHash`、`bind_vertex_elements`)、applier 的记录写入,以及 Espryt 侧 `SyncToBackendFromApplier` 对记录的逐属性走查;Magma 没有句柄化的 VAO 消费者,它多出的 422 ns 全是 client 侧发射 + applier 记录(P7 之前的纯开销)。与 P2 相比,T2 本身从 P2 实测的 +322/+345 变为 +349/+189(同量级;Magma 的差别是 P2 数字来自同批次内的相对比较)。这个数字进优化阶段的清单:候选是 vertex-input 发射的 per-draw 抑制(同一 VAO/同一 buffer 集合的连续 draw 不重发;现在 `ContentHash` 命中时仍走一遍构造)与 applier 记录的就地更新。
|
||||
|
||||
`DriverBench` 的 T1/T2 口径与 §11–§12 相同,只是分档换了:**T1** = `ns_per_op(push, 默认位图 0x1ff) − ns_per_op(pull)`(整个边界的每 draw 代价),**T2** = `ns_per_op(push, MOBILEGL_PIPE_PUSH=0x7f) − ns_per_op(pull)`(P2 的边界本身),于是 **T1 − T2 恰好隔离出 P3a 加了什么、删了什么**;`mc_state_toggle` 作为非 P3a 的对照并列发布(一次 blend 开关既不碰 buffer 也不碰 VAO,它不该动)。`DriverBench` 走独立的 `build-bench`(门的构建配了 `-DMOBILEGL_BUILD_BENCHMARK=OFF`),并且必须经 `DRIVERBENCH_EGL_LIB` 显式 `dlopen` 一个 provider、**不得靠 `LD_LIBRARY_PATH` 遮挡**——glvnd 系统上一个裸 `libEGL.so.1` 会解析到 Mesa/llvmpipe,即在基准底下把 GPU 悄悄换成软件光栅器。**不钉上限、也不强制上限**(规则 (a));数字发布出来,让 P4a 自己决定要不要钉。
|
||||
|
||||
## 21. 决定与遗留判定
|
||||
|
||||
**整体 diff 终审(`5cb826b0..3e298c9a`)与其返工(`3e298c9a..c20e2f2b`,12 个提交)**:终审在四个包各自过审之后又抓出两个跨包接缝——(1) client 为每个 VAO 铸的 `VertexElementsCso` 槽只有 Espryt 的死亡通知会释放,Magma 不装 `StateObjectDeathOps`,默认掩码下每个 VAO 泄漏一个槽和约 1.3 KB 的 applier 记录,过 65536 个槽后 `create_vertex_elements` 永久 `Fatal{ProtocolCorruption}`;修法是 `~VertexArrayObject` 走后端无关的死亡路径(`MGPipeEmitVertexElementsDestroyAndFree`:先删记录、再发通知、最后释放槽,Espryt 的通知退化为可重入的第二条路径),泄漏测试在 DirectVulkan 上修前 48 轮 live 2→50、修后不增长;(2) `FlushPendingRangesNow` 位于只在 pull 构建里编译的 `#else`,G5 的文本哈希覆盖不到 push 构建真正跑的 `FlushPendingRangesFrom`——裁定如下。
|
||||
|
||||
> **ID-15(取代 ID-13 里"定义在任何 `#if` 之外"那句)。** `Managers.cpp` 把三层 flush 排水梯各带一份、每个预处理臂一份,任一构建只编译其中之一:`#if MOBILEGL_PIPE_PUSH` 下的 `FlushPendingRangesFrom` 是 **push** 构建跑的(legacy 调用点与 `Ops_H_Readback` 都到它),`#else` 里与 `5cb826b0` 逐字节相同的 `FlushPendingRangesNow` 是 **pull** 构建跑的;push 构建根本不编译 `FlushPendingRangesNow`。转发函数不可行:G5 的提取器不认预处理,一个转发的 `FlushPendingRangesNow` 会让同名出现两个定义、门直接退出 2(门跑不了)而不是比对。因此**两个名字都是 G5 行**:十一个函数,pull 梯对着 P3a 基线比、push 梯对着钉在 `3e298c9a` 的 sha 比;两条梯都不许漂,也不许彼此漂而门不响。
|
||||
|
||||
> **M-3 裁定:fp64 顶点数组收窄在 handle 臂上少做 D-N 十一处 `SyncGpuWrites` 里的一处,这是 P3a 的裁定而非疏漏。** D-N 的措辞是"不把这些站点*搬*离前端";这一臂不是搬走了它,而是**根本做不了**——`SyncFloat64AttributeAsFloat32ByHandle` 手里只有句柄,那次调用需要前端 `BufferObject`,而 server 没有回到前端对象的反向映射正是设计本身(`ARCHITECTURE.md` §4.2),不是设计的缺口。保住这个站点的两条路各会破坏 D-N 或 D-J 保护的东西:句柄→对象的映射正是拆分要去掉的东西,而 client 在每个 draw 上急切拉回字节是新行为和新成本。**影响面,写明以便日后核对**:默认掩码下,一个 64 位顶点数组、其*源* buffer 被 shader 写过且尚未回读,handle 臂收窄到的是旧字节、legacy 臂是新字节。就这一种:fp64 顶点数组、由 shader 写的 buffer 喂、中间没有显式回读。其它属性类型不走这条路,持久映射的源另行排除(memo 从不信任它)。**P8 把拉取搬到对象所在的 client 侧即关闭**;在那之前这是默认掩码下两臂唯一的行为差异。(同一段文字在 `MobileGL/MG_Backend/DirectGLES/Managers.cpp` 的站点上;带 adopted 源的 `DoublePrecisionScenario` 用例未加——它需要一条新工作负载而非既有用例的参数,留给 D 包。)
|
||||
|
||||
新的"看穿 GL API"接缝:`MG_IntegrationTest/Harness/PipeSlotPeek.{h,cpp}` 直接读 client 的槽分配器(泄漏测试的依据),与 `BackendCapsPeek` 并列。
|
||||
|
||||
**CI 独有的 teardown 崩溃(`DirectVulkan.Verify.CrossFrameBufferScenario.{Vertex,Index}CopyBufferSubData`,`double free or corruption`,本地 30 次不复现)**:ASan 定位为 `exit()` 时的静态析构顺序 UAF——命名空间作用域的 `gPipeInputs` 持有 `SharedPtr<VertexArrayObject>`,其析构链 `~VertexArrayObject → ~BufferObject → MGPipeEmitResourceDestroyAndFree → MGPipeSlotAllocator::FindByLifetimeId` 读的是已被 `~MGPipeSlotAllocator` 释放的哈希表(`MGPipeSlots()` 是首个 buffer 时才构造的 Meyers 单例,先于 `gPipeInputs` 析构)。全部 13 个用例 × 两个后端都走这条链,只有释放后的表恰好还能解析出句柄时 `Free()` 才写坏堆——CI 的分配器布局中招、本地没有;原生复现钥匙是 `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`(c20e2f2b 上恰好那两例失败,修后 0)。修法:`MGPipeSlots()`、`MGPipeResourceTrackerInstance()`、`g_applier` 改为永不析构的单例(堆上构造、退出时有意泄漏),一并覆盖 C-1 返工新增的第二条到达路径(`~VertexArrayObject` 的 `MGPipeEmitVertexElementsDestroyAndFree`)。 落地为三个提交:`d54ec57a`(三个单例永不析构)、`6515c8e6`(**从源头收口**:`gPipeInputs`、`g_snapshot`、`g_readScratch`、`probe` 这四个持有前端 `SharedPtr` 的静态 `PipeInputs` 改为退出时泄漏的存储——树里只有它们持有前端对象,于是没有任何前端析构函数会从 exit handler 里跑;这是 `Init.cpp`/`GlobalObjects.cpp` 已有的规则)、`fde5fda3`(其余四个 MGPipe 单例永不析构,作为纵深防御——C-1 返工把 `MGPipeVertexInputEmitterInstance()` 放上了 `~VertexArrayObject` 的死亡路径,ASan 在 6/6 代表用例上抓到 `VertexInputEmit.h` 的 heap-use-after-free)。证据:开着钥匙 `integration-verify` 844/844、push 臂 `integration-gpu` 966/966,ASan 10/10 干净;整条 verify 通道在 ASan 下只剩 `IntegerBorderColorScenario` 一个与本阶段无关的既有测试 bug(2×2 上传的 4 字节源配 `UNPACK_ALIGNMENT=4` 的栈越界读),另开任务。
|
||||
|
||||
**`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` 那 186 条过滤:P3a 跑,跑一次,在 push 下跑,不进比对器。** §14 里 P2 把这条推给 P3b,理由是"P2 在 buffer/回读方向什么也没改";**P3a 改的恰恰是这个过滤的题材**——它是 `FlushPendingRangesNow` 第一档的 kill switch(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:1071`、`:1331`),而 P3a 重写了那个函数的调用方。所以门里加了具名的一步 `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --test-dir build-push -L integration-gpu`,结果 **958/958**(§16 第 3 部分)。**放进 5–10× 的比对器里跑仍然推迟到 P3b**;这次是把拆开的两半都写下来,而不是让 CI 里那句注释再吊一轮。
|
||||
|
||||
**`g_uploadRing` 不被重置的不对称:原样保留,记为 `dev` 侧跟进。** `OnBackendContextDestroyed`(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:2481`)对 `g_uboRing` 与 `g_unpackRing` 调 `ResetRingForNewContext`(`:2492-2493`),**不对 `g_uploadRing` 调**;`RingAvailable`(`:3186`)在首次使用时按 `contextGeneration` 自愈,所以它是良性的。P3a **故意不在飞地顺手修**(`ROADMAP.md:98` 那条纪律:拆分不得借机修不相关的 `dev` 问题),把它作为 `dev` 侧跟进项留在这里。
|
||||
|
||||
**`FlushPendingRangesNow` 定义一次,句柄臂另有一条自己的档梯。** G5 的第十项与 G1 的空 resize 集之间有一处真冲突:就地重构那几个 helper 会 resize 五个 pull 符号(`FlushPendingRangesNow +14` 在内),G1 不允许。落地形状是:**`FlushPendingRangesNow` 只定义一次、对 `5cb826b0` 逐字节相同**(在 `#if MOBILEGL_PIPE_PUSH` 的 `#else` 臂里,`Managers.cpp:1316`,调用点 `:1723`、`:2906`),句柄臂另有一个 `FlushPendingRangesFrom(twin, hostBase, size)`(`:1051`,调用点 `:1721`、`:2044`、`:2766`、`:2904`)。**在 P3a 接受档梯在 push 构建里被复制一份**(与 respecify 核心已经用过的形状相同),代价是两条梯子会漂移;对冲是 `CrossFrameBufferScenario` 的十三条加 `StreamedArenaScenario` 的两条 recycle 用例,以及 §20 的 MC 26.3 p99。**它随 pull 臂在 P13 退役**(`ARCHITECTURE.md:367`)。共享模板加访问器接口的方案被否决:它同样 resize pull 符号(G1)。
|
||||
|
||||
**`MG_Test/Buffer/BufferTest.cpp` 的 fixture 只 scope 了一半的表。** push 构建下后端在 bring-up 同时装 `BufferBackendOps` 与 `MGPipeResourceOps`,而 `ScopedBackendOps` 只 scope 前者,于是 86 条 `BufferBackendOps` 分发用例里有 **26 条**被路由进了 pipe(症状是 `EnsureGpuResidentStorage()` 返回 `false`、mock 从没被调用过)。这是**合并缝**的典型形态——两个分支各自绿、合起来红:espryt 那边没有东西经 pipe 发射,client 那边没有东西注册表。集成者落了单 scope 的修法(fixture 现在像 `MG_Test/Pipe/ResourceEmitTest.cpp` 的 `ApplierGuard` scope applier 那样,保存 / 置空 / 恢复 pipe 表):**修完 86/86,整套单元 1619/1619**。它不削弱任何东西——那 86 条是 `BufferBackendOps` 的分发测试,pipe 侧的分发有 `ResourceEmitTest` 自己的覆盖。**跟进(不属于本阶段)**:给这个 fixture 一个 pipe 形的 mock,让同样的 86 条断言在句柄路径上再跑一遍。
|
||||
|
||||
**逐 dirty 位的触发率与每 draw payload 直方图:仍然未测。** §13 记的那两条在 P3a 也没有补上——没有任何一份 P3a 的结果文件报过位 5 / 9 / 10 收窄前后的 `FireCount` / `WalkCount`,也没有报过 24 桶的直方图(它仍然只在 teardown 的 JSON 里输出,而 trace app 从不到达那次 teardown,§5.1)。计划把这两项列为"虽然没人要也要发布"的项,**P3a 没有做到,如实记在这里**;补法与 §13 写的一样(给 `FormatWindowLine` 加一行,或从一个场景里经访问器读)。
|
||||
|
||||
**峰值 RSS(push vs pull,79 例 retrace):工具不报,所以没有数。** `~/w7/retrace_gate.py` 只有五个参数(`--tree --lib --out -j --only`),代码里没有任何 `rss` / `maxrss` / `getrusage` 引用。这个数原本是用来盯第七张 slot 表泄漏的——一个没人销毁的 buffer 会永远漏掉它的 twin,而这对每一个正确性门都不可见;**这一轮拿不到它**,要拿必须先给那个工具加测量。对冲仍在:`ResourceDestroy` 是从 `~BufferObject` **无条件**发射的、不是靠清扫,顺序(先发射、后 `MGPipeSlots().Free`)由 `HandleRecycleScenario` 的三个臂把关(§16、§17)。
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# MGPipe:MobileGL 前后端拆分
|
||||
|
||||
> 状态:**P0、P0.5、P1、P2 已落地**(`feat/disaggregated@738b289d`,基线 `dev@50fb1343`)。第 43 天 GO/NO-GO 判定为**继续**,下一步 P3a。见 `ROADMAP.md`。
|
||||
> 状态:**P0、P0.5、P1、P2、P3a 已落地**(`feat/disaggregated@fde5fda3`,基线 `dev@9eae9858`)。第 43 天 GO/NO-GO 判定为**继续**。P3a(handle wave 1:Espryt 的 buffer 与 VAO)已交付,**下一步 P4a**(handle wave 2:FBO / 纹理 / sampler / program 的身份与描述符)。见 `ROADMAP.md`。
|
||||
>
|
||||
> 性能纪律(2026-09-08 起):逐线程 CPU 与 tracker 绝对 ns **对着 pull 臂基线记录**,不再作阻塞门(push 比 pull 多约 10% 逐线程 CPU 已被接受),专门的优化阶段排在路线图推完之后。
|
||||
> 性能纪律(2026-09-08 起):逐线程 CPU 与 tracker 绝对 ns **对着 pull 臂基线记录**,不再作阻塞门(push 比 pull 多约 10% 逐线程 CPU 已被接受;该读数出自 -O0 APK,Release 基准线见 `MEASUREMENTS.md` §20),专门的优化阶段排在路线图推完之后。
|
||||
|
||||
## 是什么
|
||||
|
||||
@@ -31,7 +31,7 @@ MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`
|
||||
|---|---|
|
||||
| `ARCHITECTURE.md` | 已定稿的设计与架构:句柄与世代、调用目录、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程/EGL/平台、构建与纯度门、验证策略 |
|
||||
| `ROADMAP.md` | P0…P13 阶段表、两条跑道、GO/NO-GO 清单、再基线检查点、仍然开放的问题 |
|
||||
| `MEASUREMENTS.md` | P0 实测:spike A/B 结论、双设备四条 trace 的边界计数器基线、桌面数据点、语料事实、复现命令 |
|
||||
| `MEASUREMENTS.md` | 逐阶段实测:P0(spike A/B、双设备边界计数器基线、桌面数据点、语料事实)、P1(verify harness 门)、P2(五部分门、两机配对 A/B、DriverBench T1/T2、计数器)、P3a(门、接缝缺陷、Track H 普查、两机 A/B)与复现命令 |
|
||||
|
||||
代码地图(P0 已落地的部分):
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Executable → Regular
+30
-3
@@ -44,9 +44,16 @@ counted; `--require-all` turns every SKIP into a failure. That is what lets this
|
||||
land BEFORE the two headers do, and what stops an all-SKIP run from passing for free
|
||||
once they exist - the integrator flips `--require-all` on as the ratchet.
|
||||
|
||||
TWO WAYS THIS GATE USED TO GO GREEN WITHOUT LOOKING AT ANYTHING, both closed:
|
||||
an explicit `--compiler` is resolved in EVERY mode (the default `--mode text` never runs
|
||||
one, so a gate line naming a binary this image does not have used to pass), and
|
||||
`--expect-probes N` pins how many probes actually ran, because an exit code cannot tell
|
||||
four probes from none. The campaign gate spells `--compiler clang++`: there is no
|
||||
`clang++-20` in the WSL image, and asking for one is now a stop rather than a silent skip.
|
||||
|
||||
python3 scripts/check_include_closure.py --mode text --self-test
|
||||
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test
|
||||
python3 scripts/check_include_closure.py --mode both --self-test --require-all
|
||||
python3 scripts/check_include_closure.py --mode both --compiler clang++ --self-test
|
||||
python3 scripts/check_include_closure.py --mode both --self-test --require-all --expect-probes 4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -581,6 +588,9 @@ def main():
|
||||
help="run the negative controls and the parser checks (always on in CI)")
|
||||
parser.add_argument("--require-all", action="store_true",
|
||||
help="a SKIP (header not present yet) is a failure")
|
||||
parser.add_argument("--expect-probes", type=int, default=None,
|
||||
help="fail unless exactly this many probes ran; a gate that only checks "
|
||||
"the exit code cannot tell 4 probes from 0")
|
||||
parser.add_argument("--json", default=None, help="write the machine-readable result here")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -592,6 +602,17 @@ def main():
|
||||
return 1
|
||||
|
||||
modes = ["text", "clang"] if args.mode == "both" else [args.mode]
|
||||
|
||||
# AN EXPLICIT --compiler IS VALIDATED IN EVERY MODE, not only in the ones that would run
|
||||
# it. `--mode text` is the default and never reaches pick_compiler, so a gate line that
|
||||
# spelled a compiler this image does not have (clang++-20, say) used to run the text half,
|
||||
# print nothing about it and exit 0 - a green nobody earned, which is exactly what
|
||||
# ROADMAP.md:7 forbids. Asking for a compiler that is not there is an operator error
|
||||
# whatever the mode, and it stops the run here rather than half-running it.
|
||||
resolved_compiler = pick_compiler(args.compiler) if args.compiler else None
|
||||
if args.compiler and "clang" not in modes:
|
||||
say("compiler {} resolved but mode `{}` does not use it".format(resolved_compiler, args.mode))
|
||||
|
||||
if "clang" in modes:
|
||||
check_clang_prereqs()
|
||||
|
||||
@@ -606,7 +627,7 @@ def main():
|
||||
"compiler_label": "-",
|
||||
}
|
||||
if "clang" in modes:
|
||||
compiler = pick_compiler(args.compiler)
|
||||
compiler = resolved_compiler or pick_compiler(None)
|
||||
context["compiler"] = compiler
|
||||
context["compiler_label"] = compiler
|
||||
if args.compile_commands:
|
||||
@@ -676,6 +697,12 @@ def main():
|
||||
if not self_test_ok:
|
||||
problems += 1
|
||||
|
||||
# A COUNT, NOT ONLY AN EXIT CODE. Zero probes is a green a run with nothing in it earns,
|
||||
# and a --probe typo or a manifest edit is exactly how a gate stops looking at anything.
|
||||
if args.expect_probes is not None and len(selected) != args.expect_probes:
|
||||
problems += 1
|
||||
error("expected {} probe(s), ran {}".format(args.expect_probes, len(selected)))
|
||||
|
||||
say("{} probes, {} skipped, {} problem(s)".format(len(selected), skipped, problems))
|
||||
|
||||
if args.json:
|
||||
|
||||
@@ -71,8 +71,21 @@ SCAN_ROOTS = (os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "GLImpl"),
|
||||
os.path.join(REPO_ROOT, "MobileGL", "MG_State", "GLState"))
|
||||
|
||||
# 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",
|
||||
"Begin", "End")
|
||||
"Begin", "End", "Use", "Bind")
|
||||
|
||||
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
|
||||
@@ -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")
|
||||
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
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user