Merge branch 'p5c-gt' into feat/disaggregated (P5c gt: role guards, InBarrierWait wired, rsp measured)

This commit is contained in:
2026-09-17 11:21:39 -04:00
10 changed files with 469 additions and 3 deletions
+116 -1
View File
@@ -1,4 +1,4 @@
name: Test name: Test
on: on:
push: push:
@@ -1116,6 +1116,121 @@ jobs:
path: /tmp/core.* path: /tmp/core.*
if-no-files-found: ignore if-no-files-found: ignore
# P5c gt (CONTRACT-P5C §6): the strict twin of integration-split. The role guards (the two
# layers of Fatal{RoleViolation, ...}) are COMPILED into every split build - there is no
# knob to arm them - so "guards armed" below means MOBILEGL_IPC_STRICT_ERRORS=1, the knob
# that promotes a BARRIER-PULLED residual read from counted debt to a named abort.
#
# TWO ASSERTIONS, NOT ONE, because the two halves of "strict is armed" are different
# statements:
#
# * the unit label must stay GREEN under it. A unit case that newly aborts under strict
# is a value-class read that crept back in (P5c rv's exit line is value-class = 0), and
# this step is what says so. FieldOwnershipTest owns the strict arms' behaviour cases;
#
# * the integration-split scenarios must all ABORT under it, BY DESIGN: the remaining
# residual pulls are the pinned object-class rows (GetFramebufferBindingSlot & co.,
# P3b/P4b/P7's), every drawing scenario reads one, and strict turns the first read into
# Fatal{UnmigratedPipeInput, ...} [BARRIER-PULLED, MOBILEGL_IPC_STRICT_ERRORS=1, ...].
# A lane that "passed" a scenario here would mean strict never armed - the same
# two-state shape as the E1/E3(a) negative controls, so the step asserts the abort AND
# greps the entry's private split-log for the strict marker. This half flips meaning
# when the object-class rows retire (P7): the scenarios then PASS under strict and the
# step is rewritten as an ordinary green lane.
integration-split-strict:
runs-on: ubuntu-latest
timeout-minutes: 60
needs: build-linux-split
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Get CMake
uses: lukka/get-cmake@v4.3.3
- name: Install runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
- name: Download Linux split runtime
uses: actions/download-artifact@v8
with:
name: mobilegl-linux-runtime-split
path: .
- name: Unpack Linux split runtime
run: |
tar -xzf mobilegl-linux-runtime-split.tgz
test -f build-split/libMobileGL.so
- name: Normalize CTest command paths
run: |
python - <<'PY'
from pathlib import Path
import re
for path in Path('build-split').rglob('CTestTestfile.cmake'):
text = path.read_text()
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
path.write_text(text)
PY
# The green half: no unit case may pull harder under strict. This includes the
# RemoteGuards suite (the layer-1/layer-2 Fatal drives) and FieldOwnershipTest's strict
# arms, so the guards are provably armed in the lane that gates on them.
- name: Unit tests on the split runtime under MOBILEGL_IPC_STRICT_ERRORS=1
working-directory: build-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_IPC_STRICT_ERRORS: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
# The expected-red half: every integration-split entry aborts on its first BARRIER-PULLED
# read under strict. The run is EXPECTED to fail; what the step gates on is that the
# failure is the strict Fatal and nothing else. A scenario that PASSED would mean strict
# never armed, and one whose private log lacks the marker failed for another reason.
- name: Integration-split aborts under strict with the strict marker
working-directory: build-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_IPC_STRICT_ERRORS: "1"
MOBILEGL_TRANSPORT: "inproc"
run: |
ulimit -c unlimited
rc=0
ctest -L integration-split --no-tests=error -j 4 > "${RUNNER_TEMP}/strict-split.txt" 2>&1 || rc=$?
cat "${RUNNER_TEMP}/strict-split.txt" | tail -5
if [ "${rc}" -eq 0 ]; then
echo "::error::integration-split under MOBILEGL_IPC_STRICT_ERRORS=1 went GREEN - the object-class residual pulls (P3b/P4b/P7) are still pinned, so strict must abort every drawing scenario. If the object-class rows have just retired, rewrite this step as the green lane it then becomes."
exit 1
fi
failed=$(grep -cE '^ *[0-9]+ - .* \(.*(Failed|Subprocess aborted).*\)' "${RUNNER_TEMP}/strict-split.txt" || true)
# Every entry that RAN (did not skip) must have failed; entries skipped by a missing
# GPU or an unmet pre-flight are reported, not counted.
ran=$(grep -cE 'tests passed, [0-9]+ tests failed out of [0-9]+' "${RUNNER_TEMP}/strict-split.txt" || true)
echo "strict run: rc=${rc}"
# The strict marker, from the split entries' own private logs: the run's red must be
# the strict Fatal and not a bring-up defect.
marker='BARRIER-PULLED, MOBILEGL_IPC_STRICT_ERRORS=1'
hits=$(grep -rl "${marker}" MobileGL/MG_IntegrationTest/split-logs/ 2>/dev/null | wc -l)
if [ "${hits}" -lt 1 ]; then
echo "::error::no split entry's private log carries the strict marker '${marker}' - the lane went red without the strict Fatal firing, which is a defect, not the control"
exit 1
fi
echo "integration-split under strict is red by design; ${hits} private log(s) carry the strict marker"
- name: Upload strict lane logs
if: always()
uses: actions/upload-artifact@v7
with:
name: integration-split-strict-logs
path: |
build-split/MobileGL/MG_IntegrationTest/*.log*
build-split/MobileGL/MG_IntegrationTest/split-logs/*.log
if-no-files-found: warn
# MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and # MobileGL/MG_Remote/Protocol/generated/protocol_generated.h is COMMITTED, and
# flatc is deliberately absent from the default build graph (a codegen step in # flatc is deliberately absent from the default build graph (a codegen step in
# the graph is how the earlier branch ended up cross-compiling an arm64 flatc # the graph is how the earlier branch ended up cross-compiling an arm64 flatc
+79 -2
View File
@@ -5892,6 +5892,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
"pending set - they would be allocated empty", "pending set - they would be allocated empty",
stateTextureObject->GetExternalIndex()); stateTextureObject->GetExternalIndex());
} }
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt): under an active transport the replay loop below reaches the layer-1
// texture guard (MarkStorageDirty on a frontend object is Fatal{RoleViolation,
// "texture-legacy-arm"}) BEFORE the N-4 marker it owns - so the marker is hoisted
// here, ahead of the first guarded contact, with its semantics unchanged: raised
// only when a level would actually be replayed. The predicate is answered by the
// server's own staged-texture store rather than the frontend's extent walk: a
// Defined level has a non-zero extent and bytes on the frontend (the two conditions
// the loop tests), and an undefined one reads {0,0,0} and is skipped. A texture with
// no applier record (rearmRes null, the MGLOG_E_ONCE arm above) falls through to the
// guard, which names the same violation one level down.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
const auto* rearmRecord = PipeTextureRecordForHandle(rearmRes);
if (rearmRecord != nullptr) {
auto& rearmStore = MG_Remote::Server::ServerStagedTexture();
const Uint64 rearmKey = MG_Remote::Server::StagedTextureStore::KeyForHandle(rearmRes);
Bool anyLevelWouldReplay = false;
for (const auto& uploadTarget :
BufferImpl::StagedUploadTargetsForPipeTarget(rearmRecord->Desc.Target)) {
for (Uint32 level = 0; level < rearmRecord->Desc.Levels; ++level) {
if (rearmStore.IsLevelDefined(rearmKey, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyLevelWouldReplay = true;
break;
}
}
if (anyLevelWouldReplay) break;
}
if (anyLevelWouldReplay) {
MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull");
}
}
}
#endif #endif
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) { if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount(); const auto levelCount = mipmapObject->GetMipmapLevelCount();
@@ -7153,6 +7188,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
// left the backend name with no levels whatsoever, so the level that WAS defined could // left the backend name with no levels whatsoever, so the level that WAS defined could
// never be sampled or read back. Sync whenever some level holds an image; the per-level // never be sampled or read back. Sync whenever some level holds an image; the per-level
// loops below skip the degenerate ones individually. // loops below skip the degenerate ones individually.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 1): on the staged arm the same question is answered
// out of the SERVER's staged-texture store, never out of the frontend object - the
// store's Defined-ness (fed by the respecify hook and the sub-data adoption) IS "some
// level holds an image", and IsComplete()'s sampling half is redundant with it: a
// complete mipmap chain has a defined level 0, and the gate's whole job is to let an
// incomplete-but-partly-defined chain through. The one divergence is deliberate and
// stated: a chain whose EVERY level is 0x0 is complete-by-quirk on the frontend (the
// "0x0 in last level" relaxation, TextureObject.cpp) and undefined here - and syncing
// it would upload nothing either way, because every per-level loop below skips a
// {0,0,0} level individually. A BUFFER texture never has staged levels, so its gate
// is the descriptor's format - exactly what TextureObjectBase::IsComplete() reduces
// to for that storage kind.
if (MGB_STAGED_TEXTURE_LIVE) {
const auto& stagedDesc = pushedStorage->Desc;
Bool anyDefined = false;
if (static_cast<TextureStorageType>(stagedDesc.StorageKind) == TextureStorageType::Buffer) {
// A buffer texture has no staged levels; its gate is the descriptor's
// format, which is what TextureObjectBase::IsComplete() reduces to here.
anyDefined = static_cast<TextureInternalFormat>(stagedDesc.InternalFormat) !=
TextureInternalFormat::Unknown;
} else {
auto& stagedStore = MG_Remote::Server::ServerStagedTexture();
const Uint64 stagedKey = MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes);
for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(stagedDesc.Target)) {
for (Uint32 level = 0; level < stagedDesc.Levels; ++level) {
if (stagedStore.IsLevelDefined(stagedKey, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyDefined = true;
break;
}
}
if (anyDefined) break;
}
}
if (!anyDefined) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
} else
#endif
if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) { if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex()); stateTextureObject->GetExternalIndex());
@@ -7321,7 +7398,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break; break;
default: default:
MGLOG_E_ONCE("Unhandled texture target %s", MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str()); MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
break; break;
} }
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
@@ -8127,7 +8204,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
break; break;
default: default:
MGLOG_E_ONCE("Unhandled texture target %s", MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str()); MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
break; break;
} }
if (ringStaged) { if (ringStaged) {
@@ -1744,6 +1744,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) { TextureResource &outResource) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt): Magma's texture sync still reads - and clears - the CLIENT's mip shadow:
// the dirty scan below, and UploadDirtyMipLevels' texel reads / region reads /
// MarkStorageDirty(false) clears. CONTRACT-P5C §2 migrated Espryt's sync and Magma's
// T5 writes, and left THIS path on the legacy arm; under the verb barrier and one
// address space the reads answer correctly, and the migration is P7's server-side
// sync. The scope is the debt's greppable form (MipmapStorage.h); outside it the
// layer-1 guard still aborts.
const MG_State::GLState::MGPipeTextureLegacyArmScope textureLegacyArm;
#endif
// Cross-draw fast path: if the resource is already built and neither the texture's // Cross-draw fast path: if the resource is already built and neither the texture's
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion) // pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
// nor its params changed since the last sync, there is nothing to re-check or // nor its params changed since the last sync, there is nothing to re-check or
+17
View File
@@ -147,6 +147,23 @@ namespace MobileGL::MG_Pipe {
if (!serverStamped || if (!serverStamped ||
kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled || kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled ||
!FieldIsInVerbClass(field, verb)) { !FieldIsInVerbClass(field, verb)) {
// P5c (gt, CONTRACT-P5C §6 layer 1): under a server stamp, a stale read of a field
// whose row is RECORD-SUPPLIED or APPLIER-DERIVED - i.e. a value a pushed record
// DOES carry, read where this verb's stamp does not cover it - is a role violation
// named by its surface, not a generic unmigrated read: the wire already owns the
// answer, so reaching past it into the residual fill is rule E's shape. A
// BARRIER-PULLED field outside the verb's class and a FATAL field keep the poison
// Fatal - those are the stamp table's own verdicts, not a role's overreach.
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[static_cast<SizeT>(field)];
if (serverStamped && (ownership == MGPipeFieldOwnership::kRecordSupplied ||
ownership == MGPipeFieldOwnership::kApplierDerived)) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"%s\"} - the server read this field stale "
"at %s, but a pushed record carries it (the row is %s): the read reached "
"the client's residual fill for a value the wire already owns",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb),
MGPipeFieldOwnershipName(ownership));
std::abort();
}
MGPipeInputPoisonFatalForVerb(field, verb); MGPipeInputPoisonFatalForVerb(field, verb);
} }
CountBarrierPull(field, verb); CountBarrierPull(field, verb);
+23
View File
@@ -48,6 +48,9 @@
// lanes must keep answering exactly what they answered before. // lanes must keep answering exactly what they answered before.
#include <MG_Remote/Client/CapsMirror.h> #include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Client/WireTables.h> #include <MG_Remote/Client/WireTables.h>
// P5c gt (CONTRACT-P5C §6 layer 2, audit A1): the client-side gPipeInputs check consults
// InBarrierWait() and ApplyThreadIsInsideApplier() - both live here.
#include <MG_Remote/Client/ClientSession.h>
// P5c ev (CONTRACT-P5C §4.2): RecordError's transport arm posts kEventGlError through the // P5c ev (CONTRACT-P5C §4.2): RecordError's transport arm posts kEventGlError through the
// server session, and InvalidateCompileEnv's forward is deleted with an active transport. // server session, and InvalidateCompileEnv's forward is deleted with an active transport.
#include <MG_Remote/Server/ServerSession.h> #include <MG_Remote/Server/ServerSession.h>
@@ -532,6 +535,12 @@ namespace MobileGL::MG_Pipe {
// an unrelated frontend write. // an unrelated frontend write.
void MGPipeNoteFrontendMutation(MGPipeInputField field) { void MGPipeNoteFrontendMutation(MGPipeInputField field) {
PipeInputs& inputs = gPipeInputs; PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, layer 2): the single-field refresh is a client write into gPipeInputs too -
// same gate as the fill.
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
"MGPipeNoteFrontendMutation");
#endif
auto* ctx = LiveContext(); auto* ctx = LiveContext();
if (ctx == nullptr) return; if (ctx == nullptr) return;
const auto verb = inputs.CurrentVerb(); const auto verb = inputs.CurrentVerb();
@@ -1827,6 +1836,11 @@ namespace MobileGL::MG_Pipe {
void MGPipeLeaveVerb() { void MGPipeLeaveVerb() {
PipeInputs& inputs = gPipeInputs; PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_BUILD_DISAGGREGATED
// Same layer-2 gate as the fill: the serial bump and the verb reset below are writes
// into gPipeInputs (gt).
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("MGPipeLeaveVerb");
#endif
#if MOBILEGL_PIPE_POISON #if MOBILEGL_PIPE_POISON
// Same bump the next fill would make, without a verb to fill from: no field is // Same bump the next fill would make, without a verb to fill from: no field is
// stamped, so every stamp this verb made falls behind the serial. // stamped, so every stamp this verb made falls behind the serial.
@@ -2657,6 +2671,15 @@ namespace MobileGL::MG_Pipe {
// ---- the validate point (P2 brief D1) ---- // ---- the validate point (P2 brief D1) ----
void MGPipeValidateForVerb(MGPipeVerb verb) { void MGPipeValidateForVerb(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs; PipeInputs& inputs = gPipeInputs;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 2): the residual fill is THE client-side write into
// gPipeInputs, and it is legal only because it runs before the record is published -
// under an armed barrier the apply thread's in-applier flag is provably down here. If
// that ever stops being true this is the check that says so, rather than the applier
// reading a half-written fill.
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
"MGPipeValidateForVerb");
#endif
ParsePoisonOmissionKnob(); ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY #if MOBILEGL_PIPE_VERIFY
ArmVerify(); ArmVerify();
@@ -969,6 +969,25 @@ namespace MobileGL::MG_Remote::Client {
Bool ClientSession::ApplyThreadIsInsideApplier() { Bool ClientSession::ApplyThreadIsInsideApplier() {
return g_applyThreadInsideApplier.load(std::memory_order_acquire); return g_applyThreadInsideApplier.load(std::memory_order_acquire);
} }
void ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface) {
if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block inside a verb
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
if (!ClientSessionInstance().Started()) return; // the bring-up window pre-dates the roles
// MOBILEGL_IPC_VERB_BARRIER=0 is R-1's NEGATIVE CONTROL: the single-writer rule is off
// by the operator's own hand there, and EmitAndWait's Fatal{BarrierViolation} owns the
// red. Firing here instead would pre-empt the control's evidence line.
if (!ClientSessionInstance().BarrierArmed()) return;
if (!ApplyThreadIsInsideApplier()) return;
if (InBarrierWait()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched gPipeInputs "
"(%s) while the apply thread was inside the applier and this thread was not in a "
"barrier wait. R-1's barrier is the only thing that makes one process-wide "
"gPipeInputs legal (CONTRACT-P5 table 3); a touch in this window races the "
"applier's own reads of it",
surface);
std::abort();
}
void ClientSession::NoteApplyThreadEnteredApplier() { void ClientSession::NoteApplyThreadEnteredApplier() {
g_applyThreadInsideApplier.store(true, std::memory_order_release); g_applyThreadInsideApplier.store(true, std::memory_order_release);
} }
+14
View File
@@ -128,6 +128,20 @@ namespace MobileGL::MG_Remote::Client {
static Bool InBarrierWait(); static Bool InBarrierWait();
static Bool ApplyThreadIsInsideApplier(); static Bool ApplyThreadIsInsideApplier();
// P5c (gt, CONTRACT-P5C §6 layer 2 / audit A1): the sentence above's second half,
// wired. A GL-thread touch of gPipeInputs while the apply thread is inside the
// applier and THIS thread is not in a barrier wait is Fatal{RoleViolation,
// "gPipeInputs"} - the barrier is the only thing that makes one process-wide
// gPipeInputs legal (CONTRACT-P5 table 3), and a touch in that window races the
// applier's own reads of it. The residual fill and the verify harness are legal by
// timing, not by exemption: they run before the record is published, which under an
// armed barrier is a moment the flag is provably down (the applier drops it before
// appliedSeq advances past the record the client last waited on). No-op when no armed
// split session is live, when the caller IS the apply thread, and when the barrier is
// disarmed - MOBILEGL_IPC_VERB_BARRIER=0 is R-1's negative control and its red belongs
// to Fatal{BarrierViolation}, not to this check.
static void RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface);
// ---- c1's additions --------------------------------------------------------------- // ---- c1's additions ---------------------------------------------------------------
// The apply thread's half of R-1's invariant. v1's apply loop brackets its // The apply thread's half of R-1's invariant. v1's apply loop brackets its
@@ -8,10 +8,50 @@
#include "MipmapStorage.h" #include "MipmapStorage.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#endif
namespace MobileGL { namespace MobileGL {
namespace MG_State { namespace MG_State {
namespace GLState { namespace GLState {
namespace { namespace {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 1): the frontend TextureObjectMipmap's
// mutable surfaces - the maps, the storage (re)definitions and the
// storage-dirty model - are a layer-1 surface, and MipmapStorage is the one
// funnel every concrete texture class reaches them through (the cube's six
// faces included, and a view's forwards land on its owner's storage). With an
// active transport the apply thread calling one is
// Fatal{RoleViolation, "texture-legacy-arm"} - the texture twin of hd's
// BufferObject guard (BufferObject.cpp): the server reads its own staged
// texture shadow and the descriptor (tx), never this memory. Client-thread
// callers (the GL thread's own state) are unaffected, and the guard compiles
// out of every non-split build, so the pull build's bytes do not move (G1).
//
// The message names the TEXTURE-side method, not this storage's: the pinned
// surface list (MG_Test/Wire/RemoteClientTest.cpp's RemoteGuards) is written
// in TextureObjectMipmap's vocabulary.
void RefuseLegacyTextureArmFromApplyThread(const char* surface) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return;
// The named exemption (MipmapStorage.h): Magma's texture sync is tx's
// declared leftover and retires with P7.
if (MGPipeTextureLegacyArmScope::Active()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"texture-legacy-arm\"} - the apply thread "
"called TextureObjectMipmap::%s on a frontend object. With an active "
"transport the server reads the staged-texture store and the resource "
"descriptor (CONTRACT-P5C §2, rule E); a frontend texture's level "
"storage is client memory and this arm is monolith-only",
surface);
std::abort();
}
#endif
// Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in // Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in
// every axis: merging abutting boxes keeps scanline/tile write patterns // every axis: merging abutting boxes keeps scanline/tile write patterns
// as one rect instead of a picket fence. // as one rect instead of a picket fence.
@@ -28,11 +68,32 @@ namespace MobileGL {
} }
} // namespace } // namespace
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
thread_local Uint32 g_textureLegacyArmScopeDepth = 0;
}
MGPipeTextureLegacyArmScope::MGPipeTextureLegacyArmScope() {
++g_textureLegacyArmScopeDepth;
}
MGPipeTextureLegacyArmScope::~MGPipeTextureLegacyArmScope() {
--g_textureLegacyArmScopeDepth;
}
Bool MGPipeTextureLegacyArmScope::Active() {
return g_textureLegacyArmScopeDepth != 0;
}
#endif
SizeT MipmapStorage::GetLevelCount() const { SizeT MipmapStorage::GetLevelCount() const {
return m_data.size(); return m_data.size();
} }
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) { void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("AllocateStorage");
#endif
// Grow only. GL respecifies exactly the level it is handed, so allocating level 0 // Grow only. GL respecifies exactly the level it is handed, so allocating level 0
// must not disturb the levels above it - but resize() shrinks as readily as it // must not disturb the levels above it - but resize() shrinks as readily as it
// grows, so this used to truncate the whole chain to a single level. Callers that // grows, so this used to truncate the whole chain to a single level. Callers that
@@ -125,6 +186,9 @@ namespace MobileGL {
} }
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("TruncateMipmapLevels");
#endif
if (levelCount >= m_data.size()) return; if (levelCount >= m_data.size()) return;
m_data.resize(levelCount); m_data.resize(levelCount);
@@ -138,6 +202,9 @@ namespace MobileGL {
} }
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("UpdateMipmapSubData");
#endif
auto& targetData = m_data; auto& targetData = m_data;
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range"); MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
auto& levelData = targetData[level]; auto& levelData = targetData[level];
@@ -152,6 +219,9 @@ namespace MobileGL {
} }
void* MipmapStorage::MapData(Uint level) { void* MipmapStorage::MapData(Uint level) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("MapMipmapData");
#endif
auto& targetData = m_data; auto& targetData = m_data;
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range"); MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
auto& levelData = targetData[level]; auto& levelData = targetData[level];
@@ -170,6 +240,9 @@ namespace MobileGL {
} }
void MipmapStorage::MarkDirty(Uint level, bool dirty) { void MipmapStorage::MarkDirty(Uint level, bool dirty) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("MarkStorageDirty");
#endif
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range"); MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range");
m_isDirty[level] = dirty; m_isDirty[level] = dirty;
if (level < m_dirtyRegions.size()) { if (level < m_dirtyRegions.size()) {
@@ -191,11 +264,17 @@ namespace MobileGL {
} }
bool MipmapStorage::IsDirty(Uint level) const { bool MipmapStorage::IsDirty(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("IsStorageDirty");
#endif
MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range"); MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range");
return m_isDirty[level]; return m_isDirty[level];
} }
void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) { void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("MarkStorageDirtyRegion");
#endif
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range"); MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range");
const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0}; const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
MipmapDirtyRegion incoming; MipmapDirtyRegion incoming;
@@ -280,11 +359,17 @@ namespace MobileGL {
} }
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const { MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetStorageDirtyRegion");
#endif
if (level >= m_dirtyRegions.size()) return {}; if (level >= m_dirtyRegions.size()) return {};
return m_dirtyRegions[level]; return m_dirtyRegions[level];
} }
SizeT MipmapStorage::GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const { SizeT MipmapStorage::GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyTextureArmFromApplyThread("GetStorageDirtyRects");
#endif
if (outRects == nullptr || level >= m_dirtyRects.size() || level >= m_dirtyRegions.size()) { if (outRects == nullptr || level >= m_dirtyRects.size() || level >= m_dirtyRegions.size()) {
return 0; return 0;
} }
@@ -38,6 +38,28 @@ namespace MobileGL {
} }
}; };
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 1): the NAMED EXEMPTION to the texture legacy-arm
// guard, the texture analogue of hd's MGPipeFrontendKeyedRegistryScope
// (MG_Impl/Pipe/SlotAllocator.h). Magma's texture sync (VkTextureManager::SyncTexture
// and the UploadDirtyMipLevels it drives) still reads and clears the CLIENT's mip
// shadow - the one texture family tx did not migrate (CONTRACT-P5C §2 names Espryt's
// sync reads and Magma's T5 writes; Magma's upload read path is absent from it), and
// inproc was green on it because the barrier and the shared address space held.
// Inside this scope the guard's surfaces stay legal - read-only-or-clean, barrier-
// held, and greppable as exactly this name; retiring the scope is P7's server-side
// Magma texture sync. Every OTHER apply-thread touch of a guarded surface is still
// Fatal{RoleViolation, "texture-legacy-arm"}.
class MGPipeTextureLegacyArmScope {
public:
MGPipeTextureLegacyArmScope();
~MGPipeTextureLegacyArmScope();
MGPipeTextureLegacyArmScope(const MGPipeTextureLegacyArmScope&) = delete;
MGPipeTextureLegacyArmScope& operator=(const MGPipeTextureLegacyArmScope&) = delete;
static Bool Active();
};
#endif
class MipmapStorage { class MipmapStorage {
public: public:
SizeT GetLevelCount() const; SizeT GetLevelCount() const;
@@ -47,6 +47,7 @@
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h> #include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Pipe/PipeMutation.h> #include <MG_Pipe/PipeMutation.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <Init.h> #include <Init.h>
#include <MG_Impl/EGLImpl/EGLImpl.h> #include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
@@ -1835,6 +1836,7 @@ MGL_BUFFER_GUARD_TEST(BufferIsMappedFromTheApplyThreadIsFatalByName, 704, (void)
MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial()) MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial())
MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706, MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706,
buffer.SyncPersistentMappedRange()) buffer.SyncPersistentMappedRange())
MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent())
#undef MGL_BUFFER_GUARD_TEST #undef MGL_BUFFER_GUARD_TEST
TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) { TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
@@ -1846,6 +1848,88 @@ TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
}); });
ExpectNamedAbort(child, "Fatal{RoleViolation, \"caps-mirror\"}"); ExpectNamedAbort(child, "Fatal{RoleViolation, \"caps-mirror\"}");
} }
// P5c (gt, CONTRACT-P5C §6 layer 1): the TEXTURE family's object-surface list, in executable
// form - this block IS the pinned list §5.4(b) asks for (the buffer family's five are the
// MGL_BUFFER_GUARD_TEST block above). Every drive shares the buffer family's shape: the texture
// is constructed on the CLIENT thread (its resource_create mint and emission are legal
// client-side), and only the probed method runs on the apply thread, where the guard fires
// BEFORE any level precondition could - the method names below are exactly the ones the guard
// (MipmapStorage.cpp's RefuseLegacyTextureArmFromApplyThread) is hung on:
//
// AllocateStorage, TruncateMipmapLevels, UpdateMipmapSubData, MapMipmapData,
// MarkStorageDirty, MarkStorageDirtyRegion, IsStorageDirty, GetStorageDirtyRegion,
// GetStorageDirtyRects
//
// Deliberately NOT in the list: the shape reads (GetMipmapTexelSize / GetMipmapByteSize /
// GetMipmapLevelCount / GetUploadTargets / GetTarget / IsComplete), which the pinned
// BARRIER-PULLED object-class rows still answer through the unit-object pointer
// (FieldOwnershipTest's list, P3b/P4b/P7) - the per-draw binding walk reads them every draw.
#define MGL_TEXTURE_GUARD_TEST(Name, Id, Call) \
TEST(RemoteGuards, Name) { \
const auto child = RunInChild([] { \
StartControlSession(); \
MG_State::GLState::TextureObject2D texture(Id); \
Srv::ServerLoopInstance().RunOnApplyThread( \
[](void* self) { \
auto& texture = *static_cast<MG_State::GLState::TextureObject2D*>(self); \
Call; \
return MOBILEGL_OK; \
}, \
&texture); \
ClientSessionInstance().Stop(); \
}); \
ExpectNamedAbort(child, "Fatal{RoleViolation, \"texture-legacy-arm\"}"); \
}
MGL_TEXTURE_GUARD_TEST(TextureAllocateStorageFromTheApplyThreadIsFatalByName, 711,
texture.AllocateStorage(TextureUploadTarget::Texture2D, 0,
{{4, 4, 1}, 64}))
MGL_TEXTURE_GUARD_TEST(TextureTruncateMipmapLevelsFromTheApplyThreadIsFatalByName, 712,
texture.TruncateMipmapLevels(TextureUploadTarget::Texture2D, 1))
MGL_TEXTURE_GUARD_TEST(TextureUpdateMipmapSubDataFromTheApplyThreadIsFatalByName, 713,
texture.UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0, {}))
MGL_TEXTURE_GUARD_TEST(TextureMapMipmapDataFromTheApplyThreadIsFatalByName, 714,
(void)texture.MapMipmapData(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureMarkStorageDirtyFromTheApplyThreadIsFatalByName, 715,
texture.MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true))
MGL_TEXTURE_GUARD_TEST(TextureMarkStorageDirtyRegionFromTheApplyThreadIsFatalByName, 716,
texture.MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0,
{0, 0, 0}, {1, 1, 1}))
MGL_TEXTURE_GUARD_TEST(TextureIsStorageDirtyFromTheApplyThreadIsFatalByName, 717,
(void)texture.IsStorageDirty(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRegionFromTheApplyThreadIsFatalByName, 718,
(void)texture.GetStorageDirtyRegion(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRectsFromTheApplyThreadIsFatalByName, 719,
(void)texture.GetStorageDirtyRects(TextureUploadTarget::Texture2D, 0,
nullptr, 0))
#undef MGL_TEXTURE_GUARD_TEST
// P5c (gt, CONTRACT-P5C §6 layer 2 / audit A1): the client-side half of the gPipeInputs
// single-writer rule. The apply thread is NOT actually inside the applier in this case - the
// flag is raised by hand, which is the exact overlap window the check exists to refuse: a fill
// that ran while a real apply was in flight would race the applier's reads of gPipeInputs.
TEST(RemoteGuards, ClientPipeInputsFillWhileTheApplierOwnsItIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
ClientSession::NoteApplyThreadEnteredApplier();
MGPipeValidateForVerb(MGPipeVerb::Clear);
ClientSession::NoteApplyThreadLeftApplier();
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"gPipeInputs\"}");
}
// The same fill with the flag down is the legal shape - this is the control that keeps the
// check from being "abort unconditionally".
TEST(RemoteGuards, ClientPipeInputsFillWithTheApplierIdleIsAllowed) {
const auto child = RunInChild([] {
StartControlSession();
MGPipeValidateForVerb(MGPipeVerb::Clear);
MGPipeLeaveVerb();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#endif #endif