mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
[MG_State, MG_Backend, MG_Remote, MG_Impl, CI] (Disaggregated): P5c gt - the role guards close the phase
Layer 1 arms the texture object surfaces at MipmapStorage (the one point every concrete
texture class and view forwards through): AllocateStorage, TruncateMipmapLevels,
UpdateMipmapSubData, MapMipmapData, MarkStorageDirty(Region), IsStorageDirty,
GetStorageDirtyRegion(s) take Fatal{RoleViolation, texture-legacy-arm} from the apply
thread with an active transport. What the lane then exposed is fixed, not exempted: the
completeness gate answers from the staged store's defined-ness, the two logging reads go
through the descriptor macro, and the remint replay loop keeps its named
Fatal{UnmigratedEmulation} ahead of the guard. Magma's texture sync read path is the one
named exemption (MGPipeTextureLegacyArmScope, P7). Layer 2 wires InBarrierWait as the
client half of the gPipeInputs single-writer rule (RefusePipeInputsTouchWhileApplierOwnsIt:
applier owns it AND this thread not in a barrier wait => Fatal{RoleViolation,
gPipeInputs}; the barrier-off negative control is deferred to its own Fatal), and stale
reads of RECORD-SUPPLIED / APPLIER-DERIVED fields under a server stamp now report
RoleViolation with the field named. The RemoteGuards suite pins all nine texture surfaces
plus HasDefinedContent by name, and a negative control for the fill window. CI gains the
integration-split-strict job: unit lane as the hard gate under MOBILEGL_IPC_STRICT_ERRORS=1,
the scenario lane expected-red with the marker asserted (rewritable to plain green once P7
retires the object-class rows). rsp is per-frame in the stats line and measured on the
locally available traces (bsl in-world 948.5/frame avg over 123 frames, complementary
1591.9, iterationrp 286 until its named remint-pull Fatal); value-class pulls are zero by
rv's pinned list, what remains is the object-class fifteen.
Evidence: unit 2187/2187 (also under STRICT_ERRORS=1); integration-split 111/111; red-once
per layer (the texture guard and the gPipeInputs guard each turned their named death test
red and restored green). CONTRACT-P5C sections 5.4, 6.
This commit is contained in:
+116
-1
@@ -1,4 +1,4 @@
|
||||
name: Test
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1116,6 +1116,121 @@ jobs:
|
||||
path: /tmp/core.*
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -5892,6 +5892,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"pending set - they would be allocated empty",
|
||||
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
|
||||
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
|
||||
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
|
||||
// never be sampled or read back. Sync whenever some level holds an image; the per-level
|
||||
// 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())) {
|
||||
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
@@ -7321,7 +7398,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
default:
|
||||
MGLOG_E_ONCE("Unhandled texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
|
||||
MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
|
||||
break;
|
||||
}
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
|
||||
@@ -8127,7 +8204,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
default:
|
||||
MGLOG_E_ONCE("Unhandled texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
|
||||
MG_Util::ConvertTextureTargetToString(MGB_TEXTURE_TARGET(stateTextureObject)).c_str());
|
||||
break;
|
||||
}
|
||||
if (ringStaged) {
|
||||
|
||||
@@ -1744,6 +1744,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -147,6 +147,23 @@ namespace MobileGL::MG_Pipe {
|
||||
if (!serverStamped ||
|
||||
kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled ||
|
||||
!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);
|
||||
}
|
||||
CountBarrierPull(field, verb);
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
// lanes must keep answering exactly what they answered before.
|
||||
#include <MG_Remote/Client/CapsMirror.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
|
||||
// server session, and InvalidateCompileEnv's forward is deleted with an active transport.
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
@@ -532,6 +535,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// an unrelated frontend write.
|
||||
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
|
||||
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();
|
||||
if (ctx == nullptr) return;
|
||||
const auto verb = inputs.CurrentVerb();
|
||||
@@ -1827,6 +1836,11 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void MGPipeLeaveVerb() {
|
||||
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
|
||||
// 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.
|
||||
@@ -2657,6 +2671,15 @@ namespace MobileGL::MG_Pipe {
|
||||
// ---- the validate point (P2 brief D1) ----
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb) {
|
||||
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();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
ArmVerify();
|
||||
|
||||
@@ -969,6 +969,25 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Bool ClientSession::ApplyThreadIsInsideApplier() {
|
||||
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() {
|
||||
g_applyThreadInsideApplier.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,20 @@ namespace MobileGL::MG_Remote::Client {
|
||||
static Bool InBarrierWait();
|
||||
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 ---------------------------------------------------------------
|
||||
|
||||
// The apply thread's half of R-1's invariant. v1's apply loop brackets its
|
||||
|
||||
@@ -8,10 +8,50 @@
|
||||
|
||||
#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 MG_State {
|
||||
namespace GLState {
|
||||
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
|
||||
// every axis: merging abutting boxes keeps scanline/tile write patterns
|
||||
// as one rect instead of a picket fence.
|
||||
@@ -28,11 +68,32 @@ namespace MobileGL {
|
||||
}
|
||||
} // 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 {
|
||||
return m_data.size();
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
@@ -125,6 +186,9 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyTextureArmFromApplyThread("TruncateMipmapLevels");
|
||||
#endif
|
||||
if (levelCount >= m_data.size()) return;
|
||||
|
||||
m_data.resize(levelCount);
|
||||
@@ -138,6 +202,9 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyTextureArmFromApplyThread("UpdateMipmapSubData");
|
||||
#endif
|
||||
auto& targetData = m_data;
|
||||
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
||||
auto& levelData = targetData[level];
|
||||
@@ -152,6 +219,9 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void* MipmapStorage::MapData(Uint level) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyTextureArmFromApplyThread("MapMipmapData");
|
||||
#endif
|
||||
auto& targetData = m_data;
|
||||
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
||||
auto& levelData = targetData[level];
|
||||
@@ -170,6 +240,9 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
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");
|
||||
m_isDirty[level] = dirty;
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
@@ -191,11 +264,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
bool MipmapStorage::IsDirty(Uint level) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyTextureArmFromApplyThread("IsStorageDirty");
|
||||
#endif
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range");
|
||||
return m_isDirty[level];
|
||||
}
|
||||
|
||||
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");
|
||||
const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
|
||||
MipmapDirtyRegion incoming;
|
||||
@@ -280,11 +359,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyTextureArmFromApplyThread("GetStorageDirtyRegion");
|
||||
#endif
|
||||
if (level >= m_dirtyRegions.size()) return {};
|
||||
return m_dirtyRegions[level];
|
||||
}
|
||||
|
||||
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()) {
|
||||
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 {
|
||||
public:
|
||||
SizeT GetLevelCount() const;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <Init.h>
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.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(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706,
|
||||
buffer.SyncPersistentMappedRange())
|
||||
MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent())
|
||||
#undef MGL_BUFFER_GUARD_TEST
|
||||
|
||||
TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
|
||||
@@ -1846,6 +1848,88 @@ TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
|
||||
});
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user