mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
Merge branch 'p5c-gt' into feat/disaggregated (P5c gt: role guards, InBarrierWait wired, rsp measured)
This commit is contained in:
+116
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user