mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Pipe: per-program global-constants key and frontend-written spans replace the client shadow diff.
This commit is contained in:
@@ -1572,7 +1572,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// version.
|
||||
if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return;
|
||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||
programObject.MarkUBOContentDirty();
|
||||
programObject.MarkUBOContentDirtyRange(static_cast<Uint>(offset + byteOffsetInsideUniform),
|
||||
static_cast<Uint>(writeSize));
|
||||
} else {
|
||||
const auto& ttype = programObject.GetUniformTypeFacts(location);
|
||||
if (!ttype.isTexture && !ttype.isImage) return;
|
||||
|
||||
@@ -210,34 +210,33 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
Uint64 bytes = 0;
|
||||
const MGPipeHandle cso = AcquireShaderCso(*program, bytes);
|
||||
// (ShaderCso, Version) IS the key, so the latch is the key: an unchanged pair means
|
||||
// the server already holds these bytes and re-sending them would move the record's
|
||||
// serial for nothing.
|
||||
if (cso == m_constantsCso && version == m_constantsVersion) return bytes;
|
||||
// THE KEY IS PER HANDLE, because the block is: the applier keeps one image per shader
|
||||
// CSO record, so a program drawn again after another one ran in between still holds
|
||||
// what it was last sent. An unchanged (ShaderCso, Version) pair means the server
|
||||
// already holds these bytes and re-sending them would move the record's serial for
|
||||
// nothing. The latch lives beside the record latch, so a recycled slot or a death
|
||||
// retires it with that one; a re-issued create and Reset() retire it explicitly.
|
||||
Latch* latch = MGPipeHandleIsNull(cso) ? nullptr : &LatchFor(cso);
|
||||
const Bool held = latch != nullptr && latch->ConstantsLive && latch->ConstantsEpoch == m_constantsEpoch;
|
||||
if (held && latch->ConstantsVersion == version) return bytes;
|
||||
// Taken on every send, span or whole, so the range always describes the bytes
|
||||
// written since the image the server holds for this program.
|
||||
const auto dirty = program->TakeUBODirtyRange();
|
||||
|
||||
m_lastConstants = MGPGlobalConstants{};
|
||||
m_lastConstants.ShaderCso = cso;
|
||||
m_lastConstants.Version = version;
|
||||
// P14: the server holds this program's previous image while the latch names it (every
|
||||
// path that could drop or replace the applier's block clears the latch), so only the
|
||||
// span that moved goes out - when it is small enough to be worth it.
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
// P14: the server holds this program's previous image while its latch is held (every
|
||||
// path that could drop or replace the applier's block retires the latch), so only the
|
||||
// span the frontend wrote since then goes out - when it is small enough to be worth it.
|
||||
const Uint8* image = static_cast<const Uint8*>(program->GetUBOData());
|
||||
if (cso == m_constantsCso && m_constantsShadow.size() == size && size <= 0xffffu) {
|
||||
// Word-wise from both ends, then byte-wise inside the first differing word.
|
||||
const Uint8* shadow = m_constantsShadow.data();
|
||||
const auto word = [](const Uint8* at) {
|
||||
Uint64 value;
|
||||
std::memcpy(&value, at, sizeof(value));
|
||||
return value;
|
||||
};
|
||||
Uint32 first = 0;
|
||||
while (first + 8 <= size && word(image + first) == word(shadow + first)) first += 8;
|
||||
while (first < size && image[first] == shadow[first]) ++first;
|
||||
Uint32 last = size;
|
||||
while (last >= first + 8 && word(image + last - 8) == word(shadow + last - 8)) last -= 8;
|
||||
while (last > first && image[last - 1] == shadow[last - 1]) --last;
|
||||
// Nothing moved but the version: one byte carries the new version.
|
||||
if (first == size) {
|
||||
if (held && latch->ConstantsSize == size && size <= 0xffffu) {
|
||||
Uint32 first = std::min<Uint32>(dirty.Begin, size);
|
||||
Uint32 last = std::min<Uint32>(dirty.End, size);
|
||||
// Nothing written but the version moved: one byte carries the new version.
|
||||
if (first >= last) {
|
||||
first = 0;
|
||||
last = 1;
|
||||
}
|
||||
@@ -248,8 +247,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image + first);
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, image + first, static_cast<Uint64>(length));
|
||||
std::memcpy(m_constantsShadow.data() + first, image + first, length);
|
||||
m_constantsVersion = version;
|
||||
latch->ConstantsVersion = version;
|
||||
++m_constantSets;
|
||||
++m_constantSpans;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
@@ -263,17 +261,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// pointer. Offset carries the staging address for diagnostics only; nothing reads
|
||||
// it as a length.
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image);
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
// `size` is GetUBOSize(), and it is passed because the record declares 0 - the
|
||||
// monolith convention (the bytes ride beside the record) that CONTRACT-P5 table 1
|
||||
// rule A cannot keep under split. It is the row's largest and least bounded blob,
|
||||
// per program per frame, so it is also the one R-10's max-record counter watches.
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, program->GetUBOData(),
|
||||
static_cast<Uint64>(size));
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
m_constantsShadow.assign(image, image + size);
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, image, static_cast<Uint64>(size));
|
||||
if (latch != nullptr) {
|
||||
latch->ConstantsLive = true;
|
||||
latch->ConstantsEpoch = m_constantsEpoch;
|
||||
latch->ConstantsVersion = version;
|
||||
latch->ConstantsSize = size;
|
||||
}
|
||||
++m_constantSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
|
||||
@@ -508,7 +508,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// not happen to move the content version, of which a recycled slot is one.
|
||||
// Invalidated rather than re-emitted here, because this function has no business
|
||||
// deciding when the constants go out: the next EmitGlobalConstants sees an
|
||||
// unlatched key and sends them.
|
||||
// unlatched key and sends them whole.
|
||||
latch.ConstantsLive = false;
|
||||
if (m_constantsCso == handle) {
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
@@ -606,12 +607,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// The RECORD half stays, and that is the rule rather than an oversight: the applier
|
||||
// keeps its shader-CSO records across a make-current because a program lives in a share
|
||||
// group, and re-publishing one would move its Serial for nothing. The global-constants
|
||||
// key goes with the working state because its record's bytes are per (Cso, Version) and
|
||||
// a fresh server has not been told them.
|
||||
// keys go with the working state because its record's bytes are per (Cso, Version) and
|
||||
// a fresh server has not been told them: every per-handle key retires at once by epoch.
|
||||
void Reset() {
|
||||
m_boundCso = kMGPipeNullHandle;
|
||||
m_drawCso = kMGPipeNullHandle;
|
||||
m_dispatchCso = kMGPipeNullHandle;
|
||||
++m_constantsEpoch;
|
||||
m_constantsCso = kMGPipeNullHandle;
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
// The composite memo's freshness goes with them - and only its freshness. Its
|
||||
@@ -629,9 +631,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// ---- what a unit case reads ----
|
||||
const MGPProgramDesc& LastProgramDesc() const { return m_lastDesc; }
|
||||
const MGPGlobalConstants& LastGlobalConstants() const { return m_lastConstants; }
|
||||
// THE (Cso, Version) KEY set_global_constants is suppressed against. Exposed so a case
|
||||
// can pin that a re-issued create_shader_state invalidates it - the applier clears the
|
||||
// block on the re-issue (wire W6), so a latch that survived it would never re-send.
|
||||
// THE LAST (Cso, Version) set_global_constants sent. Exposed so a case can pin that a
|
||||
// re-issued create_shader_state invalidates it - the applier clears the block on the
|
||||
// re-issue (wire W6), so a latch that survived it would never re-send. The per-handle
|
||||
// key the suppression reads retires at the same three places.
|
||||
MGPipeHandle GlobalConstantsCso() const { return m_constantsCso; }
|
||||
Uint32 GlobalConstantsVersion() const { return m_constantsVersion; }
|
||||
// D-J3's counted refusal: programs whose linked snapshot carried more modules than
|
||||
@@ -645,6 +648,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 DrawProgramSetCount() const { return m_drawSets; }
|
||||
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
|
||||
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
|
||||
Uint64 GlobalConstantsSpanCount() const { return m_constantSpans; }
|
||||
// P5e (pg). The record a case reads back, and the two counters D-J3's rule asks for:
|
||||
// how many set_program_bindings went out, and how many programs were REFUSED because
|
||||
// one of their three sets is larger than the record can name. A refusal leaves the
|
||||
@@ -691,6 +695,13 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint32 BindingsGen = 0;
|
||||
Uint32 BindingsBackendStateVersion = 0;
|
||||
Uint32 BindingsBlockBindingVersion = 0;
|
||||
// set_global_constants' key, here for the bindings' reason: what the applier's record
|
||||
// at this handle holds, as (Version, Size), valid only while ConstantsLive and the
|
||||
// epoch is the emitter's current one.
|
||||
Bool ConstantsLive = false;
|
||||
Uint32 ConstantsVersion = 0;
|
||||
Uint32 ConstantsSize = 0;
|
||||
Uint64 ConstantsEpoch = 0;
|
||||
};
|
||||
|
||||
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
|
||||
@@ -745,11 +756,12 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeHandle m_boundCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_drawCso = kMGPipeNullHandle;
|
||||
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
|
||||
// The last (Cso, Version) set_global_constants sent, for the unit cases; the suppression
|
||||
// itself reads the per-handle Latch keys.
|
||||
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
|
||||
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
// The image last sent for m_constantsCso (whole or patched by spans): what the server
|
||||
// holds while the latch names it.
|
||||
Vector<Uint8> m_constantsShadow;
|
||||
// Moved by Reset(): a Latch's constants key holds only at the epoch it was taken in.
|
||||
Uint64 m_constantsEpoch = 1;
|
||||
Uint64 m_constantSpans = 0;
|
||||
|
||||
Uint64 m_creates = 0;
|
||||
|
||||
@@ -611,7 +611,7 @@ namespace MobileGL::MG_State {
|
||||
continue;
|
||||
}
|
||||
Memcpy(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span);
|
||||
destination.MarkUBOContentDirty();
|
||||
destination.MarkUBOContentDirtyRange(destinationOffset, static_cast<Uint>(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,8 +744,31 @@ namespace MobileGL::MG_State::GLState {
|
||||
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
||||
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
||||
Uint32 GetUBOContentVersion() const { return m_uboContentVersion; }
|
||||
// A write whose extent is not known marks the whole shadow.
|
||||
void MarkUBOContentDirty() const {
|
||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||
m_uboDirtyBegin = 0;
|
||||
m_uboDirtyEnd = ~Uint32{0};
|
||||
}
|
||||
void MarkUBOContentDirtyRange(Uint offset, Uint size) const {
|
||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||
m_uboDirtyBegin = std::min<Uint32>(m_uboDirtyBegin, offset);
|
||||
m_uboDirtyEnd = std::max<Uint32>(m_uboDirtyEnd, offset + size);
|
||||
}
|
||||
// The byte range of the shadow written since the last take, cleared by taking it. The
|
||||
// pipe's program emitter is its one taker: it sends this range as a span to a server
|
||||
// that already holds the image as of its previous take. End is NOT clamped (a whole-
|
||||
// shadow mark reads ~0u), and Begin >= End means nothing was written. A new object
|
||||
// starts as wholly written.
|
||||
struct UBODirtyRange {
|
||||
Uint32 Begin;
|
||||
Uint32 End;
|
||||
};
|
||||
UBODirtyRange TakeUBODirtyRange() const {
|
||||
const UBODirtyRange range{m_uboDirtyBegin, m_uboDirtyEnd};
|
||||
m_uboDirtyBegin = ~Uint32{0};
|
||||
m_uboDirtyEnd = 0;
|
||||
return range;
|
||||
}
|
||||
|
||||
// ---- the reserved gl_NumSamples stand-in (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) ----
|
||||
@@ -771,7 +794,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
Memcpy(¤t, slot, sizeof(Int));
|
||||
if (current == samples) return true;
|
||||
Memcpy(slot, &samples, sizeof(Int));
|
||||
MarkUBOContentDirty();
|
||||
MarkUBOContentDirtyRange(offset, static_cast<Uint>(sizeof(Int)));
|
||||
return true;
|
||||
}
|
||||
// ---- glUniform* inside the phase-A -> phase-B window ----
|
||||
@@ -1440,6 +1463,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
mutable SizeT m_backendHashMemoNextSlot = 0;
|
||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||
mutable Uint32 m_uboContentVersion = 0;
|
||||
mutable Uint32 m_uboDirtyBegin = 0;
|
||||
mutable Uint32 m_uboDirtyEnd = ~Uint32{0};
|
||||
mutable Uint32 m_linkVersion = 0;
|
||||
|
||||
// ---- Link OUTPUT ----
|
||||
|
||||
@@ -735,6 +735,116 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
||||
}
|
||||
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
|
||||
}
|
||||
// A default uniform block wide enough that one vec4 is less than half of it, so a write to
|
||||
// one uniform crosses as a span rather than as the whole block.
|
||||
const char* kVsWide = R"(#version 430 core
|
||||
uniform vec4 u_a;
|
||||
uniform vec4 u_b;
|
||||
uniform vec4 u_c;
|
||||
uniform vec4 u_d;
|
||||
void main() { gl_Position = u_a + u_b + u_c + u_d; }
|
||||
)";
|
||||
|
||||
GLuint MakeWideProgram() {
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::AttachShader(program, MakeShader(GL_VERTEX_SHADER, kVsWide));
|
||||
GL::AttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFs));
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the wide program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
// Binds `name` and drives both program emissions to their fixed point (the first
|
||||
// EmitGlobalConstants reads the version before GetUBOSize() joins phase B, which bumps it).
|
||||
void DrawWith(GLuint name) {
|
||||
GL::UseProgram(name);
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
for (int i = 0; i < 8 && Emitter().EmitGlobalConstants(Ctx()) > 0u; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// THE set_global_constants KEY IS PER HANDLE, because the applier keeps one block per shader
|
||||
// CSO record: a program drawn again after another one ran in between, with no uniform of its
|
||||
// own written, still holds on the server what it was last sent. A single last-sent key re-sent
|
||||
// the whole block on every such switch back.
|
||||
TEST(ProgramEmit, ASwitchBackToAnUnchangedProgramSendsNoGlobalConstants) {
|
||||
EmitterScope scope;
|
||||
const GLuint a = MakeWideProgram();
|
||||
const GLuint b = MakeWideProgram();
|
||||
DrawWith(a);
|
||||
DrawWith(b);
|
||||
const Uint64 setsBefore = Emitter().GlobalConstantsSetCount();
|
||||
ASSERT_GT(setsBefore, 0u) << "both programs declare uniforms, so blocks must have gone out";
|
||||
|
||||
GL::UseProgram(a);
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(Emitter().EmitGlobalConstants(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().GlobalConstantsSetCount(), setsBefore)
|
||||
<< "the server already holds program a's block at this version";
|
||||
}
|
||||
|
||||
// The span is the range the frontend WROTE since the image the server holds for that program,
|
||||
// and it survives a switch to another program and back.
|
||||
TEST(ProgramEmit, AUniformWriteAfterASwitchCrossesAsTheSpanItWrote) {
|
||||
EmitterScope scope;
|
||||
const GLuint a = MakeWideProgram();
|
||||
const GLuint b = MakeWideProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(a);
|
||||
ASSERT_TRUE(program);
|
||||
DrawWith(a);
|
||||
DrawWith(b);
|
||||
|
||||
GL::UseProgram(a);
|
||||
const GLint location = GL::GetUniformLocation(a, "u_c");
|
||||
ASSERT_GE(location, 0);
|
||||
GL::Uniform4f(location, 1.0f, 2.0f, 3.0f, 4.0f);
|
||||
const Uint offset = program->GetUniformOffset(static_cast<Uint>(location));
|
||||
ASSERT_NE(offset, ProgramObject::kInvalidUniformOffset);
|
||||
ASSERT_GE(program->GetUBOSize(), 32u) << "the block must be wide enough for a span";
|
||||
|
||||
const Uint64 spansBefore = Emitter().GlobalConstantsSpanCount();
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_GT(Emitter().EmitGlobalConstants(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().GlobalConstantsSpanCount(), spansBefore + 1u);
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Range, (static_cast<Uint32>(offset) << 16) | 16u)
|
||||
<< "the record must carry exactly the vec4 that was written";
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Blob.Offset,
|
||||
reinterpret_cast<Uint64>(static_cast<const Uint8*>(program->GetUBOData()) + offset));
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Version, program->GetUBOContentVersion());
|
||||
}
|
||||
|
||||
// A write whose extent the frontend does not know marks the whole block, and the next send is
|
||||
// then the whole block rather than a span.
|
||||
TEST(ProgramEmit, AnUnrangedContentMarkSendsTheWholeBlock) {
|
||||
EmitterScope scope;
|
||||
const GLuint a = MakeWideProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(a);
|
||||
ASSERT_TRUE(program);
|
||||
DrawWith(a);
|
||||
|
||||
program->MarkUBOContentDirty();
|
||||
EXPECT_GT(Emitter().EmitGlobalConstants(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Range, 0u);
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Version, program->GetUBOContentVersion());
|
||||
}
|
||||
|
||||
// Reset() is a fresh server: every program's key retires at once, so the next send of an
|
||||
// UNCHANGED program is its whole block.
|
||||
TEST(ProgramEmit, AResetRetiresEveryProgramsGlobalConstantsKey) {
|
||||
EmitterScope scope;
|
||||
const GLuint a = MakeWideProgram();
|
||||
DrawWith(a);
|
||||
const Uint64 setsBefore = Emitter().GlobalConstantsSetCount();
|
||||
|
||||
Emitter().Reset();
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_GT(Emitter().EmitGlobalConstants(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().GlobalConstantsSetCount(), setsBefore + 1u);
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Range, 0u);
|
||||
}
|
||||
|
||||
// FINAL REVIEW C-2: the death helper forwards to this emitter before the slot is freed, so
|
||||
// a dead program's handle no longer reads as published in the record memo between the death
|
||||
// and the recycle (the memo's own Gen test covers only the recycle).
|
||||
|
||||
@@ -124,6 +124,20 @@ The plan and its measurements are in [PLAN-P15.md](PLAN-P15.md); the audit is in
|
||||
the next lead is the GPU side (render-stage trace of Espryt vs MobileGlues passes), not driver calls.
|
||||
4. Small CPU batch: ValidateForVerb residue, Tracker::Update (0.018, mostly inherent),
|
||||
EmitGlobalConstants (0.026). Expected ≤0.03 ms in total.
|
||||
- DONE on host, branch `cpubatch` (2026-10-09). **Device measurement pending.** Line-level
|
||||
profile of fbm2 h-r1..r3 (both backends, p15h syms) and m2ab e-DirectVulkan r1..r3 (LTO).
|
||||
- EmitGlobalConstants, cut: the (ShaderCso, Version) key is per handle (the applier keeps one
|
||||
block per record), so switching back to a program whose uniforms did not move sends nothing,
|
||||
and a moved one sends a span against its own held image instead of the whole block. The span
|
||||
is the range the frontend wrote (ProgramObject::MarkUBOContentDirtyRange / TakeUBODirtyRange),
|
||||
so the client shadow and its two-ended diff scan are gone. The scan plus shadow copies were
|
||||
~0.0065 ms/frame; with the skipped switch-back sends, expect ~0.007-0.010 ms/frame.
|
||||
- Tracker::Update: inherent, skipped. ~30 ns per verb over ~25 frontend counters; the only real
|
||||
cut is the epoch skip (PLAN stage 1), which is not a small item.
|
||||
- ValidateForVerb residue: skipped. The largest named piece (~0.008 ms/frame) is the
|
||||
MOBILEGL_PIPE_POISON stamp walk, armed by MOBILEGL_BUILD_DISAGGREGATED. The bench libraries
|
||||
are the plugin build (split ON); the FCL-embedded build (split OFF, INFO) compiles it out.
|
||||
The rest (prologue, singleton guards, gates) is spread below 0.002 each.
|
||||
5. API 29 build plus its A/B (`wip/api29-switch`).
|
||||
6. Smaller follow-ups:
|
||||
- DONE 2026-10-09: `bench_session.sh stop` now runs `pin_clocks.sh restore` first (verified over a
|
||||
|
||||
Reference in New Issue
Block a user