From 534f14e3f2dd4d6f85c0d4f91c361c627d10d180 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 6 Oct 2026 09:48:58 -0400 Subject: [PATCH] [Feat] (P13 W5 S1): the fatal vocabulary moves to MG_Pipe and the record arm gets its own fail / latch seam. --- .../{MG_Remote => MG_Pipe}/FatalFamilies.def | 2 +- MobileGL/MG_Pipe/PipeApply.cpp | 60 ++++++++++++++ MobileGL/MG_Pipe/PipeFatalFamily.h | 55 ++++++++++++ MobileGL/MG_Pipe/PipeSessionFail.h | 40 +++++++++ MobileGL/MG_Remote/FatalFamily.h | 40 ++------- MobileGL/MG_Remote/FatalFunnel.cpp | 19 +++++ MobileGL/MG_Test/Pipe/CMakeLists.txt | 6 ++ MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp | 83 +++++++++++++++++++ scripts/ci/fatal_census.py | 23 ++++- scripts/ci/fatal_census_baseline.json | 4 +- 10 files changed, 294 insertions(+), 38 deletions(-) rename MobileGL/{MG_Remote => MG_Pipe}/FatalFamilies.def (99%) create mode 100644 MobileGL/MG_Pipe/PipeFatalFamily.h create mode 100644 MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp diff --git a/MobileGL/MG_Remote/FatalFamilies.def b/MobileGL/MG_Pipe/FatalFamilies.def similarity index 99% rename from MobileGL/MG_Remote/FatalFamilies.def rename to MobileGL/MG_Pipe/FatalFamilies.def index 72156ed8..36250d42 100644 --- a/MobileGL/MG_Remote/FatalFamilies.def +++ b/MobileGL/MG_Pipe/FatalFamilies.def @@ -1,4 +1,4 @@ -// MobileGL - MobileGL/MG_Remote/FatalFamilies.def +// MobileGL - MobileGL/MG_Pipe/FatalFamilies.def // 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 diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index a200bd03..58a2ec54 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -119,6 +119,66 @@ namespace MobileGL::MG_Pipe { + // ---------------------------------------------------------------------------- + // P13 W5: THE RECORD ARM'S FAIL / LATCH SEAM (PipeSessionFail.h). Constant-initialized + // pointers, so a registration made during another translation unit's static init (FatalFunnel.cpp) + // is never overwritten by this one's. + namespace { + constinit MGPipeRecordFailHook g_recordFailHook = nullptr; + constinit MGPipeRecordLatchHook g_recordLatchHook = nullptr; + constinit MGPipeRecordLatchedHook g_recordLatchedHook = nullptr; + + void FormatRecordLine(char (&line)[512], const char* fmt, va_list args) { + const int written = std::vsnprintf(line, sizeof(line), fmt, args); + if (written < 0) { + std::snprintf(line, sizeof(line), "MGPipe: unformattable Fatal diagnostic (format=%s)", fmt); + } + } + + [[noreturn]] void RecordDeath(MGFatalFamily family, const char* line) { + if (g_recordFailHook != nullptr) g_recordFailHook(family, line); + // NO HOOK (no MG_Remote in this library): the line, its stderr echo - the logger's + // console sink is off and a gtest death matcher reads the child's stderr - and the abort. + MGLOG_F("%s", line); + std::fputs(line, stderr); + std::fputc('\n', stderr); + std::fflush(stderr); + std::abort(); + } + } // namespace + + void MGPipeInstallRecordFailHooks(MGPipeRecordFailHook fail, MGPipeRecordLatchHook latch, + MGPipeRecordLatchedHook latched) { + g_recordFailHook = fail; + g_recordLatchHook = latch; + g_recordLatchedHook = latched; + } + + void MGPipeRecordFail(MGFatalFamily family, const char* fmt, ...) { + char line[512]; + va_list args; + va_start(args, fmt); + FormatRecordLine(line, fmt, args); + va_end(args); + RecordDeath(family, line); + } + + bool MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...) { + char line[512]; + va_list args; + va_start(args, fmt); + FormatRecordLine(line, fmt, args); + va_end(args); + if (g_recordLatchHook != nullptr) return g_recordLatchHook(family, line); + RecordDeath(family, line); + } + + bool MGPipeRecordLatched() { return g_recordLatchedHook != nullptr && g_recordLatchedHook(); } + + MGPipeRecordFailHook MGPipeRecordFailHookInstalled() { return g_recordFailHook; } + MGPipeRecordLatchHook MGPipeRecordLatchHookInstalled() { return g_recordLatchHook; } + MGPipeRecordLatchedHook MGPipeRecordLatchedHookInstalled() { return g_recordLatchedHook; } + #if MOBILEGL_BUILD_DISAGGREGATED // ---------------------------------------------------------------------------- // THE BACKEND-SIDE SESSION-FAIL SEAM (P7 wave 0). PipeSessionFail.h holds the argument; diff --git a/MobileGL/MG_Pipe/PipeFatalFamily.h b/MobileGL/MG_Pipe/PipeFatalFamily.h new file mode 100644 index 00000000..e979c31c --- /dev/null +++ b/MobileGL/MG_Pipe/PipeFatalFamily.h @@ -0,0 +1,55 @@ +// MobileGL - MobileGL/MG_Pipe/PipeFatalFamily.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 + +// P13 W5: THE FATAL VOCABULARY WITHOUT THE WIRE. The record arm runs in a library that has no +// MG_Remote (the FCL shape), and its deaths still need their family words: a staged store that +// cannot answer, a record whose bounds are wrong. So the enum and its names are generated here +// from FatalFamilies.def, and MG_Remote/FatalFamily.h keeps only what needs the protocol - the +// projection of a family onto the seven-value wire FatalCode - and re-exports these names. + +#pragma once + +#include +#include + +#include "FatalFamilies.def" + +namespace MobileGL::MG_Pipe { + + // One value per row of FatalFamilies.def, in file order. The names ARE the family words that + // appear in every `Fatal{...}` log line, which is what lets FatalFamilyName round-trip + // against the string a site passes. + enum class MGFatalFamily : ::std::uint32_t { +#define X(Family, WireCode, Why) Family, + MGL_FATAL_FAMILY_LIST(X) +#undef X + }; + + // The family's own word, byte-for-byte what appears inside `Fatal{, ...}`. A death + // site passes both the enum and its full message string; a test asserts this name is present + // in that string, which is what keeps the two from diverging. + inline const char* FatalFamilyName(MGFatalFamily family) { + switch (family) { +#define X(Family, WireCode, Why) \ + case MGFatalFamily::Family: return #Family; + MGL_FATAL_FAMILY_LIST(X) +#undef X + } + return ""; + } + + // The count, for a test that walks every family. + inline constexpr ::std::size_t MGFatalFamilyCount() { + ::std::size_t n = 0; +#define X(Family, WireCode, Why) ++n; + MGL_FATAL_FAMILY_LIST(X) +#undef X + return n; + } + +} // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Pipe/PipeSessionFail.h b/MobileGL/MG_Pipe/PipeSessionFail.h index 25053232..e9f7cd79 100644 --- a/MobileGL/MG_Pipe/PipeSessionFail.h +++ b/MobileGL/MG_Pipe/PipeSessionFail.h @@ -39,6 +39,8 @@ #pragma once +#include + namespace MobileGL::MG_Pipe { // THE FAMILIES THAT CROSS THIS BOUNDARY, AND ONLY THOSE. Not `MGFatalFamily` cast to an @@ -85,6 +87,44 @@ namespace MobileGL::MG_Pipe { #endif void MGPipeSessionFail(MGPipeFatalFamily family, const char* fmt, ...); + // ---- P13 W5: THE RECORD ARM'S DEATHS, IN EVERY BUILD ------------------------------------- + // + // The record arm's own code - the staged stores, the verb sink, the verb port - dies by + // family word (MG_Pipe::MGFatalFamily, the whole FatalFamilies.def vocabulary) and, where a + // session can be ended instead of the process, latches. With MG_Remote linked, FatalFunnel.cpp + // registers SessionFail / SessionLatch / SessionLatched here at static init, so every DISAGG + // process - unit cases with no role init included - counts, publishes and latches exactly as + // a direct call did. Without MG_Remote (the FCL shape) the defaults are the monolith answer: + // log the line, echo it to stderr (a death test reads the child's stderr), abort; latching is + // dying; nothing is ever latched. + // + // `line` is the FULLY FORMATTED message, family word and all, as for MGPipeSessionFail. + using MGPipeRecordFailHook = void (*)(MGFatalFamily family, const char* line); + using MGPipeRecordLatchHook = bool (*)(MGFatalFamily family, const char* line); + using MGPipeRecordLatchedHook = bool (*)(); + // Last writer wins; a null leaves that half on its default. + void MGPipeInstallRecordFailHooks(MGPipeRecordFailHook fail, MGPipeRecordLatchHook latch, + MGPipeRecordLatchedHook latched); + // The installed hooks, or nullptr - so a test proves the registration happened (and restores + // what it replaced) without having to die to find out. + MGPipeRecordFailHook MGPipeRecordFailHookInstalled(); + MGPipeRecordLatchHook MGPipeRecordLatchHookInstalled(); + MGPipeRecordLatchedHook MGPipeRecordLatchedHookInstalled(); + [[noreturn]] +#if defined(__GNUC__) || defined(__clang__) + __attribute__((format(printf, 2, 3))) +#endif + void MGPipeRecordFail(MGFatalFamily family, const char* fmt, ...); + // Returns false (a Bool site reads `return MGPipeRecordLatch(...);`) when the hook latched the + // calling session; with no latch to raise it dies as MGPipeRecordFail does. + bool +#if defined(__GNUC__) || defined(__clang__) + __attribute__((format(printf, 2, 3))) +#endif + MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...); + // Whether the calling session already latched (false with no hook). + bool MGPipeRecordLatched(); + #if MOBILEGL_BUILD_DISAGGREGATED // ---- THE LATCH TWIN: A LOST GPU DEVICE ENDS ITS SESSION, NOT THE PROCESS ---------------- // diff --git a/MobileGL/MG_Remote/FatalFamily.h b/MobileGL/MG_Remote/FatalFamily.h index 4c9b6e0d..73d9e1fd 100644 --- a/MobileGL/MG_Remote/FatalFamily.h +++ b/MobileGL/MG_Remote/FatalFamily.h @@ -10,23 +10,21 @@ // wire FatalCode, both generated from FatalFamilies.def so a family, its name and its wire code // cannot drift apart: adding a family is one row, and a row that forgets its code does not // compile. +// +// P13 W5: the vocabulary itself (the enum, its names, its count) lives in MG_Pipe/PipeFatalFamily.h +// so the record arm can die by name in a library without MG_Remote; this header adds the wire +// projection and re-exports the vocabulary under its old names. #pragma once +#include #include -#include "FatalFamilies.def" - namespace MobileGL::MG_Remote { - // One value per row of FatalFamilies.def, in file order. The names ARE the family words that - // appear in every `Fatal{...}` log line, which is what lets FatalFamilyName round-trip - // against the string a site passes. - enum class MGFatalFamily : ::std::uint32_t { -#define X(Family, WireCode, Why) Family, - MGL_FATAL_FAMILY_LIST(X) -#undef X - }; + using ::MobileGL::MG_Pipe::FatalFamilyName; + using ::MobileGL::MG_Pipe::MGFatalFamily; + using ::MobileGL::MG_Pipe::MGFatalFamilyCount; // THE PROJECTION, AS A TOTAL TABLE. Not a switch with a default arm: this build has no // -Werror, so a default would silently swallow a family added without a code. Every family @@ -43,26 +41,4 @@ namespace MobileGL::MG_Remote { return ::MobileGL::Wire::FatalCode::ProtocolCorruption; } - // The family's own word, byte-for-byte what appears inside `Fatal{, ...}`. A death - // site passes both the enum and its full message string; a test asserts this name is present - // in that string, which is what keeps the two from diverging. - inline const char* FatalFamilyName(MGFatalFamily family) { - switch (family) { -#define X(Family, WireCode, Why) \ - case MGFatalFamily::Family: return #Family; - MGL_FATAL_FAMILY_LIST(X) -#undef X - } - return ""; - } - - // The count, for a test that walks every family. - inline constexpr ::std::size_t MGFatalFamilyCount() { - ::std::size_t n = 0; -#define X(Family, WireCode, Why) ++n; - MGL_FATAL_FAMILY_LIST(X) -#undef X - return n; - } - } // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/FatalFunnel.cpp b/MobileGL/MG_Remote/FatalFunnel.cpp index 402eb01e..7cacd3c2 100644 --- a/MobileGL/MG_Remote/FatalFunnel.cpp +++ b/MobileGL/MG_Remote/FatalFunnel.cpp @@ -307,4 +307,23 @@ namespace MobileGL::MG_Remote { } } // namespace + // P13 W5: THE RECORD ARM'S DEATHS GO THROUGH THIS FUNNEL WHEREVER MG_Remote IS LINKED. + // Registered at static init rather than at role init: both roles, and unit cases that never + // run one, call into the record arm, and each of its deaths must count, publish and latch + // exactly as the direct SessionFail / SessionLatch call it replaced did. The line arrives + // formatted, so it is passed through "%s" - the same bytes reach the log and the frame. + namespace { + [[noreturn]] void RecordFailThroughFunnel(MGFatalFamily family, const char* line) { + SessionFail(family, "%s", line); + } + bool RecordLatchThroughFunnel(MGFatalFamily family, const char* line) { + return SessionLatch(family, "%s", line); + } + [[maybe_unused]] const bool g_recordFailHooksRegistered = [] { + MG_Pipe::MGPipeInstallRecordFailHooks(&RecordFailThroughFunnel, &RecordLatchThroughFunnel, + &SessionLatched); + return true; + }(); + } // namespace + } // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Test/Pipe/CMakeLists.txt b/MobileGL/MG_Test/Pipe/CMakeLists.txt index d4ac85be..e2a43163 100644 --- a/MobileGL/MG_Test/Pipe/CMakeLists.txt +++ b/MobileGL/MG_Test/Pipe/CMakeLists.txt @@ -24,6 +24,12 @@ if (MSVC) target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor) endif() +# P13 W5: the record arm's fail / latch seam, in both library shapes (with and without MG_Remote). +add_executable(RecordFailSeamTest RecordFailSeamTest.cpp) +target_include_directories(RecordFailSeamTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL) +target_link_libraries(RecordFailSeamTest PRIVATE GTest::gtest_main ${LINK_LIBRARIES}) +gtest_discover_tests(RecordFailSeamTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) + # P7 B4 aggregate-wait regression control shares the production selector; no device is needed. add_executable(MagmaAggregateWaitTest MagmaAggregateWaitTest.cpp) target_include_directories(MagmaAggregateWaitTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL) diff --git a/MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp b/MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp new file mode 100644 index 00000000..0ffc371c --- /dev/null +++ b/MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp @@ -0,0 +1,83 @@ +// MobileGL - MobileGL/MG_Test/Pipe/RecordFailSeamTest.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 + +// P13 W5: THE RECORD ARM'S FAIL / LATCH SEAM (MG_Pipe/PipeSessionFail.h), in both library shapes. +// Without MG_Remote the defaults are the monolith answer - the line on stderr, then the abort; with +// it, FatalFunnel.cpp's static registration is in place before any case runs, so the record arm's +// deaths count and publish through SessionFail exactly as a direct call did. + +#include +#include + +#include + +#include + +using namespace MobileGL; +using MG_Pipe::MGFatalFamily; + +TEST(RecordFailSeam, TheVocabularyIsTheDefFilesWords) { + EXPECT_STREQ(MG_Pipe::FatalFamilyName(MGFatalFamily::ProtocolCorruption), "ProtocolCorruption"); + EXPECT_STREQ(MG_Pipe::FatalFamilyName(MGFatalFamily::BackendDeviceLost), "BackendDeviceLost"); + EXPECT_GT(MG_Pipe::MGFatalFamilyCount(), 10u); +} + +TEST(RecordFailSeam, TheFunnelIsRegisteredWhereverMgRemoteIsLinked) { +#if MOBILEGL_BUILD_DISAGGREGATED + EXPECT_NE(MG_Pipe::MGPipeRecordFailHookInstalled(), nullptr) + << "FatalFunnel.cpp's static registration did not run: the record arm's deaths would skip " + "SessionFail's count and SessionFault frame"; + EXPECT_NE(MG_Pipe::MGPipeRecordLatchHookInstalled(), nullptr); + EXPECT_NE(MG_Pipe::MGPipeRecordLatchedHookInstalled(), nullptr); +#else + EXPECT_EQ(MG_Pipe::MGPipeRecordFailHookInstalled(), nullptr) << "a library without MG_Remote has no funnel"; + EXPECT_EQ(MG_Pipe::MGPipeRecordLatchHookInstalled(), nullptr); + EXPECT_EQ(MG_Pipe::MGPipeRecordLatchedHookInstalled(), nullptr); +#endif + EXPECT_FALSE(MG_Pipe::MGPipeRecordLatched()) << "nothing latched in a process with no session"; +} + +TEST(RecordFailSeamDeathTest, AFailWritesTheCallersLineAndDies) { + EXPECT_DEATH(MG_Pipe::MGPipeRecordFail(MGFatalFamily::ProtocolCorruption, + "MGPipe: Fatal{ProtocolCorruption, \"record-seam-unit\"} n=%d", 7), + "Fatal\\{ProtocolCorruption, \"record-seam-unit\"\\} n=7"); +} + +TEST(RecordFailSeamDeathTest, ALatchWithNoSessionToLatchDiesTheSameWay) { + EXPECT_DEATH((void)MG_Pipe::MGPipeRecordLatch(MGFatalFamily::BackendDeviceLost, + "MGPipe: Fatal{BackendDeviceLost, \"record-seam-unit\"} n=%d", 9), + "Fatal\\{BackendDeviceLost, \"record-seam-unit\"\\} n=9"); +} + +namespace { + MGFatalFamily g_seenFamily{}; + std::string g_seenLine; + bool CaptureLatch(MGFatalFamily family, const char* line) { + g_seenFamily = family; + g_seenLine = line; + return false; + } + bool AlwaysLatched() { return true; } +} // namespace + +// An installed latch hook gets the family and the FORMATTED line, and its answer is the call's. +TEST(RecordFailSeam, AnInstalledLatchHookGetsTheFormattedLine) { + const auto fail = MG_Pipe::MGPipeRecordFailHookInstalled(); + const auto latch = MG_Pipe::MGPipeRecordLatchHookInstalled(); + const auto latched = MG_Pipe::MGPipeRecordLatchedHookInstalled(); + MG_Pipe::MGPipeInstallRecordFailHooks(fail, &CaptureLatch, &AlwaysLatched); + const bool answer = MG_Pipe::MGPipeRecordLatch(MGFatalFamily::BackendDeviceLost, + "MGPipe: Fatal{BackendDeviceLost, \"captured\"} k=%u", 3u); + const bool nowLatched = MG_Pipe::MGPipeRecordLatched(); + MG_Pipe::MGPipeInstallRecordFailHooks(fail, latch, latched); + EXPECT_FALSE(answer); + EXPECT_TRUE(nowLatched); + EXPECT_EQ(g_seenFamily, MGFatalFamily::BackendDeviceLost); + EXPECT_EQ(g_seenLine, "MGPipe: Fatal{BackendDeviceLost, \"captured\"} k=3"); + EXPECT_EQ(MG_Pipe::MGPipeRecordLatchHookInstalled(), latch) << "the case did not restore the hooks"; +} diff --git a/scripts/ci/fatal_census.py b/scripts/ci/fatal_census.py index e541750a..efbf1439 100644 --- a/scripts/ci/fatal_census.py +++ b/scripts/ci/fatal_census.py @@ -110,7 +110,8 @@ EXCLUDED_PARTS = ("MG_Test", "MG_Benchmark", "MG_IntegrationTest") SOURCE_SUFFIXES = (".cpp", ".h", ".inc") BASELINE = Path(__file__).with_name("fatal_census_baseline.json") -FAMILIES_DEF = ROOT / "MobileGL" / "MG_Remote" / "FatalFamilies.def" +# P13 W5: the vocabulary moved to MG_Pipe so the record arm can name its deaths without MG_Remote. +FAMILIES_DEF = ROOT / "MobileGL" / "MG_Pipe" / "FatalFamilies.def" PROTOCOL_FBS = ROOT / "MobileGL" / "MG_Remote" / "Protocol" / "protocol.fbs" # The funnel's own files. WireLog.cpp's abort is reached only through WireLogFatal, and rule 2 @@ -125,6 +126,9 @@ FUNNEL_FILES = { # call, so it is exempt from both rules for the same reason WireLog is. "MobileGL/MG_Remote/FatalFunnel.cpp", "MobileGL/MG_Remote/FatalFunnel.h", + # P13 W5: the seam header declares MGPipeRecordFail / MGPipeRecordLatch; it has no abort and no + # call, and its declarations would otherwise read as calls with no family word. + "MobileGL/MG_Pipe/PipeSessionFail.h", } # THE ALLOW-LIST FOR FUNNELS THAT DO NOT GET A WHOLE FILE TO THEMSELVES (P7 wave 0). An unmarked @@ -141,6 +145,10 @@ FUNNEL_SITES = { "the no-hook default of the backend session-fail seam (MG_Pipe/PipeSessionFail.h): the " "line it logs is the CALLER's, family word and all, and with MG_Remote's hook installed " "this abort is never reached at all"), + ("[[noreturn]] void RecordDeath(", + "P13 W5: the no-hook default of the record arm's fail seam (MGPipeRecordFail / " + "MGPipeRecordLatch): the line is the CALLER's, family word and all, and wherever MG_Remote " + "is linked FatalFunnel.cpp's static registration sends it through SessionFail instead"), ), "MobileGL/MG_Pipe/generated/PipeWire.inc": ( ("MGPipeWireProtocolFatal", @@ -180,6 +188,10 @@ SESSION_FAIL_CALL = re.compile(r"(?