mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feat] (Pipe): compute the per-verb dirty mask at the validate point and count how often each bit fires
- MGPipeFillForVerb becomes MGPipeValidateForVerb and MGP_FILL expands to the new name. The
macro spelling, the 83 call sites and the verb enum do not change: the dispatch is
kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor* entry
points with one call site per verb instead of nine (P2 brief D1, against ARCHITECTURE.md's
eight - FillPoints.def argues in its own comment for splitting kProgramOp out, and the landed
table is what runs).
- MG_Impl/Pipe/Tracker.h: MGPipeDirty's 18 bits, the widened Uint16 shutters, the per-verb walk
and the per-bit-per-verb-class fire tallies. The widening happens in the TRACKER and MG_State
is not changed for it; a wrap costs one extra re-push and never a missed one.
- NOTHING IS EMITTED YET. The mask is computed, latched and counted, and the full P1 residual
fill runs after it unchanged. That is the point of this step: it says the walk is
semantically free before any field stops being pulled, so a regression in the next commit
cannot be blamed on the walk.
- Every shutter over-fires on purpose. Bits 2 and 3 are BYTE compares, not value compares,
because a NaN patch level is a legal glPatchParameterfv value and has to equal itself; bits
5..17 are composed with a mixing hash, which can in principle collide, and that is stated in
the file and is acceptable only because nothing consumes those bits in P2.
- The shutter for bits 6..8 reads the current program's version counters WITHOUT
GetProgramForDraw, so the walk never joins a pending link to answer "did the shader move".
- Header-only rather than Tracker.{h,cpp}: the root CMakeLists.txt that would have to name a
new .cpp belongs to package A and is frozen behind the p2/contract tag. One translation unit
in the library includes it, so inline costs nothing, and splitting it out is one list(APPEND)
line whenever the ownership allows.
- MG_Test/ScopedPipeVerb.h and MG_Test/Pipe/PipeInputsTest.cpp follow the rename. Five comment
references to the old name live in package A's files (MGPipe.h, the two generated .inc, and
gen_pipe.py) and are deliberately left for their owner.
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <Config.h>
|
||||
|
||||
@@ -595,8 +596,8 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
}
|
||||
|
||||
// ---- the filler ----
|
||||
void MGPipeFillForVerb(MGPipeVerb verb) {
|
||||
// ---- the validate point (P2 brief D1) ----
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
ParsePoisonOmissionKnob();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
@@ -620,7 +621,17 @@ namespace MobileGL::MG_Pipe {
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(inputs, ctx);
|
||||
if (ctx == nullptr) return;
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
|
||||
const MGPipeVerbClass verbClass = kMGPipeVerbClass[static_cast<SizeT>(verb)];
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)];
|
||||
|
||||
// ---- step 2: the dirty walk (P2 brief D1, D4) ----
|
||||
// The mask is computed, latched and counted here and nothing is emitted from it
|
||||
// yet: this commit is the safety net that says the walk is semantically free
|
||||
// before any field stops being pulled. The emission steps land on top of it.
|
||||
const Uint32 dirty = MGPipeTrackerInstance().Update(*ctx, verbClass);
|
||||
(void)dirty;
|
||||
|
||||
// ---- step 4: the residual fill ----
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
|
||||
@@ -18,11 +18,21 @@
|
||||
namespace MobileGL::MG_Pipe {
|
||||
struct PipeInputs;
|
||||
|
||||
// PipeFill.cpp. Bumps the per-verb serial, records the verb and the context identity,
|
||||
// and copies every field in the verb class's may-read mask (kMGPipeClassFieldMask) out
|
||||
// of the live GLContext, stamping each with the new serial. In a verify build it then
|
||||
// runs the entry compare against a second snapshot (P1 brief D8).
|
||||
void MGPipeFillForVerb(MGPipeVerb verb);
|
||||
// PipeFill.cpp. THE VALIDATE POINT (ARCHITECTURE.md 5.1, P2 brief D1). In order:
|
||||
// 1. bump the per-verb serial, record the verb and the context identity;
|
||||
// 2. run the tracker's DIRTY WALK for this verb's class (MG_Impl/Pipe/Tracker.h);
|
||||
// 3. EMIT, for each set dirty bit whose subsystem bit is on in the runtime
|
||||
// MOBILEGL_PIPE_PUSH bitmask, the P2 call that carries it;
|
||||
// 4. run the P1 residual fill for every field an emitted call did NOT supply,
|
||||
// stamping each with the new serial exactly as before;
|
||||
// 5. in a verify build, the entry compare against a second snapshot (P1 brief D8) -
|
||||
// which stops being a tautology the moment step 3 supplies a field step 4 skips.
|
||||
//
|
||||
// It was MGPipeFillForVerb through P1, when steps 2 and 3 did not exist. The macro
|
||||
// spelling, the 83 call sites and the verb enum are unchanged: the dispatch is
|
||||
// kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor*
|
||||
// entry points with one call site per verb instead of nine.
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb);
|
||||
|
||||
// Ends the verb in flight without starting another: bumps the serial, so every field the
|
||||
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
|
||||
@@ -49,7 +59,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
|
||||
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeValidateForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
|
||||
#else
|
||||
#define MGP_FILL(Verb) ((void)0)
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/Tracker.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
|
||||
//
|
||||
// WHERE IT RUNS. Not above MGP_FILL and not in the GL setter: MGPipeValidateForVerb, the
|
||||
// one statement MGP_FILL already expands to before every gBackendFunctionsTable.GL call
|
||||
// (PipeFill.h). Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), so a
|
||||
// setter that pushed would push twice per batch for a state the batch may not even read;
|
||||
// the validate point coalesces the whole bracket into the two draws that observe it
|
||||
// (ARCHITECTURE.md 5.1).
|
||||
//
|
||||
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
|
||||
// computed by comparing a shutter against what the tracker last pushed. P2 EMITS for bits
|
||||
// 0..4 only (the value-class ones); bits 5..17 are computed, latched and counted so the
|
||||
// per-bit fire rate is a measurement rather than a plan, and their fields keep going
|
||||
// through the residual fill until P3a/P3b/P4a/P4b.
|
||||
//
|
||||
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
|
||||
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
|
||||
// dangerous direction precisely because the P1 verify comparator cannot see it for
|
||||
// object-class state (it compares those by identity only). So each shutter below is
|
||||
// deliberately coarser than the state it guards - five bits share one buffer aggregate,
|
||||
// the framebuffer bit fires on any attachment write anywhere - and the narrowing is P3's
|
||||
// work, paid for with the fire rates this file publishes.
|
||||
//
|
||||
// NO TIMER LIVES HERE. ROADMAP.md forbids committing hot-path instrumentation; the
|
||||
// absolute ns/draw comes from DriverBench, which times whole frames from outside the
|
||||
// library (P2 brief D17). The only counting is the per-bit fire tally, behind
|
||||
// PipeStats::Enabled() like every other counting site in the tree.
|
||||
//
|
||||
// HEADER-ONLY, and that is an ownership decision rather than a design one: the P2 brief
|
||||
// asks for Tracker.{h,cpp}, but the root CMakeLists.txt that would have to name a new .cpp
|
||||
// belongs to package A and is frozen behind the p2/contract tag. Everything here is
|
||||
// included by exactly one translation unit in the library (MG_Impl/Pipe/PipeFill.cpp) plus
|
||||
// the unit tests, so inline costs nothing. Splitting it back out is one list(APPEND) line.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// One bit per row of the ARCHITECTURE.md 5.2 table, hand-written rather than generated:
|
||||
// the list is design, not derived data, and the generator has nothing to derive it from.
|
||||
enum class MGPipeDirty : Uint32 {
|
||||
// ---- value class: P2 emits for these five ----
|
||||
NewRenderState = 0, // RenderState::m_version -> set_dynamic_state
|
||||
NewPipelineState, // RenderState::m_pipelineStateVersion -> create/bind_render_state
|
||||
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
|
||||
NewPatchState, // the patch trio, NaN legal -> set_patch_state
|
||||
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
|
||||
// ---- value class: computed and counted, emitted from P3a on ----
|
||||
NewVertexElements, // the bound VAO's attribute configuration
|
||||
NewShader, // the current program's link version
|
||||
NewShaderBindings, // image units, block bindings, uniform write set
|
||||
NewGlobalConstants, // the default-uniform-block image
|
||||
// ---- object class: computed and counted, emitted from P3b/P4b on ----
|
||||
NewVertexBuffers,
|
||||
NewIndexBuffer,
|
||||
NewFramebuffer,
|
||||
NewSamplerViews,
|
||||
NewSamplers,
|
||||
NewShaderImages,
|
||||
NewConstBuffers,
|
||||
NewShaderBuffers,
|
||||
NewSoTargets,
|
||||
Count,
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeDirtyCount = static_cast<SizeT>(MGPipeDirty::Count);
|
||||
static_assert(kMGPipeDirtyCount <= 32, "the dirty mask is a Uint32");
|
||||
|
||||
inline constexpr Uint32 MGPipeDirtyBit(MGPipeDirty bit) {
|
||||
return Uint32{1} << static_cast<Uint32>(bit);
|
||||
}
|
||||
|
||||
// The five P2 emits for.
|
||||
inline constexpr Uint32 kMGPipeDirtyEmittedAtP2 =
|
||||
MGPipeDirtyBit(MGPipeDirty::NewRenderState) | MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewPixelPack) | MGPipeDirtyBit(MGPipeDirty::NewPatchState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults);
|
||||
|
||||
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
|
||||
"NEW_RENDER_STATE",
|
||||
"NEW_PIPELINE_STATE",
|
||||
"NEW_PIXEL_PACK",
|
||||
"NEW_PATCH_STATE",
|
||||
"NEW_VERTEX_ATTRIB_DEFAULTS",
|
||||
"NEW_VERTEX_ELEMENTS",
|
||||
"NEW_SHADER",
|
||||
"NEW_SHADER_BINDINGS",
|
||||
"NEW_GLOBAL_CONSTANTS",
|
||||
"NEW_VERTEX_BUFFERS",
|
||||
"NEW_INDEX_BUFFER",
|
||||
"NEW_FRAMEBUFFER",
|
||||
"NEW_SAMPLER_VIEWS",
|
||||
"NEW_SAMPLERS",
|
||||
"NEW_SHADER_IMAGES",
|
||||
"NEW_CONST_BUFFERS",
|
||||
"NEW_SHADER_BUFFERS",
|
||||
"NEW_SO_TARGETS",
|
||||
};
|
||||
|
||||
// Which runtime MOBILEGL_PIPE_PUSH subsystem bit gates a dirty bit's emission. Zero for
|
||||
// a bit P2 does not emit, which is what makes "the bitmask is a true per-subsystem A/B"
|
||||
// literally true rather than approximately.
|
||||
inline constexpr Uint64 MGPipeSubsystemForDirty(MGPipeDirty bit) {
|
||||
switch (bit) {
|
||||
case MGPipeDirty::NewRenderState:
|
||||
case MGPipeDirty::NewPipelineState:
|
||||
return kMGPipeSubsystemRenderState;
|
||||
case MGPipeDirty::NewPixelPack:
|
||||
return kMGPipeSubsystemPixelPack;
|
||||
case MGPipeDirty::NewPatchState:
|
||||
return kMGPipeSubsystemPatchState;
|
||||
case MGPipeDirty::NewVertexAttribDefaults:
|
||||
return kMGPipeSubsystemVertexAttribDefaults;
|
||||
default:
|
||||
// Bits 5..17 have no call of their own until P3a/P3b/P4a/P4b, so there is no
|
||||
// subsystem to switch and the residual fill keeps supplying their fields.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A COMPOSITE shutter, for the bits whose "did anything move" is more than one counter.
|
||||
// It is a hash, so two different states can in principle collide and cost a MISSED fire.
|
||||
// That is acceptable for bits 5..17 and only for them: nothing consumes those bits in
|
||||
// P2, and P3 replaces each with its own exact shutter as it takes the subsystem over.
|
||||
// The five bits P2 EMITS for are never composed - they are widened counters and byte
|
||||
// compares, neither of which can collide.
|
||||
inline constexpr Uint64 MGPipeMixShutter(Uint64 accumulator, Uint64 value) {
|
||||
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
// A Uint16 counter widened at the TRACKER boundary, never in MG_State
|
||||
// (ARCHITECTURE.md 5.2: MG_State is not changed for this). A decrease is a wrap and adds
|
||||
// 65536. A wrap is harmless locally - one extra re-push, never a missed one - which is
|
||||
// exactly what TrackerTest.WrapAroundRePushesButNeverMisses pins.
|
||||
class MGPipeWidenedCounter {
|
||||
public:
|
||||
Uint64 Observe(Uint16 now) {
|
||||
if (m_started && now < m_last) m_high += 0x10000ull;
|
||||
m_started = true;
|
||||
m_last = now;
|
||||
return m_high + now;
|
||||
}
|
||||
void Reset() {
|
||||
m_high = 0;
|
||||
m_last = 0;
|
||||
m_started = false;
|
||||
}
|
||||
|
||||
private:
|
||||
Uint64 m_high = 0;
|
||||
Uint16 m_last = 0;
|
||||
Bool m_started = false;
|
||||
};
|
||||
|
||||
class MGPipeTracker {
|
||||
public:
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
// The dirty walk. Compares every shutter against what was last pushed, LATCHES the
|
||||
// new values, counts the fires per verb class, and returns the mask. Latching here
|
||||
// rather than after emission is deliberate: a bit whose subsystem is switched off is
|
||||
// not emitted, but its fields are then still pulled by the residual fill, so the
|
||||
// pushed block is correct either way and a bit can never fire twice for one change.
|
||||
Uint32 Update(GLContext& ctx, MGPipeVerbClass verbClass) {
|
||||
// A different context is a different server: nothing the tracker latched about
|
||||
// the old one says anything about this one, and the first walk on a fresh
|
||||
// context must publish a COMPLETE state rather than an increment.
|
||||
if (m_context != &ctx) {
|
||||
Reset();
|
||||
m_context = &ctx;
|
||||
}
|
||||
|
||||
Uint64 now[kMGPipeDirtyCount];
|
||||
const RenderStateParameters& render = ctx.GetRenderStateParameters();
|
||||
|
||||
// ---- bits 0..1: the two Uint16 render-state counters, widened HERE ----
|
||||
now[Index(MGPipeDirty::NewRenderState)] =
|
||||
m_renderStateVersion.Observe(static_cast<Uint16>(ctx.GetRenderStateParametersVersion()));
|
||||
now[Index(MGPipeDirty::NewPipelineState)] =
|
||||
m_pipelineStateVersion.Observe(static_cast<Uint16>(ctx.GetPipelineStateVersion()));
|
||||
|
||||
// ---- bit 4 and the value-class bits 5..8 ----
|
||||
now[Index(MGPipeDirty::NewVertexAttribDefaults)] = ctx.GetAnyVertexAttribDefaultGeneration();
|
||||
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
const Uint64 vaoIdentity =
|
||||
vao ? MGPipeMixShutter(vao->GetLifetimeId(), vao->GetConfigVersion()) : 0;
|
||||
now[Index(MGPipeDirty::NewVertexElements)] = vaoIdentity;
|
||||
|
||||
// Deliberately NOT GetProgramForDraw: that joins a pending link, and the tracker
|
||||
// must not force a compile just to answer "did the shader move". These version
|
||||
// counters are plain members and are exactly what the backends already read
|
||||
// without joining (Core.cpp, the glUseProgram half of join site J1).
|
||||
const auto& program = ctx.GetCurrentProgram();
|
||||
Uint64 shader = 0;
|
||||
Uint64 bindings = 0;
|
||||
Uint64 constants = 0;
|
||||
Uint64 programImages = 0;
|
||||
if (program) {
|
||||
shader = MGPipeMixShutter(program->GetLifetimeId(), program->GetLinkVersion());
|
||||
bindings = MGPipeMixShutter(
|
||||
MGPipeMixShutter(MGPipeMixShutter(program->GetImageUnitVersion(),
|
||||
program->GetBackendStateVersion()),
|
||||
program->GetBlockBindingVersion()),
|
||||
program->GetUniformWriteSetVersion());
|
||||
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
|
||||
programImages = program->GetImageUnitVersion();
|
||||
}
|
||||
now[Index(MGPipeDirty::NewShader)] = shader;
|
||||
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
|
||||
now[Index(MGPipeDirty::NewGlobalConstants)] = constants;
|
||||
|
||||
// ---- the object-class bits 9..17 ----
|
||||
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
|
||||
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
|
||||
const Uint64 buffers = ctx.GetAnyBufferChangeGeneration();
|
||||
|
||||
now[Index(MGPipeDirty::NewVertexBuffers)] =
|
||||
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity);
|
||||
// The index buffer lives in the bound VAO's element slot and P2 has no cheap
|
||||
// shutter for that slot alone, so it shares the buffer aggregate and over-fires
|
||||
// on any buffer write anywhere. P3b narrows it when it takes the subsystem over.
|
||||
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(buffers, vaoIdentity);
|
||||
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
|
||||
ctx.GetAnyFramebufferAttachmentGeneration(),
|
||||
m_framebufferBind.Observe(
|
||||
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion()));
|
||||
now[Index(MGPipeDirty::NewSamplerViews)] =
|
||||
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
|
||||
now[Index(MGPipeDirty::NewSamplers)] =
|
||||
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration());
|
||||
now[Index(MGPipeDirty::NewShaderImages)] =
|
||||
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages);
|
||||
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
|
||||
now[Index(MGPipeDirty::NewShaderBuffers)] = buffers;
|
||||
now[Index(MGPipeDirty::NewSoTargets)] =
|
||||
MGPipeMixShutter(buffers, ctx.GetTransformFeedbackGeneration());
|
||||
|
||||
Uint32 dirty = 0;
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||
// Bits 2 and 3 are handled below: they are BitwiseEqual shutters, not
|
||||
// counters, so they have no entry in `now`.
|
||||
if (i == Index(MGPipeDirty::NewPixelPack) || i == Index(MGPipeDirty::NewPatchState)) {
|
||||
continue;
|
||||
}
|
||||
if (!m_primed || now[i] != m_lastPushed[i]) dirty |= Uint32{1} << static_cast<Uint32>(i);
|
||||
m_lastPushed[i] = now[i];
|
||||
}
|
||||
|
||||
// ---- bit 2: the PACK half of the pixel store, BitwiseEqual ----
|
||||
const PixelStoreParameters pack = ctx.GetPixelStoreParameters(false);
|
||||
if (!m_primed || std::memcmp(&pack, &m_pack, sizeof(pack)) != 0) {
|
||||
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPixelPack);
|
||||
m_pack = pack;
|
||||
}
|
||||
|
||||
// ---- bit 3: the patch trio, BitwiseEqual, and NaN IS LEGAL ----
|
||||
// A NaN outer level is a legal glPatchParameterfv value and must compare equal to
|
||||
// itself (ARCHITECTURE.md 5.2). Float equality says it is not; memcmp says it is,
|
||||
// which is the whole reason this is a byte compare.
|
||||
PatchTrio patch{};
|
||||
patch.PatchVertices = render.PatchVertices;
|
||||
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = render.PatchDefaultOuterLevel[i];
|
||||
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = render.PatchDefaultInnerLevel[i];
|
||||
if (!m_primed || std::memcmp(&patch, &m_patch, sizeof(patch)) != 0) {
|
||||
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPatchState);
|
||||
m_patch = patch;
|
||||
}
|
||||
|
||||
m_primed = true;
|
||||
m_lastDirty = dirty;
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
const SizeT cls = static_cast<SizeT>(verbClass);
|
||||
++m_walks[cls];
|
||||
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
||||
if (dirty & (Uint32{1} << static_cast<Uint32>(i))) ++m_fires[i][cls];
|
||||
}
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
|
||||
// Context teardown, server reset, a unit test's fixture. The next Update returns
|
||||
// every bit set, which is what makes the first verb on a fresh context publish a
|
||||
// complete state rather than an increment. Deliberately does NOT clear the fire
|
||||
// tallies: they are a per-run measurement, not per-context state.
|
||||
void Reset() {
|
||||
std::memset(m_lastPushed, 0, sizeof(m_lastPushed));
|
||||
m_renderStateVersion.Reset();
|
||||
m_pipelineStateVersion.Reset();
|
||||
m_framebufferBind.Reset();
|
||||
m_pack = PixelStoreParameters{};
|
||||
m_patch = PatchTrio{};
|
||||
m_context = nullptr;
|
||||
m_lastDirty = 0;
|
||||
m_primed = false;
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
std::memset(m_fires, 0, sizeof(m_fires));
|
||||
std::memset(m_walks, 0, sizeof(m_walks));
|
||||
}
|
||||
|
||||
Uint64 FireCount(MGPipeDirty bit, MGPipeVerbClass verbClass) const {
|
||||
return m_fires[Index(bit)][static_cast<SizeT>(verbClass)];
|
||||
}
|
||||
Uint64 FireCount(MGPipeDirty bit) const {
|
||||
Uint64 total = 0;
|
||||
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_fires[Index(bit)][i];
|
||||
return total;
|
||||
}
|
||||
Uint64 WalkCount(MGPipeVerbClass verbClass) const {
|
||||
return m_walks[static_cast<SizeT>(verbClass)];
|
||||
}
|
||||
Uint64 WalkCount() const {
|
||||
Uint64 total = 0;
|
||||
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_walks[i];
|
||||
return total;
|
||||
}
|
||||
|
||||
Uint32 LastDirty() const { return m_lastDirty; }
|
||||
Bool Primed() const { return m_primed; }
|
||||
|
||||
private:
|
||||
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
|
||||
|
||||
struct PatchTrio {
|
||||
Uint PatchVertices;
|
||||
Float Outer[4];
|
||||
Float Inner[2];
|
||||
};
|
||||
|
||||
Uint64 m_lastPushed[kMGPipeDirtyCount]{};
|
||||
MGPipeWidenedCounter m_renderStateVersion;
|
||||
MGPipeWidenedCounter m_pipelineStateVersion;
|
||||
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
|
||||
// that wrapped would let a composite shutter repeat and cost a missed fire.
|
||||
MGPipeWidenedCounter m_framebufferBind;
|
||||
// Bits 2 and 3 are BitwiseEqual shutters, not counters.
|
||||
PixelStoreParameters m_pack{};
|
||||
PatchTrio m_patch{};
|
||||
|
||||
const void* m_context = nullptr;
|
||||
Uint32 m_lastDirty = 0;
|
||||
Bool m_primed = false;
|
||||
|
||||
Uint64 m_fires[kMGPipeDirtyCount][kMGPipeVerbClassCount]{};
|
||||
Uint64 m_walks[kMGPipeVerbClassCount]{};
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -190,12 +190,12 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||
#if !MOBILEGL_PIPE_POISON
|
||||
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||
#else
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||
|
||||
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
// The value was still copied: only the stamp is withheld.
|
||||
@@ -212,7 +212,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||
}
|
||||
}
|
||||
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetBoundVertexArray));
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetRenderStateParameters));
|
||||
@@ -220,7 +220,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
|
||||
|
||||
// And the omission is scoped to its verb: a different verb of the same class keeps it.
|
||||
MGPipeFillForVerb(MGPipeVerb::BindImageTexture);
|
||||
MGPipeValidateForVerb(MGPipeVerb::BindImageTexture);
|
||||
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||
#endif
|
||||
}
|
||||
@@ -237,9 +237,9 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
|
||||
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
|
||||
::_exit(3); // reached only if the poison failed
|
||||
@@ -258,9 +258,9 @@ TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) {
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetRenderStateParameters();
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0);
|
||||
(void)gPipeInputs.GetActiveTextureUnit();
|
||||
});
|
||||
@@ -309,10 +309,10 @@ TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
|
||||
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
|
||||
::_exit(3);
|
||||
@@ -334,7 +334,7 @@ TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
|
||||
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
@@ -354,7 +354,7 @@ TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
|
||||
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||
#else
|
||||
const Uint64 serialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||
EXPECT_EQ(serial, serialBefore + 1);
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
||||
@@ -392,11 +392,11 @@ TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) {
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
|
||||
MGPipeValidateForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; // the child's DrawArrays fill
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
const Float boundary = gPipeInputs.GetLineWidth(); // boundary == live: completes
|
||||
(void)gPipeInputs.GetRenderStateParameters();
|
||||
MG_State::pGLContext->SetLineWidth(boundary + 1.0f);
|
||||
@@ -426,12 +426,12 @@ TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear);
|
||||
MGPipeValidateForVerb(MGPipeVerb::Clear);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
@@ -455,7 +455,7 @@ TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) {
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "NoSuchField";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
::_exit(3);
|
||||
});
|
||||
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
@@ -479,14 +479,14 @@ TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) {
|
||||
GTEST_SKIP() << "no fork() on this platform";
|
||||
#else
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
MGPipeFillForVerb(MGPipeVerb::Clear);
|
||||
MGPipeValidateForVerb(MGPipeVerb::Clear);
|
||||
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MG_Config::Features.PipeVerifyFatal = false;
|
||||
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawElements);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawElements);
|
||||
std::exit(0);
|
||||
});
|
||||
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||
@@ -511,7 +511,7 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
|
||||
#else
|
||||
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
||||
const auto verb = static_cast<MGPipeVerb>(v);
|
||||
MGPipeFillForVerb(verb);
|
||||
MGPipeValidateForVerb(verb);
|
||||
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
||||
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||
const auto field = static_cast<MGPipeInputField>(f);
|
||||
@@ -532,7 +532,7 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
|
||||
// Without the notice the two reads below differ and the pushed value is the stale one.
|
||||
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbRefreshesThePushedField) {
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
ASSERT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
|
||||
ASSERT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
|
||||
ASSERT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
|
||||
@@ -566,7 +566,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
|
||||
#else
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
|
||||
ctx.BumpSamplingResolutionGeneration();
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
|
||||
@@ -575,7 +575,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
|
||||
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
|
||||
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
|
||||
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||
MGPipeFillForVerb(MGPipeVerb::FenceSync);
|
||||
MGPipeValidateForVerb(MGPipeVerb::FenceSync);
|
||||
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
|
||||
ctx.BumpSamplingResolutionGeneration();
|
||||
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
|
||||
@@ -598,7 +598,7 @@ TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
|
||||
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const ChildResult r = RunInChild([] {
|
||||
MG_Config::Features.PipeVerify = true;
|
||||
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
||||
(void)gPipeInputs.GetSamplingResolutionGeneration(); // boundary == live: completes
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
const Uint64 before = ctx.GetSamplingResolutionGeneration();
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace MobileGL::MG_Test {
|
||||
explicit ScopedPipeVerb([[maybe_unused]] MG_Pipe::MGPipeVerb verb)
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
: m_verb(verb) {
|
||||
MG_Pipe::MGPipeFillForVerb(m_verb);
|
||||
MG_Pipe::MGPipeValidateForVerb(m_verb);
|
||||
}
|
||||
#else
|
||||
{
|
||||
@@ -60,7 +60,7 @@ namespace MobileGL::MG_Test {
|
||||
// entry point's MGP_FILL would.
|
||||
void Renew() {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MG_Pipe::MGPipeFillForVerb(m_verb);
|
||||
MG_Pipe::MGPipeValidateForVerb(m_verb);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user