mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[Feat] (Pipe): the MOBILEGL_PIPE_VERIFY shadow comparator - a per-verb entry compare over the fill set and a compare-at-read in every accessor, first differing field and verb serial, fatal by default
- Entry compare: at the end of MGPipeFillForVerb a file-static second PipeInputs is filled by SnapshotFromGLContext (the branch that survives P13) over the same class mask, MOBILEGL_PIPE_VERIFY_CORRUPT perturbs one field of that snapshot arm, and MGPipeVerifyInputs compares every field in the mask through MGPipeInputsFieldEqual (V by G4's MGPipeFieldEqual, O by identity, F equal by definition). Both arms come from the same context at the same instant, so this arm is tautological until P2 - the CORRUPT knob keeps it falsifiable.
- Compare-at-read: MGP_INPUT_VERIFY_READ now calls MGPipeVerifyReadHook(*this, field, i0, i1), which re-reads the whole field from the live context into a scratch block and compares it against the stored value on the live block only - a superset of "the same indices"; the indices decorate the report. This is the arm that is real in P1.
- Reporting per D8: MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"Field@Verb\", verb=<serial>, where=entry|read}") then abort unless MOBILEGL_PIPE_VERIFY_FATAL=0, which counts and summarises at teardown with MGLOG_E; arming logs "MGPipe: verify armed - 63 fields, 69 verbs, fatal=N" once, the knobs acknowledge themselves, an unknown field name is Fatal{PipeVerifyBadKnob}; a push build without the comparator answers MOBILEGL_PIPE_VERIFY=1 with one MGLOG_W_ONCE.
- MGPipeVerifyInputs carries default visibility so the retrace-verify job's nm -D probe can prove the verify library was the one swapped in; pointer corruption flips low bits instead of nulling (a null pointer already null was invisible to the compare), a SharedPtr becomes an aliasing pointer with no control block; CurrentVertexAttributeValue gets its own bitwise equality.
This commit is contained in:
@@ -7,12 +7,13 @@
|
||||
// End of Source File Header
|
||||
|
||||
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
|
||||
// name lookups the runtime knobs need, and - in a verify build - the per-field equality and
|
||||
// the corruption injector the comparator uses. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
|
||||
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
|
||||
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
|
||||
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
@@ -43,6 +44,8 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
namespace {
|
||||
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
|
||||
|
||||
// Every overload is declared up front: the array overloads recurse into their element
|
||||
// type, and a call inside a template only sees what was declared before the template.
|
||||
template <class T>
|
||||
@@ -54,6 +57,7 @@ namespace MobileGL::MG_Pipe {
|
||||
template <class T, SizeT N>
|
||||
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
|
||||
template <class T>
|
||||
void CorruptStorage(T& v);
|
||||
template <class T>
|
||||
@@ -63,6 +67,7 @@ namespace MobileGL::MG_Pipe {
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]);
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v);
|
||||
|
||||
// ---- equality over one field's storage ----
|
||||
// O-class storage compares by identity: a raw pointer into the context, or the object a
|
||||
@@ -86,6 +91,14 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
|
||||
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
|
||||
}
|
||||
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
|
||||
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
|
||||
// what turns a fourth member into a build break rather than a blind spot.
|
||||
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
|
||||
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
|
||||
"CurrentVertexAttributeValue grew a member; update the comparator");
|
||||
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
|
||||
}
|
||||
template <class T>
|
||||
Bool StorageEqual(const T& a, const T& b) {
|
||||
return MGPipeFieldEqual(a, b);
|
||||
@@ -93,15 +106,21 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---- corruption of one field's storage ----
|
||||
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
|
||||
// scalar or enum moves by one, a pointer becomes null, a SharedPtr is dropped, an array
|
||||
// corrupts its first element, and any other struct has its first byte XOR'ed with 0x5A.
|
||||
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
|
||||
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
|
||||
// flipped address with no control block, an array corrupts its first element, and any
|
||||
// other struct has its first byte XOR'ed with 0x5A.
|
||||
template <class T>
|
||||
T* FlipPointer(T* p) {
|
||||
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T*& p) {
|
||||
p = nullptr;
|
||||
p = FlipPointer(p);
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(SharedPtr<T>& p) {
|
||||
p.reset();
|
||||
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
|
||||
}
|
||||
template <class T, SizeT N>
|
||||
void CorruptStorage(T (&a)[N]) {
|
||||
@@ -110,6 +129,9 @@ namespace MobileGL::MG_Pipe {
|
||||
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
|
||||
CorruptStorage(c.Blend);
|
||||
}
|
||||
void CorruptStorage(CurrentVertexAttributeValue& v) {
|
||||
v.floatValue[0] += 1.f;
|
||||
}
|
||||
template <class T>
|
||||
void CorruptStorage(T& v) {
|
||||
if constexpr (std::is_same_v<T, Bool>) {
|
||||
@@ -128,13 +150,25 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, PipeInputs& a, PipeInputs& b) {
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
|
||||
// A forwarded field has no storage and is equal by definition; VisitStorage answers
|
||||
// false for it, hence the explicit sticky test first.
|
||||
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
|
||||
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
|
||||
}
|
||||
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField) {
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
|
||||
if (outField != nullptr) *outField = field;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
|
||||
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
|
||||
CorruptStorage(x);
|
||||
|
||||
@@ -50,10 +50,20 @@ namespace MobileGL::MG_Pipe {
|
||||
#else
|
||||
#define MGP_INPUT_CHECK(Field) ((void)0)
|
||||
#endif
|
||||
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8): re-reads
|
||||
// the same accessor with the same indices from the live context and compares. Armed by
|
||||
// the comparator commit; until then every build's accessor is a load.
|
||||
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
|
||||
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
|
||||
// against the stored value, and reports the FIRST divergence as
|
||||
// Fatal{PipeVerifyDiffer, "Field@Verb", verb=<serial>, where=read} (the indices go in a
|
||||
// preceding MGLOG_E). Only the live block (gPipeInputs) is verified; a snapshot's own
|
||||
// accessors are plain loads. Off in every other build.
|
||||
struct PipeInputs;
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1);
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) \
|
||||
::MobileGL::MG_Pipe::MGPipeVerifyReadHook(*this, (Field), static_cast<Uint>(Index0), static_cast<Uint>(Index1))
|
||||
#else
|
||||
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
|
||||
#endif
|
||||
|
||||
// The V/O storage of every field that has storage, by field id. The seven F-class
|
||||
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
|
||||
@@ -675,10 +685,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
|
||||
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
|
||||
// always equal (no storage).
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, PipeInputs& a, PipeInputs& b);
|
||||
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b);
|
||||
// PipeInputs.cpp. The entry compare: every field in `mask` of the pushed block against the
|
||||
// snapshot, first differing field out. Exported from the shared library on purpose - the
|
||||
// retrace-verify CI job proves it swapped in a verify build by finding this symbol with
|
||||
// nm -D, so a "green" run against a library without the comparator cannot happen.
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
|
||||
MGPipeInputField* outField);
|
||||
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
|
||||
// scalar, ^0x5A the first byte of a struct, null a pointer). Returns false for a forwarded
|
||||
// field, which has nothing to corrupt.
|
||||
// scalar, ^0x5A the first byte of a struct, flip a pointer's low bits - never
|
||||
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
|
||||
// which has nothing to corrupt.
|
||||
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
@@ -318,8 +318,114 @@ namespace MobileGL::MG_Pipe {
|
||||
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
|
||||
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
|
||||
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
|
||||
// once per verb (the pushed block against a second snapshot of the live context,
|
||||
// taken at the same instant - tautological until P2 gives the first arm a real
|
||||
// filler, and kept falsifiable by MOBILEGL_PIPE_VERIFY_CORRUPT), and the
|
||||
// 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
|
||||
|
||||
struct VerifyState {
|
||||
Bool Parsed = false;
|
||||
Bool Enabled = false;
|
||||
Bool Fatal = true;
|
||||
Bool InHook = false; // a re-read that re-enters an accessor is not re-verified
|
||||
Optional<MGPipeInputField> Corrupt;
|
||||
std::atomic<Uint64> Divergences{0};
|
||||
~VerifyState() {
|
||||
const Uint64 count = Divergences.load(std::memory_order_relaxed);
|
||||
if (count != 0) {
|
||||
MGLOG_E("MGPipe: verify summary - %llu divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0",
|
||||
static_cast<unsigned long long>(count));
|
||||
}
|
||||
}
|
||||
};
|
||||
VerifyState g_verify;
|
||||
|
||||
void ArmVerify() {
|
||||
if (g_verify.Parsed) return;
|
||||
g_verify.Parsed = true;
|
||||
g_verify.Enabled = MG_Config::Features.PipeVerify;
|
||||
if (!g_verify.Enabled) return;
|
||||
g_verify.Fatal = MG_Config::Features.PipeVerifyFatal;
|
||||
const String& corrupt = MG_Config::Features.PipeVerifyCorrupt;
|
||||
if (!corrupt.empty()) {
|
||||
const auto field = MGPipeFindInputField(corrupt.c_str());
|
||||
if (!field) {
|
||||
BadKnob("MOBILEGL_PIPE_VERIFY_CORRUPT", corrupt.c_str(), "no such field in kMGPipeInputFieldNames");
|
||||
}
|
||||
g_verify.Corrupt = field;
|
||||
}
|
||||
// The lanes grep for this line: a verify run whose log lacks it never armed.
|
||||
MGLOG_I("MGPipe: verify armed - %u fields, %u verbs, fatal=%d", static_cast<unsigned>(kMGPipeInputFieldCount),
|
||||
static_cast<unsigned>(kMGPipeVerbCount), g_verify.Fatal ? 1 : 0);
|
||||
if (g_verify.Corrupt) {
|
||||
MGLOG_I("MGPipe: verify corruption armed - %s", kMGPipeInputFieldNames[static_cast<SizeT>(*g_verify.Corrupt)]);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportDivergence(MGPipeInputField field, const char* where) {
|
||||
const Uint64 serial = MGPipeFillAccess::Filled(gPipeInputs).CurrentVerbSerial;
|
||||
MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}",
|
||||
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(gPipeInputs.CurrentVerb()),
|
||||
static_cast<unsigned long long>(serial), where);
|
||||
if (g_verify.Fatal) std::abort();
|
||||
g_verify.Divergences.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void EntryCompare(PipeInputs& inputs, const MGPipeFieldMask& mask) {
|
||||
if (!g_verify.Enabled) return;
|
||||
SnapshotFromGLContext(g_snapshot, mask);
|
||||
// Negative control A: perturb the SNAPSHOT arm, so a green run goes red naming the
|
||||
// field. A field outside this verb's mask is not compared and stays untouched.
|
||||
if (g_verify.Corrupt && MGPipeFieldMaskHas(mask, *g_verify.Corrupt)) {
|
||||
MGPipeApplyVerifyCorruption(g_snapshot, *g_verify.Corrupt);
|
||||
}
|
||||
MGPipeInputField differing = MGPipeInputField::kFieldCount;
|
||||
if (!MGPipeVerifyInputs(inputs, g_snapshot, mask, &differing)) ReportDivergence(differing, "entry");
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
} // namespace
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask) {
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(snapshot, ctx);
|
||||
MGPipeFillAccess::SetVerb(snapshot, gPipeInputs.CurrentVerb());
|
||||
if (ctx == nullptr) return;
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field) || kMGPipeInputFieldSticky[i]) continue;
|
||||
MGPipeFillAccess::CopyField(snapshot, *ctx, field);
|
||||
}
|
||||
}
|
||||
|
||||
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
|
||||
if (&self != &gPipeInputs || !g_verify.Enabled || g_verify.InHook) return;
|
||||
const auto index = static_cast<SizeT>(field);
|
||||
if (kMGPipeInputFieldSticky[index]) return;
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) return;
|
||||
// The whole field is re-read and compared - a superset of "the same indices", so a
|
||||
// divergence in an index the backend did not ask for is still a divergence between
|
||||
// the boundary value and the live value. The indices only decorate the report.
|
||||
g_verify.InHook = true;
|
||||
MGPipeFillAccess::CopyField(g_readScratch, *ctx, field);
|
||||
const Bool equal = MGPipeInputsFieldEqual(field, self, g_readScratch);
|
||||
g_verify.InHook = false;
|
||||
if (equal) return;
|
||||
MGLOG_E("MGPipe: verify read of %s (index %u, %u) differs from the live context", kMGPipeInputFieldNames[index],
|
||||
index0, index1);
|
||||
ReportDivergence(field, "read");
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_VERIFY
|
||||
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
|
||||
if (verb == nullptr || field == nullptr) {
|
||||
g_omission = PoisonOmission{};
|
||||
@@ -386,6 +492,16 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeFillForVerb(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
ParsePoisonOmissionKnob();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
ArmVerify();
|
||||
#else
|
||||
// The runtime knob without the compiled comparator is a no-op that would look green;
|
||||
// this warning is what a lane's arming assertion turns into red.
|
||||
if (MG_Config::Features.PipeVerify) {
|
||||
MGLOG_W_ONCE("MGPipe: MOBILEGL_PIPE_VERIFY=1 requested but the comparator is not compiled in "
|
||||
"(configure with -DMOBILEGL_PIPE_VERIFY=ON)");
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
|
||||
// Starts at 1, so FilledGen == 0 means "never filled".
|
||||
@@ -415,5 +531,8 @@ namespace MobileGL::MG_Pipe {
|
||||
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
|
||||
#endif
|
||||
}
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
EntryCompare(inputs, mask);
|
||||
#endif
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
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
|
||||
@@ -29,6 +31,14 @@ namespace MobileGL::MG_Pipe {
|
||||
// fill; tests call it directly. Both null clears the omission. An unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
void MGPipeSetPoisonOmission(const char* verb, const char* field);
|
||||
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// PipeFill.cpp. The second arm of the comparator (P1 brief D8, ARCHITECTURE.md 13.2-2):
|
||||
// fills `snapshot` from the live GLContext the old way, for every field in `mask`. This
|
||||
// is the branch that survives P13, which is why it is its own function rather than the
|
||||
// filler's loop.
|
||||
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)
|
||||
#else
|
||||
|
||||
Reference in New Issue
Block a user